Skill: web3aivc/rapidapi (version 0.1.0)

--- name: rapidapi-universal-skill description: Template-driven RapidAPI client with auto-registered actions and a universal call entrypoint metadata: {"openclaw":{"requires":{"env":["RAPIDAPI_KEY"]},"primaryEnv":"RAPIDAPI_KEY"}} --- Use `{baseDir}/index.js` to call RapidAPI with templates from `{baseDir}/templates`. Prefer this skill when the task involves RapidAPI endpoints or template-defined actions. ## What This Skill Actually Does This skill is a minimal RapidAPI client that turns RapidAPI endpoint definitions into callable actions. It is meant for: - converting a RapidAPI endpoint into a stable action name - standardizing inputs into query/body/header/path params - returning a consistent `ok/status/data/error/meta` shape It is not a server. It is a small local client you can call from scripts or other skills. ## Key Capabilities - Auto-registers templates from `{baseDir}/templates/*.json` - `listActions()` enumerates all registered actions with schemas - `callAction(name, params)` calls a template-defined endpoint - `callRapidApi(payload)` allows direct RapidAPI calls without a template - `scripts/import-endpoint.js` converts a RapidAPI endpoint JSON payload into a template file ## Basic Usage Use config-driven init (recommended): ```js import { createRapidApiSkill } from "{baseDir}/index.js"; import config from "{baseDir}/config.json" assert { type: "json" }; const skill = await createRapidApiSkill({ config }); const res = await skill.callAction("get_user_tweets", { user: "2455740283", count: 20 }); ``` Or direct call (no template): ```js const skill = await createRapidApiSkill({ config }); const res = await skill.callRapidApi({ host: "twitter241.p.rapidapi.com", path: "/user-tweets", method: "GET", query: { user: "2455740283", count: 20 } }); ``` ## Template Design Notes Templates are plain JSON. They should contain: - `name`, `host`, `path`, `method` - `querySchema` (and optionally `bodySchema`, `headerSchema`, `pathParams`) Example snippet: ```json { "name": "get_user_tweets", "host": "twitter241.p.rapidapi.com", "path": "/user-tweets", "method": "GET", "querySchema": { "user": {"type": "string", "required": true}, "count": {"type": "number", "required": true}, "cursor": {"type": "string"} } } ``` ## Where It Fits Use this skill when you need a consistent, reusable RapidAPI interface without building a backend. It is especially useful for: - social data APIs (X/Twitter, TikTok, LinkedIn) - search/aggregation APIs - repeated RapidAPI calls across multiple tasks or workflows

path: index.js
content: import { callRapidApi } from "./lib/engine.js";
import {
  loadRegistry,
  requireAction,
  extractParams,
  applyParamMapping,
  applyPathParams
} from "./lib/registry.js";
import { getByPath } from "./lib/normalize.js";
import { toErrorResult, HttpError } from "./lib/errors.js";

export async function createRapidApiSkill(options = {}) {
  const primaryEnv = options.primaryEnv || "RAPIDAPI_KEY";
  const injectedEnv = options.env || {};
  const config = options.config || {};
  const rapidApiKey =
    options.apiKey ||
    options.rapidApiKey ||
    config.rapidApiKey ||
    injectedEnv[primaryEnv] ||
    process.env[primaryEnv];
  const templatesDir =
    options.templatesDir || config.templatesDir || "./templates";
  const allowNonRapidApiHosts =
    typeof options.allowNonRapidApiHosts === "boolean"
      ? options.allowNonRapidApiHosts
      : typeof config.allowNonRapidApiHosts === "boolean"
      ? config.allowNonRapidApiHosts
      : String(process.env.ALLOW_NON_RAPIDAPI_HOSTS || "true").toLowerCase() === "true";
  const timeoutMs =
    typeof options.timeoutMs === "number"
      ? options.timeoutMs
      : typeof config.timeoutMs === "number"
      ? config.timeoutMs
      : process.env.RAPIDAPI_TIMEOUT_MS
      ? Number(process.env.RAPIDAPI_TIMEOUT_MS)
      : 15000;

  if (!rapidApiKey) {
    throw new Error(`${primaryEnv} is required`);
  }

  const registry = await loadRegistry(templatesDir);

  async function callAction(name, input = {}) {
    const action = requireAction(registry, name);
    const meta = {
      host: action.template.host,
      path: action.template.path,
      method: action.template.method || "GET"
    };

    try {
      if (input && typeof input !== "object") {
        throw new HttpError(400, "Input must be an object");
      }

      const mapping = applyParamMapping(input, action.template.paramMapping);

      const query = {
        ...extractParams(input, action.template.querySchema),
        ...mapping.query
      };
      const body = {
        ...extractParams(input, action.template.bodySchema),
        ...mapping.body
      };
      const pathParams = {
        ...extractParams(input, action.template.pathParams),
        ...mapping.path
      };
      const headers = {
        ...(action.template.headers || {}),
        ...extractParams(input, action.template.headerSchema),
        ...mapping.header
      };

      const finalPath = applyPathParams(action.template.path, pathParams);

      const result = await callRapidApi(
        {
          host: action.template.host,
          path: finalPath,
          method: action.template.method,
          query,
          body,
          headers,
          timeoutMs: action.template.timeoutMs,
          responseType: action.template.response?.type || "json"
        },
        rapidApiKey,
        allowNonRapidApiHosts,
        timeoutMs
      );

      const errorPath = action.template.response?.errorPath;
      if (errorPath) {
        const maybeError = getByPath(result.data, errorPath);
        if (maybeError) {
          throw new HttpError(502, "RapidAPI error payload", maybeError);
        }
      }

      if (action.template.response?.normalize) {
        const normalized = {};
        for (const [key, pathValue] of Object.entries(
          action.template.response.normalize
        )) {
          normalized[key] = getByPath(result.data, pathValue);
        }
        result.data = normalized;
      } else if (action.template.response?.dataPath) {
        result.data = getByPath(result.data, action.template.response.dataPath);
      }

      return result;
    } catch (err) {
      return toErrorResult(err, meta);
    }
  }

  async function callRapidApiDirect(input) {
    const meta = { host: input?.host || "unknown", path: input?.path || "unknown", method: input?.method || "unknown" };
    try {
      return await callRapidApi(
        input,
        rapidApiKey,
        allowNonRapidApiHosts,
        timeoutMs
      );
    } catch (err) {
      return toErrorResult(err, meta);
    }
  }

  return {
    listActions: () => registry.list(),
    callAction,
    callRapidApi: callRapidApiDirect
  };
}


path: README.md
content: # RapidAPI Universal Skill (Minimal)

Minimal-dependency RapidAPI skill core. No server, no frameworks, no build step.

## What you get
- A universal RapidAPI request engine (no server required)
- Declarative templates that map to stable action names
- Automatic action registration from `templates/*.json`
- A power-user entrypoint: `callRapidApi` for ad‑hoc calls

## When to use this
Use this skill if you want a repeatable, programmatic interface over RapidAPI:
- You need to call the same endpoint across tasks without retyping host/path/method
- You want to document inputs and defaults as a template
- You want to share a portable template library in version control

Not a good fit if you need a long‑running API server or webhooks.

## Install / Use
Node.js 18+ required (uses global `fetch`).

Create `config.json` from `config.example.json` and pass it in:
```json
{
  "rapidApiKey": "your_rapidapi_key",
  "templatesDir": "./templates",
  "timeoutMs": 15000,
  "allowNonRapidApiHosts": true
}
```

Example:
```js
import { createRapidApiSkill } from "./index.js";
import config from "./config.json" assert { type: "json" };

const skill = await createRapidApiSkill({ config });

console.log(skill.listActions());

const res = await skill.callAction("toktok_user_info", { username: "example_user" });
console.log(res);

const direct = await skill.callRapidApi({
  host: "linkedin-data-api.p.rapidapi.com",
  path: "/get-profile",
  method: "GET",
  query: { url: "https://www.linkedin.com/in/example/" }
});
console.log(direct);
```

## Response Shape (stable contract)
Every call returns this shape so callers can handle errors consistently:
```json
{
  "ok": true,
  "status": 200,
  "data": {},
  "meta": { "host": "...", "path": "...", "method": "GET" }
}
```

Errors always follow:
```json
{
  "ok": false,
  "status": 400,
  "error": { "message": "...", "details": {} },
  "meta": { "host": "...", "path": "...", "method": "GET" }
}
```

## How templates map to requests
- `querySchema` → query string params
- `bodySchema` → JSON body (for non‑GET methods)
- `headerSchema` → extra request headers
- `pathParams` → `{param}` substitution in the path

`paramMapping` lets you map a single input into a specific bucket:
```json
{
  "paramMapping": { "profileUrl": "query.url" }
}
```

## OpenClaw compatibility
This skill follows OpenClaw conventions:
- `SKILL.md` with single-line `metadata` JSON
- `metadata.openclaw.primaryEnv` is `RAPIDAPI_KEY`
- You can pass `apiKey` or inject env via `skills.entries.<name>.env` / `apiKey`

Programmatic usage with injected env:
```js
const skill = await createRapidApiSkill({
  [REDACTED_SECRET]" // maps to primaryEnv
});
```

Programmatic usage with config:
```js
import config from "./config.json" assert { type: "json" };
const skill = await createRapidApiSkill({ config });
```

## Scripts
List actions:
```bash
node scripts/list.js
```

Call a preset action:
```bash
echo '{\"action\":\"toktok_user_info\",\"params\":{\"username\":\"example_user\"}}' | node scripts/call.js
```

Call a direct RapidAPI request:
```bash
echo '{\"rapidapi\":{\"host\":\"linkedin-data-api.p.rapidapi.com\",\"path\":\"/get-profile\",\"method\":\"GET\",\"query\":{\"url\":\"https://www.linkedin.com/in/example/\"}}}' | node scripts/call.js
```

## Import endpoint JSON (auto-generate template)
If you already have a RapidAPI endpoint JSON payload (from the RapidAPI UI or export), you can generate a template file automatically.\n\nThe input must include a top-level `host`.\n\n```bash
echo '{\"host\":\"twitter241.p.rapidapi.com\",\"data\":{\"endpoint\":{\"name\":\"Get User Tweets\",\"method\":\"GET\",\"route\":\"/user-tweets\",\"params\":{\"parameters\":[{\"name\":\"user\",\"condition\":\"REQUIRED\",\"paramType\":\"STRING\"},{\"name\":\"count\",\"condition\":\"REQUIRED\",\"paramType\":\"STRING\",\"value\":\"20\"},{\"name\":\"cursor\",\"condition\":\"OPTIONAL\",\"paramType\":\"STRING\"}]}}}}' | node scripts/import-endpoint.js
```

## Template system
Templates live in `templates/*.json` and are auto-registered at startup.

See details: `docs/template-schema.md`

## Error format
```json
{
  "ok": false,
  "status": 400,
  "error": {
    "message": "Missing required parameter: url",
    "details": {}
  }
}
```

## Security defaults
- Disallows overriding `X-RapidAPI-Key`
- Optional host restriction with `ALLOW_NON_RAPIDAPI_HOSTS=false`
- Method allow-list
- Body size and timeout limits


path: config.example.json
content: rapidApiKey: your_rapidapi_key
templatesDir: ./templates
timeoutMs: 15000
allowNonRapidApiHosts: yes

path: package.json
content: {
  "name": "rapidapi-universal-skill",
  "version": "0.2.0",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=18"
  }
}


path: templates/toktok_user_info.json
content: {
  "name": "toktok_user_info",
  "label": "Get TokTok user info",
  "description": "Fetch public user profile by username",
  "host": "x.toktokapi.com",
  "path": "/user/info",
  "method": "GET",
  "querySchema": {
    "username": {
      "type": "string",
      "required": true,
      "description": "TokTok username"
    }
  },
  "response": {
    "type": "json",
    "dataPath": "data"
  }
}


path: templates/linkedin_profile.json
content: {
  "name": "linkedin_profile",
  "label": "Get LinkedIn profile",
  "description": "Fetch LinkedIn profile from public URL",
  "host": "linkedin-data-api.p.rapidapi.com",
  "path": "/get-profile",
  "method": "GET",
  "querySchema": {
    "url": {
      "type": "string",
      "required": true,
      "description": "LinkedIn profile URL"
    }
  },
  "response": {
    "type": "json",
    "dataPath": "data"
  }
}


path: templates/toktok_user_posts.json
content: {
  "name": "toktok_user_posts",
  "label": "Get TokTok user posts",
  "description": "Fetch public posts by username",
  "host": "x.toktokapi.com",
  "path": "/user/posts",
  "method": "GET",
  "querySchema": {
    "username": {
      "type": "string",
      "required": true,
      "description": "TokTok username"
    },
    "count": {
      "type": "number",
      "required": false,
      "default": 20,
      "description": "Number of items"
    }
  },
  "response": {
    "type": "json",
    "dataPath": "data.items"
  }
}


path: lib/normalize.js
content: export function getByPath(obj, path) {
  if (!path) return obj;
  const parts = String(path).split(".").filter(Boolean);
  let current = obj;
  for (const part of parts) {
    if (current == null) return undefined;
    current = current[part];
  }
  return current;
}


path: lib/registry.js
content: import fs from "node:fs/promises";
import path from "node:path";
import { HttpError } from "./errors.js";

function isObject(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

export async function loadRegistry(templatesDir) {
  const actions = new Map();
  const resolvedDir = path.resolve(templatesDir);
  const entries = await fs.readdir(resolvedDir, { withFileTypes: true });

  for (const entry of entries) {
    if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
    const filePath = path.join(resolvedDir, entry.name);
    const raw = await fs.readFile(filePath, "utf-8");
    const json = JSON.parse(raw);

    if (!json || typeof json.name !== "string" || !json.name) {
      throw new Error(`Invalid template ${entry.name}: missing name`);
    }
    if (!json.host || !json.path) {
      throw new Error(`Invalid template ${entry.name}: missing host/path`);
    }
    if (json.enabled === false) continue;

    actions.set(json.name, { template: json });
  }

  return {
    actions,
    list: () =>
      Array.from(actions.values()).map((a) => ({
        name: a.template.name,
        label: a.template.label,
        description: a.template.description,
        querySchema: a.template.querySchema,
        bodySchema: a.template.bodySchema,
        pathParams: a.template.pathParams,
        headerSchema: a.template.headerSchema,
        tags: a.template.tags,
        version: a.template.version
      }))
  };
}

export function requireAction(registry, name) {
  const action = registry.actions.get(name);
  if (!action) {
    throw new HttpError(404, `Unknown action: ${name}`);
  }
  return action;
}

export function extractParams(input, schema) {
  const out = {};
  if (!schema) return out;
  if (!isObject(schema)) {
    throw new HttpError(400, "Invalid schema format");
  }
  for (const [key, rules] of Object.entries(schema)) {
    const value = input[key] ?? rules?.default;
    if (value === undefined || value === null) {
      if (rules?.required) {
        throw new HttpError(400, `Missing required parameter: ${key}`);
      }
      continue;
    }
    out[key] = value;
  }
  return out;
}

export function applyParamMapping(input, mapping) {
  const buckets = { query: {}, body: {}, path: {}, header: {} };
  if (!mapping) return buckets;
  if (!isObject(mapping)) {
    throw new HttpError(400, "Invalid paramMapping format");
  }
  for (const [inputKey, target] of Object.entries(mapping)) {
    const value = input[inputKey];
    if (value === undefined) continue;
    const [bucket, field] = String(target).split(".");
    if (!bucket || !field || !buckets[bucket]) {
      throw new HttpError(400, `Invalid paramMapping target: ${target}`);
    }
    buckets[bucket][field] = value;
  }
  return buckets;
}

export function applyPathParams(pathTemplate, params) {
  let out = pathTemplate;
  for (const [key, value] of Object.entries(params || {})) {
    out = out.replace(`{${key}}`, encodeURIComponent(String(value)));
  }
  return out;
}


path: lib/errors.js
content: export class HttpError extends Error {
  constructor(status, message, details) {
    super(message);
    this.status = status;
    this.details = details;
  }
}

export function toErrorResult(err, meta) {
  if (err instanceof HttpError) {
    return {
      ok: false,
      status: err.status,
      error: { message: err.message, details: err.details },
      meta
    };
  }
  return {
    ok: false,
    status: 500,
    error: { message: "Unexpected error", details: err },
    meta
  };
}


path: lib/engine.js
content: import { HttpError } from "./errors.js";

export async function callRapidApi(input, rapidApiKey, allowNonRapidApiHosts, defaultTimeoutMs) {
  const method = (input.method || "GET").toUpperCase();
  const meta = { host: input.host, path: input.path, method };

  if (!input.host || String(input.host).includes("/") || String(input.host).includes(":")) {
    throw new HttpError(400, "Invalid host");
  }

  if (!input.path || !String(input.path).startsWith("/")) {
    throw new HttpError(400, "Path must start with '/' ");
  }

  if (!allowNonRapidApiHosts) {
    const host = String(input.host);
    const isRapidApiHost =
      host.endsWith(".rapidapi.com") || host.endsWith(".p.rapidapi.com") || host === "rapidapi.com";
    if (!isRapidApiHost) {
      throw new HttpError(400, "Non-RapidAPI host blocked by policy");
    }
  }

  const url = new URL(`https://${input.host}${input.path}`);
  if (input.query) {
    for (const [key, value] of Object.entries(input.query)) {
      if (value === undefined || value === null) continue;
      url.searchParams.set(key, String(value));
    }
  }

  const headers = {
    "X-RapidAPI-Key": rapidApiKey,
    "X-RapidAPI-Host": input.host,
    ...(input.headers || {})
  };
  delete headers["x-rapidapi-key"];
  delete headers["X-RapidAPI-Key"];
  headers["X-RapidAPI-Key"] = rapidApiKey;

  const hasBody = !["GET", "DELETE"].includes(method);
  let body = undefined;

  if (hasBody && input.body !== undefined) {
    if (typeof input.body === "string" || input.body instanceof Uint8Array) {
      body = input.body;
    } else {
      if (!headers["Content-Type"]) {
        headers["Content-Type"] = "application/json";
      }
      body = JSON.stringify(input.body);
    }
  }

  const timeoutMs = input.timeoutMs ?? defaultTimeoutMs;
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeoutMs);

  let response;
  try {
    response = await fetch(url.toString(), { method, headers, body, signal: controller.signal });
  } finally {
    clearTimeout(id);
  }

  const contentType = response.headers.get("content-type") || "";
  let data = null;

  if (input.responseType === "text") {
    data = await response.text();
  } else if (input.responseType === "blob") {
    data = await response.arrayBuffer();
  } else if (contentType.includes("application/json")) {
    data = await response.json();
  } else {
    data = await response.text();
  }

  if (!response.ok) {
    throw new HttpError(response.status, "RapidAPI error", data);
  }

  return {
    ok: true,
    status: response.status,
    data,
    meta
  };
}


path: scripts/list.js
content: import { createRapidApiSkill } from "../index.js";

const skill = await createRapidApiSkill();
const actions = skill.listActions();

console.log(JSON.stringify({ ok: true, actions }, null, 2));


path: scripts/call.js
content: import { createRapidApiSkill } from "../index.js";

function readStdin() {
  return new Promise((resolve, reject) => {
    let data = "";
    process.stdin.setEncoding("utf8");
    process.stdin.on("data", (chunk) => (data += chunk));
    process.stdin.on("end", () => resolve(data));
    process.stdin.on("error", reject);
  });
}

const inputRaw = await readStdin();
if (!inputRaw) {
  console.error("Expected JSON input via stdin");
  process.exit(2);
}

let input;
try {
  input = JSON.parse(inputRaw);
} catch (err) {
  console.error("Invalid JSON input");
  process.exit(2);
}

const { action, params, rapidapi } = input;
const skill = await createRapidApiSkill();

if (action) {
  const result = await skill.callAction(action, params || {});
  console.log(JSON.stringify(result, null, 2));
  process.exit(result.ok ? 0 : 1);
}

if (rapidapi) {
  const result = await skill.callRapidApi(rapidapi);
  console.log(JSON.stringify(result, null, 2));
  process.exit(result.ok ? 0 : 1);
}

console.error("Input must include either 'action' or 'rapidapi'");
process.exit(2);


path: scripts/import-endpoint.js
content: import fs from "node:fs/promises";
import path from "node:path";

function readStdin() {
  return new Promise((resolve, reject) => {
    let data = "";
    process.stdin.setEncoding("utf8");
    process.stdin.on("data", (chunk) => (data += chunk));
    process.stdin.on("end", () => resolve(data));
    process.stdin.on("error", reject);
  });
}

function toSnake(name) {
  return name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .replace(/_+/g, "_");
}

function parseEndpoint(payload) {
  if (!payload) throw new Error("Missing payload");
  const endpoint = payload.endpoint || payload.data?.endpoint || payload;
  if (!endpoint || !endpoint.method || !endpoint.route) {
    throw new Error("Missing endpoint.method or endpoint.route");
  }
  return endpoint;
}

const raw = await readStdin();
if (!raw) {
  console.error("Expected JSON input via stdin");
  process.exit(2);
}

let input;
try {
  input = JSON.parse(raw);
} catch (_err) {
  console.error("Invalid JSON input");
  process.exit(2);
}

const endpoint = parseEndpoint(input);
const host = input.host || input.rapidApiHost || input.rapidapiHost;
if (!host) {
  console.error("Missing host. Provide {\"host\":\"api-host.p.rapidapi.com\"} at top-level.");
  process.exit(2);
}

const querySchema = {};
const params = endpoint.params?.parameters || [];
for (const p of params) {
  if (!p || !p.name) continue;
  const required = String(p.condition || "").toUpperCase() === "REQUIRED";
  const type = String(p.paramType || "STRING").toLowerCase();
  querySchema[p.name] = {
    type: ["string", "number", "boolean", "object", "array"].includes(type) ? type : "string",
    required,
    description: p.description || ""
  };
  if (!required && p.value !== undefined && p.value !== "") {
    querySchema[p.name].default = p.value;
  }
}

const name = toSnake(endpoint.name || endpoint.route || "endpoint");
const template = {
  name,
  label: endpoint.name || name,
  description: endpoint.description || "",
  host,
  path: endpoint.route,
  method: String(endpoint.method).toUpperCase(),
  querySchema,
  response: {
    type: "json"
  }
};

const templatesDir = path.resolve("./templates");
await fs.mkdir(templatesDir, { recursive: true });
const filePath = path.join(templatesDir, `${name}.json`);
await fs.writeFile(filePath, JSON.stringify(template, null, 2));

console.log(JSON.stringify({ ok: true, file: filePath, name }, null, 2));


path: docs/template-schema.md
content: # Template Schema Details

This document expands on the template fields and how auto-registration works.

## Required fields
- `name`: action name, used as `/actions/:name`
- `host`: RapidAPI host
- `path`: endpoint path starting with `/`
- `method`: HTTP method

## Optional fields
- `label`, `description`: human-friendly metadata
- `querySchema`, `bodySchema`, `pathParams`, `headerSchema`
  - An object whose keys are input fields
  - Each field supports:
    - `type`: `string | number | boolean | object | array`
    - `required`: `true | false`
    - `default`: default value if not provided
    - `description`: free text
- `headers`: static headers applied to every call
- `paramMapping`: map input fields to request locations
- `response`: response shaping rules
- `timeoutMs`: per-action timeout override
- `enabled`: set to `false` to skip registration
- `tags`, `version`: metadata

## paramMapping syntax
`paramMapping` lets you map a single input field into a specific request bucket.

Example:
```json
"paramMapping": {
  "profileUrl": "query.url",
  "companyId": "path.id",
  "lang": "header.Accept-Language"
}
```

Valid buckets:
- `query.<name>`
- `body.<name>`
- `path.<name>`
- `header.<name>`

## Path params
`path` supports `{param}` tokens that will be replaced using `pathParams` or `paramMapping`.

Example:
```json
"path": "/company/{id}",
"pathParams": {"id": {"type": "string", "required": true}}
```

## Response shaping
- `response.type`: `json | text | blob`
- `response.dataPath`: dot path to extract a sub-field
- `response.errorPath`: dot path for error payload detection
- `response.normalize`: a map of output keys to dot paths

Example:
```json
"response": {
  "type": "json",
  "normalize": {
    "name": "data.profile.name",
    "title": "data.profile.headline"
  }
}
```

If `normalize` is provided, it overrides `dataPath`.

## Auto-registration rules
- Every `templates/*.json` file is loaded at startup
- Templates with `enabled: false` are skipped
- Each template becomes one action at `/actions/:name`
- `GET /actions` lists the registered actions and schemas


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)