@qvac/skills 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bundled.js CHANGED
@@ -16,7 +16,7 @@ export const SKILLS = {
16
16
  "apple-reminders/cli.schema.json": "{\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"x-positionals\": [\n \"date\"\n ],\n \"properties\": {\n \"date\": {\n \"type\": \"string\"\n },\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n },\n \"today\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n }\n },\n \"x-effect\": \"read\"\n },\n \"tomorrow\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n }\n },\n \"x-effect\": \"read\"\n },\n \"week\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n }\n },\n \"x-effect\": \"read\"\n },\n \"overdue\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n }\n },\n \"x-effect\": \"read\"\n },\n \"all\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n }\n },\n \"x-effect\": \"read\"\n },\n \"list\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"create\": {\n \"type\": \"boolean\"\n },\n \"delete\": {\n \"type\": \"boolean\"\n },\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n }\n },\n \"x-positionals\": [\n \"name\"\n ]\n },\n \"add\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"list\": {\n \"type\": \"string\"\n },\n \"due\": {\n \"type\": \"string\"\n },\n \"json\": {\n \"type\": \"boolean\"\n },\n \"plain\": {\n \"type\": \"boolean\"\n },\n \"quiet\": {\n \"type\": \"boolean\"\n }\n },\n \"x-positionals\": [\n \"title\"\n ]\n },\n \"complete\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"ids\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"x-rest\": \"ids\"\n },\n \"delete\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"force\": {\n \"type\": \"boolean\"\n },\n \"ids\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"x-positionals\": [\n \"id\"\n ],\n \"x-rest\": \"ids\"\n },\n \"status\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {},\n \"x-effect\": \"read\"\n },\n \"authorize\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {}\n }\n }\n}\n",
17
17
  "apple-reminders/references/edit.md": "# Creating, Completing, and Deleting Reminders\n\nOne `remindctl` command per `exec` call, no chaining. Run `add` in the\nforeground (the default): the exit code confirms whether the reminder was\ncreated — never claim success unless the command succeeded.\n\n| Request | Command |\n| -------------------------------- | ------------------------------------------------ |\n| \"remind me to X\" | `remindctl add --title \"X\"` |\n| \"remind me to X tomorrow\" | `remindctl add --title \"X\" --due tomorrow` |\n| \"add X to my <List> list\" | `remindctl add --title \"X\" --list Personal` |\n| \"mark X done\" | `remindctl all --json` for its id, then `remindctl complete <id>` |\n| \"delete reminder X\" | `remindctl all --json` for its id, then `remindctl delete <id> --force` |\n| \"create a list called X\" | `remindctl list X --create` |\n| \"delete the list X\" | `remindctl list X --delete` |\n\n## Create reminders\n\n```bash\nremindctl add \"Buy milk\"\nremindctl add --title \"Call mom\" --list Personal --due tomorrow\nremindctl add --title \"Meeting prep\" --due \"2026-02-15 09:00\"\n```\n\n`--due` accepts `today`, `tomorrow`, `YYYY-MM-DD`, `YYYY-MM-DD HH:mm`, and ISO\n8601 (`2026-01-04T12:34:56Z`). Without `--list` the reminder lands in the\ndefault list.\n\n## Finding the id\n\n`complete` and `delete` take reminder ids, and there is NO search command —\n`remindctl search`, `remindctl show`, `remindctl find` all fail. Get the id\nfrom a JSON view: `remindctl all --json` lists every reminder in every list,\nincluding completed ones, one object per reminder with `id`, `title`,\n`listName`, `dueDate`, `isCompleted`. Pick the entry whose `title` matches\nwhat the user named and copy its `id` (a short prefix like `4A83` is enough).\nNarrow with `remindctl list <Name> --json` or `remindctl today --json` when the\nlist or day is known.\n\n```bash\nremindctl all --json\nremindctl list Personal --json\n```\n\n## Complete / delete\n\n```bash\nremindctl complete 4A83\nremindctl complete 1 2 3\nremindctl delete 4A83 --force\n```\n\nOnly delete a reminder when the user explicitly asks; prefer `complete` for\nfinished to-dos. `--force` skips the confirmation prompt, which cannot be\nanswered from `exec`.\n\n## Lists\n\n```bash\nremindctl list Projects --create\nremindctl list Work --delete\n```\n\n## Answering\n\nOnly the commands shown in this file exist; if one fails, fix its arguments\nrather than inventing another subcommand. Confirm with the title, list, and due date of what changed. If the command\nfails because `remindctl` is missing or access is denied, report that and ask\nthe user to install or authorize; never switch to another tool.\n",
18
18
  "apple-reminders/references/view.md": "# Viewing Reminders and Lists\n\nOne `remindctl` command per `exec` call, no chaining. Pick the command from the\nrequest and answer from its output.\n\n| Request | Command |\n| ---------------------------------------- | --------------------------- |\n| \"what are my reminders\" / \"due today\" | `remindctl today` |\n| \"what's due tomorrow\" / \"this week\" | `remindctl tomorrow` · `remindctl week` |\n| \"what's overdue\" | `remindctl overdue` |\n| \"everything\" / \"all my reminders\" | `remindctl all` |\n| \"what's due on <date>\" | `remindctl 2026-01-04` |\n| \"show my reminder lists\" | `remindctl list` |\n| \"show my <List> reminders\" | `remindctl list Work` |\n\n## View by date\n\n```bash\nremindctl today\nremindctl tomorrow\nremindctl week\nremindctl overdue\nremindctl all\nremindctl 2026-01-04\n```\n\nDate filters accept `today`, `tomorrow`, `yesterday`, `YYYY-MM-DD`,\n`YYYY-MM-DD HH:mm`, and ISO 8601 (`2026-01-04T12:34:56Z`).\n\n## Lists\n\n```bash\nremindctl list\nremindctl list Work\n```\n\n`remindctl list` alone names the lists; `remindctl list <Name>` shows the\nreminders in that list, including ones without a due date.\n\n## Output formats\n\n```bash\nremindctl today --json\nremindctl today --plain\nremindctl today --quiet\n```\n\nUse `--json` when you need structured fields (ids, due dates) before a\nfollow-up command; the `id` field is what `complete` and `delete` take.\n`remindctl all --json` covers every list including completed reminders. There\nis no search command — filter the JSON view by `title` yourself.\n\n## Answering\n\nReport the title, its list, and its due date for each reminder — not the whole\ndatabase. An empty result means nothing is due: say so plainly. If the command\nfails because `remindctl` is missing or access is denied, report that and ask\nthe user to install or authorize; never switch to another tool.\n",
19
- "asana/SKILL.md": "---\nname: asana\ndescription: Asana tasks, projects, and comments via the official Asana MCP server (OAuth). Also handles pasted app.asana.com task links.\ntools: [mcp_call]\nplatform: [darwin, linux, win32]\ncredentials: [asana_mcp_access_token]\nallow_list: [https://mcp.asana.com/v2/mcp]\nmcp_reads: [get_task, get_tasks, get_my_tasks, get_projects, search_tasks, search_objects]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Asana\",\n \"provider\": \"asana\",\n \"credentialKey\": \"asana_mcp_access_token\",\n \"description\": \"Tasks, projects, and comments\",\n \"helpUrl\": \"https://app.asana.com/0/my-apps\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to the Asana workspace you allow.\"\n ],\n \"configSteps\": [\n \"[Open the Asana developer console](https://app.asana.com/0/my-apps) and create an MCP app\",\n \"In OAuth settings, add the exact redirect URI shown below\",\n \"In \\\"Manage distribution\\\", allow the workspace you want this connection to access\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"asana_oauth\",\n \"authUrl\": \"https://app.asana.com/-/oauth_authorize\",\n \"tokenUrl\": \"https://app.asana.com/-/oauth_token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18981,\n \"scopes\": [],\n \"extraAuthParams\": {\n \"resource\": \"https://mcp.asana.com/v2\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Asana\n\nOne wired transport: the official Asana MCP server (v2), authorized with a pre-registered Asana OAuth app.\n\nUse the `mcp_call` tool against `https://mcp.asana.com/v2/mcp`. Authentication uses a pre-registered Asana MCP app (client ID and secret) with PKCE — the user connects \"Asana\" from the Asana skill's setup, which stores the access token under the credential key `asana_mcp_access_token`. If the credential is connected, act immediately and call the tool(s) — do NOT ask the user for a token. If the credential is missing (a tool result reports it), tell the user to connect \"Asana\"; do not attempt to drive OAuth yourself.\n\nSessions are automatic: `mcp_call` runs the initialize handshake itself and threads the session for you. Do NOT call `initialize` or `notifications/initialized`, and do NOT pass `sessionId`.\n\nIf a call returns `Status: 401`, the token is expired/invalid — tell the user to reconnect \"Asana\" from the Asana skill's setup.\n\n## Workflow\n\nPass the **tool name as `method`** and the **tool's args as `params`** directly. The MCP `url` must be a top-level field beside `method` and `params` — never put `url` inside `params`. Do NOT build a `{ \"name\": …, \"arguments\": … }` envelope, and do NOT pass `tokenCredentialKey` — the runtime wraps the envelope and injects the bearer token by host for you.\n\nV2's tool set evolves. Call `tools/list` once before the first Asana operation in a conversation, then use the exact returned name and schema. Common tools include:\n\n| tool | use for |\n| --- | --- |\n| `get_my_tasks` | list tasks assigned to the connected user |\n| `get_tasks` / `search_tasks` | list or search tasks |\n| `get_task` | fetch one task |\n| `create_tasks` / `update_tasks` | create or update tasks |\n| `search_objects` | find projects, users, teams, tags, or portfolios |\n| `get_projects` | list projects in the authorized workspace |\n\nIf a tool is absent, do not invent or fall back to a V1 `asana_*` name. Explain the V2 limitation. If a call returns an input validation error, use the `tools/list` schema and retry the same tool once.\n\n### Example: list my tasks\n\n```json\n{\n \"url\": \"https://mcp.asana.com/v2/mcp\",\n \"method\": \"get_my_tasks\",\n \"params\": {}\n}\n```\n\n## Tool choice and pasted links\n\n- Use `mcp_call` only. NEVER use `http_request` for Asana, and NEVER fetch `app.asana.com` URLs — they serve the browser login page, not data.\n- When the user pastes an Asana link, extract the task gid and pass it to the matching MCP tool: the number after `/task/` (`…/project/<p>/task/1215448540812360` → `1215448540812360`), or the last path segment in the older `app.asana.com/0/<project>/<task>` form.\n\n## Output Policy\n\n- Always quote the task `gid` when reporting tasks back so follow-up actions stay deterministic.\n- Surface workspace and project names, not just GIDs, when human-friendly.\n- For list responses, show name, assignee, due date, and completion state. Fetch full detail only on request.\n- Confirm with the user before deleting anything — task deletion is permanent and takes subtasks with it.\n- Do not ask for or echo credentials in chat. If a tool result reports the credential is missing, tell the user to connect \"Asana\" from the Asana skill's setup.\n",
19
+ "asana/SKILL.md": "---\nname: asana\ndescription: Asana tasks, projects, and comments via the official Asana MCP server (OAuth). Also handles pasted app.asana.com task links.\ntools: [mcp_call]\nplatform: [darwin, linux, win32]\ncredentials: [asana_mcp_access_token]\nallow_list: [https://mcp.asana.com/v2/mcp]\nmcp_reads: [get_task, get_tasks, get_my_tasks, get_projects, search_tasks, search_objects]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Asana\",\n \"provider\": \"asana\",\n \"credentialKey\": \"asana_mcp_access_token\",\n \"description\": \"Tasks, projects, and comments\",\n \"helpUrl\": \"https://app.asana.com/0/my-apps\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to the Asana workspace you allow.\"\n ],\n \"configSteps\": [\n \"[Open the Asana developer console](https://app.asana.com/0/my-apps) and create an MCP app\",\n \"In OAuth settings, add the exact redirect URI shown below\",\n \"In \\\"Manage distribution\\\", allow the workspace you want this connection to access\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"asana_oauth\",\n \"authUrl\": \"https://app.asana.com/-/oauth_authorize\",\n \"tokenUrl\": \"https://app.asana.com/-/oauth_token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18981,\n \"scopes\": [],\n \"extraAuthParams\": {\n \"resource\": \"https://mcp.asana.com/v2\"\n },\n \"verify\": {\n \"url\": \"https://mcp.asana.com/v2/mcp\",\n \"method\": \"POST\",\n \"body\": {\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": \"2025-03-26\",\n \"capabilities\": {},\n \"clientInfo\": { \"name\": \"qvac\", \"version\": \"1.0.0\" }\n }\n },\n \"reject\": [{ \"status\": [401, 403], \"error\": \"asana_not_mcp_app\" }]\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Asana\n\nOne wired transport: the official Asana MCP server (v2), authorized with a pre-registered Asana OAuth app.\n\nUse the `mcp_call` tool against `https://mcp.asana.com/v2/mcp`. Authentication uses a pre-registered Asana MCP app (client ID and secret) with PKCE — the user connects \"Asana\" from the Asana skill's setup, which stores the access token under the credential key `asana_mcp_access_token`. If the credential is connected, act immediately and call the tool(s) — do NOT ask the user for a token. If the credential is missing (a tool result reports it), tell the user to connect \"Asana\"; do not attempt to drive OAuth yourself.\n\nSessions are automatic: `mcp_call` runs the initialize handshake itself and threads the session for you. Do NOT call `initialize` or `notifications/initialized`, and do NOT pass `sessionId`.\n\nIf a call returns `Status: 401`, the token is expired/invalid — tell the user to reconnect \"Asana\" from the Asana skill's setup.\n\n## Workflow\n\nPass the **tool name as `method`** and the **tool's args as `params`** directly. The MCP `url` must be a top-level field beside `method` and `params` — never put `url` inside `params`. Do NOT build a `{ \"name\": …, \"arguments\": … }` envelope, and do NOT pass `tokenCredentialKey` — the runtime wraps the envelope and injects the bearer token by host for you.\n\nV2's tool set evolves. Call `tools/list` once before the first Asana operation in a conversation, then use the exact returned name and schema. Common tools include:\n\n| tool | use for |\n| --- | --- |\n| `get_my_tasks` | list tasks assigned to the connected user |\n| `get_tasks` / `search_tasks` | list or search tasks |\n| `get_task` | fetch one task |\n| `create_tasks` / `update_tasks` | create or update tasks |\n| `search_objects` | find projects, users, teams, tags, or portfolios |\n| `get_projects` | list projects in the authorized workspace |\n\nIf a tool is absent, do not invent or fall back to a V1 `asana_*` name. Explain the V2 limitation. If a call returns an input validation error, use the `tools/list` schema and retry the same tool once.\n\n### Example: list my tasks\n\n```json\n{\n \"url\": \"https://mcp.asana.com/v2/mcp\",\n \"method\": \"get_my_tasks\",\n \"params\": {}\n}\n```\n\n## Tool choice and pasted links\n\n- Use `mcp_call` only. NEVER use `http_request` for Asana, and NEVER fetch `app.asana.com` URLs — they serve the browser login page, not data.\n- When the user pastes an Asana link, extract the task gid and pass it to the matching MCP tool: the number after `/task/` (`…/project/<p>/task/1215448540812360` → `1215448540812360`), or the last path segment in the older `app.asana.com/0/<project>/<task>` form.\n\n## Output Policy\n\n- Always quote the task `gid` when reporting tasks back so follow-up actions stay deterministic.\n- Surface workspace and project names, not just GIDs, when human-friendly.\n- For list responses, show name, assignee, due date, and completion state. Fetch full detail only on request.\n- Confirm with the user before deleting anything — task deletion is permanent and takes subtasks with it.\n- Do not ask for or echo credentials in chat. If a tool result reports the credential is missing, tell the user to connect \"Asana\" from the Asana skill's setup.\n",
20
20
  "diagrams/SKILL.md": "---\nname: diagrams\ndescription: Draw diagrams in the chat - flowcharts, sequence, state and ER diagrams, Gantt charts, pie charts, mindmaps and timelines - written as Mermaid code blocks the app renders. Use when the user asks to draw, diagram, sketch, chart, plan, visualize, or map a process, flow, schedule, architecture, or relationship.\naliases: [diagram, mermaid, flowchart, mindmap]\nplatform: [darwin, linux, win32, ios, android]\n---\n\n# Diagrams\n\nDraw a diagram by writing one ```mermaid code block. The app renders it as a\npicture automatically - never describe the rendering, never apologize about\nbeing text-only, never paste ASCII art. A diagram, chart, or plan is ONLY a\nMermaid block in your reply: never run `exec`, Python, matplotlib, or fpdf2,\nnever call an image tool, and never deliver it as a PDF or image file.\n\nYour reply STARTS with the fence. No preamble, no plan, no \"I'll create a\ndiagram showing...\" - never announce or describe a diagram instead of drawing\nit. Decide the type silently, load its recipe, write the code block, close its\nfence, then add one short sentence saying what it shows - after the closing\nfence, never inside it. A complete reply looks like this:\n\n```mermaid\nflowchart TD\n A[\"User sends a message\"] --> B{\"Needs a tool?\"}\n B -->|yes| C[\"Run the tool\"]\n B -->|no| D[\"Answer directly\"]\n```\n\nFlow of a message through the assistant.\n\n## Pick the Type, Then Load Its Recipe\n\nEach type's syntax lives in its own reference file, named by the Mermaid\nkeyword. Pick the type from the ask, then call the `skill` tool with\n`name: \"diagrams\"` and `file: \"references/<type>.md\"` in the SAME turn you\ndraw, BEFORE writing the fence - even when you drew another diagram earlier in\nthis chat. Copy the recipe's syntax exactly; a diagram written from memory is\nthe usual cause of a parse error. Each load is a real `skill` tool call -\nprinting the call as JSON or text in your reply loads nothing. After the load,\nreply with the fenced block directly: no further tool calls of any kind.\n\n| The ask | Type | File |\n| ---------------------------------------------------- | ------------------- | --------------------------- |\n| steps, decisions, a process, \"map / structure this\" | `flowchart TD` | `references/flowchart.md` |\n| a pipeline left to right | `flowchart LR` | `references/flowchart.md` |\n| a family tree, org chart, reporting lines | `flowchart TD` | `references/flowchart.md` |\n| who calls whom over time, requests and replies | `sequenceDiagram` | `references/sequence.md` |\n| modes and transitions | `stateDiagram-v2` | `references/state.md` |\n| tables and their relations | `erDiagram` | `references/er.md` |\n| code types, classes, inheritance | `classDiagram` | `references/class.md` |\n| a schedule or plan with durations | `gantt` | `references/gantt.md` |\n| dated events in order | `timeline` | `references/timeline.md` |\n| shares of a whole, percentages | `pie` | `references/pie.md` |\n| a brainstorm, idea tree, \"mindmap\" | `mindmap` | `references/mindmap.md` |\n\nUse `gitGraph` ONLY when the user names it. Never use experimental or beta\ndiagram types, and never invent a keyword a recipe does not show.\n\n## Hard Rules (every type)\n\n- One diagram per code block, and the fence language is exactly `mermaid`. The\n first line inside the fence is the type keyword from the table. Default to\n ONE block; use two only when the recipe's budget forces an overview plus one\n detail. Never more than two.\n- Except in mindmaps, node ids are letters, digits, and underscores, starting\n with a letter, never reused. Mindmap nodes have no ids.\n- Except in mindmaps, every label with a space, punctuation, or brackets goes\n in double quotes: `A[\"Send request (HTTP)\"]`. Never leave bare `()[]{}` `:`\n `;` inside a label. Mindmap labels are unquoted text.\n- Never write lowercase `end` as a node or label - write `\"End\"`. Line breaks\n inside a label are `<br/>`, never `\\n`.\n- No `%%{init}%%` directives, no `%%` comments, no `classDef` or `style`\n lines. The app themes the diagram itself.\n- Keep every label at 40 characters or fewer, one node per concept, with its\n attributes inside that node's label. Dates, roles, counts, and statuses are\n never standalone nodes.\n- Syntax in one line per type, so a skimmed recipe still lands: flowchart\n `A[\"x\"] --> B{\"y?\"}` with `-->|yes|` labels; sequence `A->>B: msg` and\n `B-->>A: reply`; state `[*] --> Idle` and `Idle --> Run : start`; pie\n `\"Sleep\" : 8` (quoted label, plain number); gantt `Name :id, 2026-09-01, 5d`\n or `after id`; timeline `2024 : Event`; mindmap `root[Topic]` then every\n other line indented deeper than the root, unquoted, no ids (a line at the\n root's indentation is a second root and fails).\n\n## When NOT to use\n\n- Images, scenes, logos, or anything artistic - use the `image-generation`\n skill instead.\n- Plots of numeric data - use `pie` for shares, otherwise a markdown table.\n- Family trees, org charts, and reporting lines - `flowchart TD`, not\n `mindmap`; a mindmap is for ideas around a topic, not people in a hierarchy.\n\n## If the diagram fails\n\nWhen a diagram cannot render, the app sends its parse error back to you once\non its own, and the user may send it again. Re-load the type's recipe and fix\nby rewriting the ENTIRE code block, never a partial patch.\n\n| Error contains | Fix |\n| --- | --- |\n| `Lexical error` / `Unrecognized text` | Remove every backslash in front of a quote and rewrite the block |\n| `Expecting 'taskData'` | A gantt line is neither a header keyword nor a complete `Name :id, start, Nd` task - fix it or delete it |\n| `Expecting ...` at a flowchart or sequence label | Put the whole label in double quotes |\n| `got 'end'` | Rename the node label to `\"End\"` |\n| `Maximum text size` or edge limit | Shrink the diagram or split it in two |\n| `No diagram type detected` | Start the fence with one type keyword from the table |\n| `Duplicate id` | Give every node a fresh unique id |\n",
21
21
  "diagrams/references/class.md": "# Class Diagram\n\nFor code types: classes, their members, inheritance.\n\nBudget: 7 classes.\n\n```mermaid\nclassDiagram\n class Animal {\n +String name\n +speak()\n }\n Animal <|-- Dog\n```\n\nRules:\n\n- Members go inside `class Name { }`, one per line, `+` public and `-`\n private, methods end with `()`.\n- Inheritance is `Parent <|-- Child`; composition `Whole *-- Part`;\n association `A --> B`.\n- Class names are single tokens; no quotes, no `classDef` lines.\n\n## Now draw\n\nThis recipe is all you need. Your next output is the reply itself: one fenced\nMermaid code block (fence language `mermaid`), then one sentence. Do not call\nany tool - not `exec`, not `skill` again, not an image tool. A tool call here\nmeans the diagram was never drawn.\n",
22
22
  "diagrams/references/er.md": "# ER Diagram\n\nFor tables and their relations.\n\nBudget: 8 entities.\n\n```mermaid\nerDiagram\n USER ||--o{ ORDER : \"places\"\n ORDER ||--|{ LINE_ITEM : \"contains\"\n```\n\nRules:\n\n- Entities are UPPER_CASE single tokens.\n- `||--o{` reads \"one to zero-or-many\"; `||--|{` \"one to one-or-many\";\n `||--||` \"one to one\".\n- The relationship label follows the colon in plain double quotes typed directly (no backslash in front).\n- Attributes are optional; if used, list them inside `ENTITY { string name }`\n blocks with one `type name` per line.\n\n## Now draw\n\nThis recipe is all you need. Your next output is the reply itself: one fenced\nMermaid code block (fence language `mermaid`), then one sentence. Do not call\nany tool - not `exec`, not `skill` again, not an image tool. A tool call here\nmeans the diagram was never drawn.\n",
@@ -32,18 +32,18 @@ export const SKILLS = {
32
32
  "excel/references/edit.md": "# Editing an Attached Workbook (openpyxl)\n\nEdit a workbook that is already in this chat by running Python through the\n`exec` tool: change cells, add or insert rows, add columns or sheets, delete\nrows, columns, or sheets. Stage the workbook as an input, modify it, and save\nunder a **new** output name such as `revised.xlsx` — never overwrite the staged\ninput.\n\nMacro-enabled files (`.xlsm`) can be staged as inputs and read, but this\nruntime cannot deliver `.xlsm` back — macros never survive. Save the edit as\n`.xlsx` and tell the user the macros were not preserved.\n\n## Staging the Workbook\n\n**A workbook you built earlier in this chat is edited exactly like any other\nattachment — through its id.** The `exec` result that produced it carried\n`attachments: [{ attachmentId, fileName, byteLength }]`; scroll back, copy that\n`attachmentId` character for character, and stage it. The working directory is\nwiped between calls, so a file you saved last turn is not on disk — without a\nstaged input `load_workbook(\"report.xlsx\")` raises\n`FileNotFoundError: [Errno 44] No such file or directory`.\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"openpyxl==3.1.5\"],\n \"inputs\": [{ \"attachmentId\": \"<the id from the earlier exec result>\", \"path\": \"existing.xlsx\" }],\n \"outputs\": [\"revised.xlsx\"],\n \"command\": \"...\"\n}\n```\n\n**A workbook the user uploaded is staged the same way — by its id.** The\n`[Attached file …]` line on their message names it:\n\n```\n[Attached file \"budget.xlsx\" (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) — attachmentId: 4f9c2ab1]\n```\n\nCopy that id verbatim into `attachmentId`, exactly as for a workbook a tool\nproduced. This holds for **`.csv` and `.xlsm` uploads too**, not just `.xlsx`: an\nid-less entry resolves to an uploaded *image*, so any `inputs` entry whose `path`\nnames a data or document file is rejected outright. Only an uploaded **image** is\nstaged with `path` alone and no `attachmentId` key.\n\nStaged files land in the working directory under the bare `path` names —\nreference `load_workbook(\"existing.xlsx\")` by that name only.\n`attachment … not found in this chat` means you invented an id or the file is not\nattached. Re-copy the exact id from the `exec` result or the `[Attached file …]`\nline that names the workbook; if no id appears anywhere in the chat, ask the\nuser to attach the file again.\n\n`wb.save(\"revised.xlsx\")` must match the declared output name. Keep the\n`command` source multi-line with real newlines — never collapse it with `;`.\n\n## Adding an Image to an Existing Workbook\n\nStage two files: the workbook by its `attachmentId`, and the picture. Add `\"pillow\"`\nto `packages` — **deliberately unpinned**, since the runtime owns its version and a\npin sends openpyxl to PyPI with it.\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"openpyxl==3.1.5\", \"pillow\"],\n \"inputs\": [\n { \"attachmentId\": \"<id of the workbook>\", \"path\": \"existing.xlsx\" },\n { \"attachmentId\": \"<id from the generate_image result>\", \"path\": \"photo.png\" }\n ],\n \"outputs\": [\"revised.xlsx\"],\n \"command\": \"...\"\n}\n```\n\n`path` is a name you choose; it has nothing to do with the attachment id, and a\n`fileName` seen in a tool result is not a file on disk. A `generate_image` result\nanywhere in the conversation is the image the request points at — stage that id\nrather than asking the user to attach it again.\n\n```python\nfrom openpyxl.drawing.image import Image as XLImage\n\nimg = XLImage(\"photo.png\") # the path from inputs, nothing else\nimg.width, img.height = 320, 320 # pixels\nws.add_image(img, \"A1\") # a worksheet method; A1 is the top-left corner\n```\n\nOne `add_image` per workbook — inside a loop over sheets it embeds a copy per sheet.\nNever wrap the import or the call in a `try`/`except` that saves anyway.\n\n## The Golden Rule of Edits\n\n**The staged sheet already has its header and all its data.** Editing never\nre-creates them: no `HEADERS`, no `ROWS`, no copy of the create template. Append\nonly what is genuinely new, and change only the cells you were asked to change.\nRe-appending the header and the rows writes the whole table a second time and the\nuser opens a file where every row appears twice — the create template belongs to\nthe create path (`references/create.md`) and nowhere else.\n\nAdding one row is the whole program:\n\n```python\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"existing.xlsx\")\nws = wb.active # wb[\"Sheet Name\"] to pick another\n\nbefore = ws.max_row # the sheet is already this long\nws.append([2026, 82500, 135000, 107500])\n\nwb.save(\"revised.xlsx\") # NEW name, matching the declared output\nprint(f\"{before} rows in, {ws.max_row} rows out\")\n```\n\nThat print is the check: one added row means the count goes up by exactly one. If\nit roughly doubles, the run re-appended the existing data — fix it and rerun\nrather than delivering a workbook with the table in it twice.\n\n## Putting the Row Where It Belongs\n\nAppending is right when the table has no order, and wrong when it has one: a sheet\nrunning 2016…2026 with 2015 stuck on the end reads as broken. When the new row\nbelongs inside an existing order, insert it at that position instead.\n\nYou already know the position — 2015 sorts above 2016, and the data starts at row 2\n— so write the index as a number. Do not scan, sort, or compare anything to work it\nout:\n\n```python\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"existing.xlsx\")\nws = wb.active\n\nROW = [2015, 278, 930, 45.5]\nAT = 2 # 2015 goes above 2016 — row 1 is the header\n\nassert not any(m.max_row >= AT for m in ws.merged_cells.ranges) and not any(\n isinstance(c.value, str) and c.value.startswith(\"=\") for r in ws.iter_rows() for c in r\n), \"a merged range at or below AT, or a formula — append instead, their ranges do not move\"\n\nbefore = ws.max_row\nws.insert_rows(AT)\nfor column, value in enumerate(ROW, start=1):\n cell = ws.cell(row=AT, column=column, value=value)\n cell.number_format = ws.cell(row=AT + 1, column=column).number_format\n\nwb.save(\"revised.xlsx\")\nprint(f\"{before} rows in, {ws.max_row} rows out\")\n```\n\n`AT` is never `1` — that would push the header down into the data.\n\nThe two lines that look optional are the ones that matter. An inserted cell starts\nwith no number format, so without the copy the new row shows a bare `278` in a\ncolumn of `278.00`s; taking the format from `AT + 1` uses the row that used to sit\nthere. And `insert_rows` moves cells but neither formula ranges nor merged ranges,\nso a `=SUM(B2:B11)` total would go on summing the old span and quietly leave the\nnew row out, while a `Total` merged across `A8:B8` would stay pinned to row 8 as\nits row slid to 9 — the assert stops both before anything is saved.\n\nThe two halves are scoped differently on purpose. A merged range is disturbed only\nif it sits at or below `AT`, which is why the check is `m.max_row >= AT` rather than\n\"any merged cell\": a title merged across `A1:C1` is untouched by an insert further\ndown, and failing on it would push you to append out of order for no reason. A\nformula gives no such signal — one in `D1` can reference `B2:B11` — so any formula\nat all is enough to stop the insert.\n\nWhen the assert fires, do not delete it. Append the row at the end with the\nprevious template and tell the user the table kept its file order so their totals\nstay correct.\n\n## Other Edits — Touch Only What Changes\n\n```python\nfrom openpyxl import load_workbook\nfrom openpyxl.styles import Font\n\nwb = load_workbook(\"existing.xlsx\")\nws = wb[\"Q1 Sales\"] # or wb.active; wb.sheetnames lists them\n\nws.cell(row=1, column=5, value=\"Margin %\").font = Font(bold=True)\nfor row, margin in [(2, 0.31), (3, 0.42), (4, 0.18)]:\n cell = ws.cell(row=row, column=5, value=margin)\n cell.number_format = '0.0%'\n\nws[\"B2\"] = 150 # update a cell in place\n\nnotes = wb.create_sheet(\"Notes\")\nnotes[\"A1\"] = \"Updated unit counts for North\"\n\nwb.save(\"revised.xlsx\") # NEW name, matching the declared output\nprint(f\"{len(wb.sheetnames)} sheets: {wb.sheetnames}\")\n```\n\n`wb[\"Sheet Name\"]` raises `KeyError` when the name does not exist — when unsure,\nprint `wb.sheetnames` in the same run that edits, pick from it, and never guess.\n\n## Deleting Rows, Columns and Sheets\n\nopenpyxl deletes for real, so none of this needs XML work. `ws.delete_rows(index)`\nand `ws.delete_cols(index)` take a **1-based** index and an optional count, so\n`ws.delete_rows(5, 3)` drops rows 5, 6 and 7 together; a sheet goes with\n`del wb[\"Notes\"]`. Row 1 is the header — `delete_rows(1)` throws it away, and data\nrows start at 2, exactly as for an insert.\n\n**Delete from the bottom up.** Each delete shifts everything below it, so a loop\nover ascending indices removes the wrong rows after the first: dropping rows 3 and\n5 top-down deletes row 3, then deletes what used to be row 6. Collect the row\nnumbers first and walk them in reverse — the same rule applies right-to-left for\n`delete_cols`:\n\n```python\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"existing.xlsx\")\nws = wb.active\n\nDROP = (2021, 2023) # the column-A values whose rows go\n\nbefore = ws.max_row\ntargets = [r for r in range(2, ws.max_row + 1) if ws.cell(row=r, column=1).value in DROP]\nassert targets, f\"no row matched {DROP} — check the values are numbers, not strings\"\n\nassert not any(m.max_row >= min(targets) for m in ws.merged_cells.ranges) and not any(\n isinstance(c.value, str) and c.value.startswith(\"=\") for r in ws.iter_rows() for c in r\n), \"a merged range at or below the first deleted row, or a formula — their ranges do not move\"\n\nfor row in reversed(targets): # bottom-up; ascending order deletes the wrong rows\n ws.delete_rows(row)\n\nwb.save(\"revised.xlsx\") # NEW name, matching the declared output\nprint(f\"{before} rows in, {ws.max_row} rows out\")\n```\n\nThe `assert targets` line is what stops a no-op being delivered, and it belongs\nbefore the loop rather than after it. Without it, values that match nothing leave\nthe sheet untouched and the workbook still saves at `exitCode 0` with an\nattachment indistinguishable from a real delete. With it the run raises, nothing\nis written, and the result carries `missingOutputs` instead. The usual cause is a\ntype mismatch — the string `\"2021\"` is not the number `2021` — or a wrong column\nindex. **An assert that fires is a failed turn to diagnose, not a workbook to\ndeliver**: fix the match and rerun, and never delete the assert to get a file out.\nThe printed `rows in / rows out` line is then the reply line, not the check, and it\nstill costs no extra call — both live in the run that does the deleting.\n\nThe second assert is the `insert_rows` hazard in reverse, and it covers the same two\nthings with the same scoping. `delete_rows` moves cells but leaves formula text\nalone, so a `=SUM(B2:B11)` total goes on summing eleven rows of a table that now\nholds nine, pulling in blanks or the wrong cells. It leaves merged ranges alone too:\na `Total` merged at `A8:B8` keeps covering row 8 after a row above it is deleted, and\na title merged across `A1:C1` still claims three columns after a `delete_cols`. Both\nare silent — no error, and the damage only shows when the user opens the file.\n\nMerges are again checked from the first deleted row down (`m.max_row >= min(targets)`),\nso a banner above every deletion does not block the edit, while any formula anywhere\ndoes. When it fires, do not delete it: say which rows you would have removed and ask\nwhether to drop the merges and formulas too, or tell the user the deletion has to\nhappen in Excel, where the ranges follow. Note the column case is not covered by that\nrow check — if you are calling `delete_cols` on a sheet with horizontal merges, treat\nany merged range as a stop.\n\n## Reading Cell Values During an Edit\n\n`load_workbook` has two modes, and neither gives both formulas and values:\n\n- `load_workbook(\"f.xlsx\")` — formula cells hold the formula **string**\n (`\"=SUM(D2:D4)\"`).\n- `load_workbook(\"f.xlsx\", data_only=True)` — formula cells hold the value the\n last spreadsheet app **cached** when it saved. A file that openpyxl itself wrote\n has no cache, so these cells read `None`.\n\nPlain data cells read the same either way. When a formula cell reads `None` under\n`data_only=True`, the file was never recalculated by a spreadsheet app — compute\nthe number in Python from the data cells instead of hunting for it.\n\n**Never `save()` a workbook opened with `data_only=True`.** That mode loads values\nin place of formulas, so saving writes the values back and every formula the user\nhad is gone — silently, at `exitCode 0`, with an attachment that looks fine. A\nworkbook you intend to save is always opened plainly:\n\n```python\nfrom openpyxl import load_workbook\n\nvalues = load_workbook(\"existing.xlsx\", data_only=True) # read numbers here\nwb = load_workbook(\"existing.xlsx\") # edit and save this one\n```\n\nRead from `values`, write to `wb`, and save `wb`. One open, one job.\n\nThat is reading in service of an edit. Reading for the *user* — a summary or an\nanswer delivered as chat text — is its own flow with its own call shape: load\n`references/read.md`.\n\n## Errors\n\n- Never print the workbook's bytes or base64 — stdout is capped and the file\n travels through `outputs`. A build call prints only a short summary line.\n Never pass an absolute path to `save()`.\n- `attachment … not found in this chat` — `inputs` listed an id that is not in\n this chat (often a copied placeholder). Only stage real ids from prior tool\n results or `[Attached file …]` lines.\n- `an id-less input stages an uploaded image, and this path names a document` —\n an `.xlsx` was staged with no `attachmentId`. Spreadsheets are always staged\n by id.\n- `no uploaded image in this chat — attach an image or pass an attachmentId` —\n an id-less input was sent when the user uploaded no image at all.\n- `FileNotFoundError: [Errno 44] No such file or directory` on a workbook you\n saved in an earlier call means it was never staged: the working directory is\n fresh every call. Add the file to `inputs` with its `attachmentId`.\n- A delivered workbook whose formulas have turned into blanks means it was opened\n with `data_only=True` and then saved. Open a second, plain workbook to edit.\n- A delivered workbook whose table appears twice means the edit re-appended the\n header and rows onto the staged sheet. An edit adds only what is new.\n- `AssertionError: a merged range at or below AT, or a formula — append instead …`\n means a merged range sits at or below the insertion row, or the sheet has a formula\n whose range `insert_rows` would not move. Append the row at the end instead and say\n why in the reply.\n- `AssertionError: a merged range at or below the first deleted row, or a formula …`\n is the same hazard on a delete, and has no safe fallback: say which rows you would\n remove and ask the user how to handle the merges and formulas.\n- `AssertionError: no row matched …` means the delete found nothing, usually because\n the compared values are strings on one side and numbers on the other. Diagnose the\n match and rerun; do not remove the assert, because the workbook it would deliver\n is the staged one unchanged.\n- Rows that disappeared from the wrong places mean the delete loop ran over\n ascending indices. Collect the targets first and delete in reverse.\n- A row that renders unlike the rest of its column — `278` among `278.00`s — was\n inserted without copying `number_format` from the row below it.\n- `'MergedCell' object attribute 'value' is read-only` means the write hit a merged\n non-anchor cell — write the range's top-left cell instead.\n- `\".xlsm\" is not an allowed output type` means the run tried to deliver a\n macro-enabled file — save as `.xlsx` and tell the user macros were not preserved.\n- `KeyError` on `wb[\"Sheet Name\"]` — the sheet name does not exist; print\n `wb.sheetnames` in the run that edits and pick from it.\n- `ModuleNotFoundError: No module named 'openpyxl'` — add\n `[\"openpyxl==3.1.5\"]` to `packages` and rerun. Never try to install it.\n- `ImportError: You must install Pillow to fetch image objects` means an image was\n embedded without `\"pillow\"` in `packages` — openpyxl does not install it.\n- `FileNotFoundError` on a 32-character hex name means an attachment id was opened as\n a path. The id belongs in `attachmentId`; open the `path` you chose.\n- On an `AttributeError` from openpyxl the API name is wrong, and on a `TypeError`\n about missing positional arguments a required argument was left out — fix either\n against this file's examples. Do not retry the same call, and do not switch to a\n shell.\n\n## Finish\n\nWhen `exitCode` is `0` and `attachments` lists the `.xlsx`, stop tool use and\nanswer with one line: file name + the `rows in / rows out` (or sheets) summary\nfrom stdout. Exactly one successful `exec` per request. If the result has\n`missingOutputs`, read stderr first — an `AssertionError` there means a guard\nstopped the save on purpose and its message names what to fix.\n",
33
33
  "excel/references/read.md": "# Reading a Workbook to Answer in Chat (openpyxl)\n\nWhen the user asks what an attached workbook *holds* — a summary, a question\nanswered, specific values pulled out — the deliverable is your reply in the\nchat, not a file. This is a **read request**: exactly one `exec` call, staging\nthe workbook in `inputs` and declaring **no `outputs`**, whose whole job is to\nprint the sheets so you can read them in the result.\n\n## The exec call\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"openpyxl==3.1.5\"],\n \"inputs\": [{ \"attachmentId\": \"<id from the [Attached file …] line>\", \"path\": \"existing.xlsx\" }],\n \"maxOutputChars\": 24000,\n \"command\": \"...\"\n}\n```\n\nThe id rules: copy it verbatim from the `[Attached file …]` line on the user's\nmessage or from the earlier `exec` result that produced the file, never invent\none, never stage a workbook id-less (an id-less entry resolves to an uploaded\n*image*). If no id appears anywhere in the chat, ask the user to attach the\nfile again. `maxOutputChars` raises the stdout cap so a full workbook comes\nback in one result; keep the sample's 24000. Declare no `outputs` — a read\nbuilds nothing.\n\n## The Read Program\n\nPrints every sheet under a `[Sheet]` marker, then its rows — one printed line\nper row:\n\n```python\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"existing.xlsx\", data_only=True)\nfor name in wb.sheetnames:\n ws = wb[name]\n print(f\"[Sheet] {name} ({ws.max_row} rows)\")\n for row in ws.iter_rows(values_only=True):\n print(\" | \".join(\"\" if v is None else str(v).replace(\"\\n\", \" \") for v in row))\n```\n\nThe `replace` is load-bearing: a multi-line cell (Alt+Enter in Excel) embeds\n`\"\\n\"` in its value, and an embedded newline would split one row across two\nprinted lines. Flattened, every printed line is exactly one sheet row.\n\n`data_only=True` is the right mode here: a formula cell prints the value the\nlast spreadsheet app cached, or an empty field when the file was never\nrecalculated (`None` prints as nothing). An empty field under a `Total` header\nis that, not missing data — say so, and when the answer needs the number, work\nit out from the data rows that did print.\n\n**You cannot summarize in the call that reads.** The words in `command` are\nfixed before the program runs, so one call cannot inform itself: any summary\nwritten into it was written blind — recalled or invented, not read. Python only\n*transports* the values; the summarizing happens in your reply, after the\nresult comes back.\n\n## A Successful Read Ends Tool Use\n\nWhen the result prints the sheets, reply with the summary or the answer as chat\ntext — when the user asked for the table in the chat, that reply is a markdown\ntable built from the rows you read, never from memory. **Scale the reply to the\nworkbook**: a summary is much shorter than what it summarizes — a small sheet\nearns a few sentences, and only a many-sheet workbook earns sections. Restating\nevery row is not a summary. Do **not**:\n\n- call `exec` again to \"re-check\", \"read more\", or read the same workbook a\n second time;\n- build a summary `.xlsx` the user never asked for — an unrequested file is a\n failed turn, not a bonus.\n\nIf stdout ends with `… [truncated]`, the workbook is longer than the cap:\nanswer from what came back and say the answer covers the sheets up to that\npoint. Do not rerun the read — it prints the same beginning again.\n\nIf the user asks for the summary **as a file**, that is a read followed by a\nbuild: the read call above first, then one build call that writes the new\nworkbook from the values you actually read (load `references/create.md` for the\nbuild). The read still declares no `outputs`.\n\n## Errors\n\n- `attachment … not found in this chat` — the id was invented or the file is\n not attached. Re-copy the exact id; if none exists, ask the user to attach\n the file again instead of retrying.\n- `an id-less input stages an uploaded image, and this path names a document` —\n the workbook was staged with no `attachmentId`. Spreadsheets (`.xlsx`,\n `.csv`, `.xlsm`) are always staged by id.\n- `FileNotFoundError: [Errno 44] No such file or directory` — the file was\n never staged; the working directory is fresh every call. Add it to `inputs`\n with its `attachmentId`.\n- `ModuleNotFoundError: No module named 'openpyxl'` — add\n `[\"openpyxl==3.1.5\"]` to `packages` and rerun. Never try to install it.\n- A formula cell that prints nothing is not a bug — the file was never\n recalculated by a spreadsheet app. Compute the number from the data rows\n instead of rerunning.\n",
34
34
  "github/SKILL.md": "---\nname: github\ndescription: Search, read, and write GitHub repos, issues, and pull requests via the REST API.\ntools: [http_request]\nplatform: [darwin, linux, win32, ios, android]\ncredentials: [github_access_token]\nallow_list: [https://api.github.com/]\n---\n\n# GitHub\n\nUse `http_request` against `https://api.github.com` on every call, with `headers: {\"Accept\": \"application/vnd.github+json\"}`. The GitHub PAT credential is attached automatically to every `api.github.com` request — **never include an `auth` block**. Never fetch `github.com` web pages — they return HTML, not data; translate a pasted link to its API path instead (e.g. `github.com/{owner}/{repo}/pull/{n}` → `/repos/{owner}/{repo}/pulls/{n}`).\n\n```json\n{\n \"url\": \"https://api.github.com/search/issues?q=is:pr+is:open+repo:owner/repo&per_page=5\",\n \"method\": \"GET\",\n \"headers\": { \"Accept\": \"application/vnd.github+json\" }\n}\n```\n\n## Reads\n\n- Search repos → `GET /search/repositories?q=...`\n- Search issues/PRs → `GET /search/issues?q=...` (always qualify with `is:pr` or `is:issue` — it returns both)\n- List issues → `GET /repos/{owner}/{repo}/issues?state=open` (items with a `pull_request` key are PRs, not issues)\n- List PRs → `GET /repos/{owner}/{repo}/pulls?state=open`\n- Read a file → `GET /repos/{owner}/{repo}/contents/{path}` (`content` is base64-encoded)\n- List a user's repos → `GET /user/repos?sort=updated`\n\n## Writes\n\n- Create an issue → `POST /repos/{owner}/{repo}/issues` with `body: {\"title\": \"...\", \"body\": \"...\"}`\n- Comment on an issue or PR → `POST /repos/{owner}/{repo}/issues/{n}/comments` with `body: {\"body\": \"...\"}` (PRs are issues for commenting — use the PR number on the issues endpoint)\n- Close or edit an issue → `PATCH /repos/{owner}/{repo}/issues/{n}` with `body: {\"state\": \"closed\"}`\n\n## Notes\n\n- Paginate with `per_page` (default small — 5, rarely above 30) and `page`; never fetch more than the request needs.\n- Ask for specific fields where the endpoint supports it, and summarize in plain language rather than echoing raw JSON — responses get truncated past 8KB.\n- **401** — token missing or revoked: tell the user to connect GitHub, don't retry. **404** on a resource the user linked directly usually means it's private, not nonexistent — search can't see private repos either. **403/429** mentioning rate limits — say so and stop.\n- Never print the token or the `Authorization` header, and don't claim a write succeeded without a successful response in this turn.\n",
35
- "gmail/SKILL.md": "---\nname: gmail\ndescription: Read, search, send, and manage Gmail messages and labels via the Gmail REST API.\naliases: [inbox, email+send, email+draft, email+reply, email+forward]\ntools: [http_request, gmail_send, gmail_draft]\nplatform: [darwin, linux, win32]\ncredentials: [gmail_access_token]\nallow_list: [https://gmail.googleapis.com/gmail/v1/users/me/]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Gmail\",\n \"provider\": \"google\",\n \"credentialKey\": \"gmail_access_token\",\n \"description\": \"Read, search, send, and manage Gmail messages and labels\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your Gmail only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=gmail.googleapis.com) to enable the Gmail API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Desktop app\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"gmail_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/gmail.modify\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Gmail\n\nUse `gmail_send` to send, `gmail_draft` to draft, and `http_request` for everything else (list, search, get, labels, trash). The Gmail credential is attached automatically to every `gmail.googleapis.com` request — **never include an `auth` block**.\n\n## Prerequisites\n\nGmail must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Gmail from Settings, or to\nset the `gmail_access_token` credential.\n\n## Base URL\n\n`https://gmail.googleapis.com/gmail/v1/users/me`\n\nThe host is `gmail.googleapis.com` — not `www.googleapis.com`. Send is at `/messages/send`, never `/send`.\n\n## Common Operations\n\n### List or search messages\n\n`messages.list` returns `{id, threadId}` pairs only — no subjects, no snippets. To summarize you need a follow-up `messages.get` per id. Use `q` for Gmail search syntax (`from:`, `subject:`, `is:unread`, `newer_than:7d`, `has:attachment`, `label:work`). Keep `maxResults` ≤ 10.\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages\",\n \"method\": \"GET\",\n \"query\": { \"q\": \"is:unread newer_than:7d\", \"labelIds\": \"INBOX\", \"maxResults\": 10 }\n}\n```\n\n### Get a message (metadata)\n\nFor lists and summaries always use `format=metadata` — it skips the body and is far cheaper than `full`.\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}\",\n \"method\": \"GET\",\n \"query\": {\n \"format\": \"metadata\",\n \"metadataHeaders\": \"Subject,From,To,Date,Message-ID,References\"\n }\n}\n```\n\n### Get a message (full body)\n\nOnly when the user needs the content. The body is base64url-encoded in `payload.parts[].body.data` (or `payload.body.data`); decode it before presenting. Prefer the `text/plain` part over `text/html`.\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}\",\n \"method\": \"GET\",\n \"query\": { \"format\": \"full\" }\n}\n```\n\n### Send a message\n\nUse `gmail_send` with semantic args: `to` (array), `subject`, and `text` (or `html`). Optional: `cc`, `bcc`, `replyTo`. The tool builds the RFC 2822 message and base64url-encodes it — never construct `raw` yourself.\n\n```json\n{ \"to\": [\"<RECIPIENT_EMAIL>\"], \"subject\": \"Subject line\", \"text\": \"Message body\" }\n```\n\nIf you supply both `text` and `html`, only `text` is sent — pick one.\n\nConfirm recipient, subject, and body with the user before sending. Report success only when the response contains a message `id`.\n\n### Draft a message\n\nUse `gmail_draft` with the same envelope as `gmail_send`.\n\n```json\n{ \"to\": [\"<RECIPIENT_EMAIL>\"], \"subject\": \"Subject line\", \"text\": \"Draft body\" }\n```\n\n### Reply to a message (preserves threading)\n\nA reply is `gmail_send` with the original's `threadId`, `inReplyTo`, and `references`. Without these Gmail starts a new thread.\n\n1. Get the original with `format=metadata` and headers `Message-ID,References,Subject,From,Reply-To`; capture its `threadId`.\n2. Send with `to` <- original From (or Reply-To), `subject` <- `Re: ` + original (don't double-prefix), `inReplyTo` <- original Message-ID, `references` <- original References then that Message-ID. Keep angle brackets.\n\n```json\n{\n \"to\": [\"<RECIPIENT_EMAIL>\"],\n \"subject\": \"Re: Original subject\",\n \"text\": \"Reply body\",\n \"threadId\": \"{originalThreadId}\",\n \"inReplyTo\": \"<msg-id@mail.gmail.com>\",\n \"references\": \"<msg-id@mail.gmail.com>\"\n}\n```\n\n### Modify labels (mark read, archive, star)\n\nSystem labels: `INBOX`, `UNREAD`, `STARRED`, `IMPORTANT`, `SPAM`, `TRASH`. Mark read = remove `UNREAD`; archive = remove `INBOX`; star = add `STARRED`.\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}/modify\",\n \"method\": \"POST\",\n \"body\": { \"removeLabelIds\": [\"UNREAD\", \"INBOX\"] }\n}\n```\n\n### Trash a message\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}/trash\",\n \"method\": \"POST\"\n}\n```\n\n## Output Policy\n\n- Lists: up to 5 entries with subject, sender, and a human-readable date. Fetch full bodies only when asked.\n- Decode base64 message bodies before presenting; never include raw base64 blobs.\n- Modify/trash: state the user-facing effect (\"marked 3 messages as read\"), not the label diff.\n- Confirm destructive or outgoing actions (send, reply, trash) with the user first.\n\n## Common Mistakes\n\n- Using `www.googleapis.com` for Gmail, or building send/draft/reply through `http_request` instead of `gmail_send`/`gmail_draft`.\n- Treating `messages.list` results as if they had subjects — they need a follow-up `messages.get`.\n- Replying without `threadId` + `inReplyTo`/`references` — Gmail starts a new thread.\n- Sending a placeholder (`<RECIPIENT_EMAIL>`, `recipient@example.com`) — the runtime refuses these.\n- Inventing an address. The runtime refuses any recipient absent from the conversation and from earlier tool results; search Gmail or ask the user.\n- Including an `auth` block by hand — credentials attach automatically; a mistyped key breaks the request.\n",
36
- "gmail/operations.json": "{\n \"operations\": [\n {\n \"tool\": \"gmail_send\",\n \"description\": \"Send an email through Gmail, or reply in a thread when threadId and inReplyTo are given. Pass the message as plain fields — never build the RFC 2822 message or base64 yourself. Confirm the recipient and body with the user before sending.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"to\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" },\n \"description\": \"Recipient email addresses\"\n },\n \"subject\": { \"type\": \"string\", \"description\": \"Subject line\" },\n \"text\": { \"type\": \"string\", \"description\": \"Plain-text body\" },\n \"html\": { \"type\": \"string\", \"description\": \"HTML body, used when text is absent\" },\n \"cc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"bcc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"replyTo\": { \"type\": \"string\" },\n \"threadId\": {\n \"type\": \"string\",\n \"description\": \"Thread to reply in, from a previous messages.list or messages.get\"\n },\n \"inReplyTo\": {\n \"type\": \"string\",\n \"description\": \"Message-ID header of the message being replied to\"\n },\n \"references\": { \"type\": \"string\", \"description\": \"References header of the thread\" }\n },\n \"required\": [\"to\", \"subject\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/send\",\n \"builder\": \"gmail-send\"\n }\n },\n {\n \"tool\": \"gmail_draft\",\n \"description\": \"Create a Gmail draft without sending it. Pass the message as plain fields — never build the RFC 2822 message or base64 yourself.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"to\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" },\n \"description\": \"Recipient email addresses\"\n },\n \"subject\": { \"type\": \"string\", \"description\": \"Subject line\" },\n \"text\": { \"type\": \"string\", \"description\": \"Plain-text body\" },\n \"html\": { \"type\": \"string\", \"description\": \"HTML body, used when text is absent\" },\n \"cc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"bcc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"replyTo\": { \"type\": \"string\" },\n \"threadId\": { \"type\": \"string\", \"description\": \"Thread the draft replies in\" },\n \"inReplyTo\": {\n \"type\": \"string\",\n \"description\": \"Message-ID header of the message being replied to\"\n },\n \"references\": { \"type\": \"string\", \"description\": \"References header of the thread\" }\n },\n \"required\": [\"to\", \"subject\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/drafts\",\n \"builder\": \"gmail-draft\"\n }\n }\n ]\n}\n",
37
- "google-calendar/SKILL.md": "---\nname: google-calendar\ndescription: List, create, update, and delete Google Calendar events via the Google Calendar REST API.\naliases: [calendar, meeting+create, meeting+schedule, meeting+book, meeting+move, meeting+cancel, event+create]\ntools: [http_request, calendar_create_meet_event, calendar_add_meet]\nplatform: [darwin, linux, win32]\ncredentials: [google_calendar_access_token]\nallow_list: [https://www.googleapis.com/calendar/v3/]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Calendar\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_calendar_access_token\",\n \"description\": \"List, create, update, and delete Google Calendar events\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your calendars only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com) to enable the Google Calendar API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Desktop app\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_calendar_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/calendar\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Calendar\n\nUse `calendar_create_meet_event` and `calendar_add_meet` for Google Meet links, and `http_request` for everything else (list, plain create/update/delete, free/busy). The Google Calendar credential is attached automatically to every `www.googleapis.com/calendar/v3/` request — **never include an `auth` block**.\n\n## Prerequisites\n\nGoogle Calendar must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Calendar from Settings, or to\nset the `google_calendar_access_token` credential.\n\n## Base URL\n\n`https://www.googleapis.com/calendar/v3` — default `calendarId` is `primary` unless the user names another calendar.\n\n## Temporal Accuracy (Critical)\n\n- Resolve relative dates (\"today\", \"tomorrow\", \"next week\", \"in 2 hours\") to absolute ISO 8601 timestamps **with timezone offset** before calling the API.\n- Never infer \"now\" from memory when building `timeMin`, `timeMax`, or event start/end values.\n- If the date or time is ambiguous, ask the user instead of guessing.\n\nExample: annotation `[Current local time: 2026-08-12 14:00:00 (UTC-03:00)]`, user asks \"in 30 minutes for 1 hour\" -> `start.dateTime` = `2026-08-12T14:30:00-03:00`, `end.dateTime` = `2026-08-12T15:30:00-03:00`.\n\n## Common Operations\n\n### List upcoming events\n\nAlways use `singleEvents=true` and `orderBy=startTime` so recurring events expand and sort correctly.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events\",\n \"method\": \"GET\",\n \"query\": {\n \"timeMin\": \"<resolved ISO 8601 with offset>\",\n \"maxResults\": 10,\n \"singleEvents\": true,\n \"orderBy\": \"startTime\"\n }\n}\n```\n\n### Create an event\n\nAsk for missing required fields (summary, start, end) before creating.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events\",\n \"method\": \"POST\",\n \"body\": {\n \"summary\": \"Design review\",\n \"start\": { \"dateTime\": \"2026-06-10T14:00:00-03:00\" },\n \"end\": { \"dateTime\": \"2026-06-10T15:00:00-03:00\" },\n \"attendees\": [{ \"email\": \"colleague@example.com\" }]\n }\n}\n```\n\n### Create an event with a Google Meet link\n\nUse `calendar_create_meet_event` with `summary`, `start`, `end` (ISO 8601 with offset), and optionally `timeZone`, `description`, `location`, `attendees`, `calendarId`.\n\n```json\n{\n \"summary\": \"Sync call\",\n \"start\": \"2026-06-10T14:00:00-03:00\",\n \"end\": \"2026-06-10T14:30:00-03:00\"\n}\n```\n\nTo add a Meet link to an event that already exists, use `calendar_add_meet` with `eventId` (and `calendarId` if not `primary`).\n\n### Update an event\n\n`PATCH` with only the fields to change.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events/{eventId}\",\n \"method\": \"PATCH\",\n \"body\": { \"summary\": \"Updated title\" }\n}\n```\n\n### Delete an event\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events/{eventId}\",\n \"method\": \"DELETE\"\n}\n```\n\n### Free/busy query\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/freeBusy\",\n \"method\": \"POST\",\n \"body\": {\n \"timeMin\": \"2026-06-10T00:00:00Z\",\n \"timeMax\": \"2026-06-11T00:00:00Z\",\n \"items\": [{ \"id\": \"primary\" }]\n }\n}\n```\n\n### List calendars\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/users/me/calendarList\",\n \"method\": \"GET\"\n}\n```\n\n## Output Policy\n\n- Present events with title, date/time (with timezone), location, and attendees when available.\n- For create/update, report the event `id`, start/end time, and `htmlLink` rendered as a Markdown link — e.g. `[event title](htmlLink)`. Never present the URL bare or wrapped in backticks, or it won't be clickable.\n- Confirm before deleting events; show the event title and time so the user can verify.\n\n## Common Mistakes\n\n- Sending natural-language dates instead of absolute ISO 8601 with timezone offset.\n- Listing without `singleEvents=true` — recurring events come back collapsed and unsorted.\n- Calling get/patch/delete without a real `eventId` — list/search first.\n- Including an `auth` block by hand — credentials attach automatically; a mistyped key breaks the request.\n",
35
+ "gmail/SKILL.md": "---\nname: gmail\ndescription: Read, search, send, and manage Gmail messages and labels via the Gmail REST API.\naliases: [inbox, email+send, email+draft, email+reply, email+forward]\ntools: [http_request, gmail_get, gmail_send, gmail_draft]\nplatform: [darwin, linux, win32]\ncredentials: [gmail_access_token]\nallow_list: [https://gmail.googleapis.com/gmail/v1/users/me/]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Gmail\",\n \"provider\": \"google\",\n \"credentialKey\": \"gmail_access_token\",\n \"description\": \"Read, search, send, and manage Gmail messages and labels\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your Gmail only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=gmail.googleapis.com) to enable the Gmail API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Web application\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"gmail_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/gmail.modify\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Gmail\n\nRead with `gmail_get`, send with `gmail_send`, draft with `gmail_draft`. `http_request` is only for list/search, labels and trash. The Gmail credential is attached automatically to every `gmail.googleapis.com` request — **never include an `auth` block**. Never run code.\n\n## Prerequisites\n\nGmail must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Gmail from Settings, or to\nset the `gmail_access_token` credential.\n\n## Base URL\n\n`https://gmail.googleapis.com/gmail/v1/users/me`\n\nThe host is `gmail.googleapis.com` — not `www.googleapis.com`. Send is at `/messages/send`, never `/send`.\n\n## Read messages\n\n1. List with `http_request`. Use `q` for Gmail search (`from:`, `subject:`, `is:unread`, `newer_than:7d`, `has:attachment`, `label:work`). Keep `maxResults` ≤ 10. The result is ids only.\n2. Call `gmail_get` with each `id`. It returns `headers` (From, To, Subject, Date), `snippet` and, with `format: \"full\"`, `text`.\n3. Answer from those fields. Never run code.\n\n`http_request` to list:\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages\",\n \"method\": \"GET\",\n \"query\": { \"q\": \"is:unread newer_than:7d\", \"labelIds\": \"INBOX\", \"maxResults\": 10 }\n}\n```\n\n`gmail_get` for each id:\n\n```json\n{ \"id\": \"1a0c420faf8b68fe\" }\n```\n\nUse `format: \"full\"` only when the user asks what a message says.\n\n## Common Operations\n\n### Send a message\n\nUse `gmail_send` with semantic args: `to` (array), `subject`, and `text` (or `html`). Optional: `cc`, `bcc`, `replyTo`. The tool builds the RFC 2822 message and base64url-encodes it — never construct `raw` yourself.\n\n```json\n{ \"to\": [\"<RECIPIENT_EMAIL>\"], \"subject\": \"Subject line\", \"text\": \"Message body\" }\n```\n\nIf you supply both `text` and `html`, only `text` is sent — pick one.\n\nConfirm recipient, subject, and body with the user before sending. Report success only when the response contains a message `id`.\n\n### Draft a message\n\nUse `gmail_draft` with the same envelope as `gmail_send`.\n\n```json\n{ \"to\": [\"<RECIPIENT_EMAIL>\"], \"subject\": \"Subject line\", \"text\": \"Draft body\" }\n```\n\n### Reply to a message (preserves threading)\n\nA reply is `gmail_send` with the original's `threadId`, `inReplyTo`, and `references`. Without these Gmail starts a new thread.\n\n1. `gmail_get` the original; capture its `threadId` and its `Message-ID` and `References` headers.\n2. Send with `to` <- original From (or Reply-To), `subject` <- `Re: ` + original (don't double-prefix), `inReplyTo` <- original Message-ID, `references` <- original References then that Message-ID. Keep angle brackets.\n\n```json\n{\n \"to\": [\"<RECIPIENT_EMAIL>\"],\n \"subject\": \"Re: Original subject\",\n \"text\": \"Reply body\",\n \"threadId\": \"{originalThreadId}\",\n \"inReplyTo\": \"<msg-id@mail.gmail.com>\",\n \"references\": \"<msg-id@mail.gmail.com>\"\n}\n```\n\n### Modify labels (mark read, archive, star)\n\nSystem labels: `INBOX`, `UNREAD`, `STARRED`, `IMPORTANT`, `SPAM`, `TRASH`. Mark read = remove `UNREAD`; archive = remove `INBOX`; star = add `STARRED`.\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}/modify\",\n \"method\": \"POST\",\n \"body\": { \"removeLabelIds\": [\"UNREAD\", \"INBOX\"] }\n}\n```\n\n### Trash a message\n\n```json\n{\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}/trash\",\n \"method\": \"POST\"\n}\n```\n\n## Output Policy\n\n- Lists: up to 5 entries with subject, sender, and a human-readable date. Fetch `text` only when asked.\n- Modify/trash: state the user-facing effect (\"marked 3 messages as read\"), not the label diff.\n- Confirm destructive or outgoing actions (send, reply, trash) with the user first.\n\n## Common Mistakes\n\n- Using `www.googleapis.com` for Gmail, or building send/draft/reply through `http_request` instead of `gmail_send`/`gmail_draft`.\n- Treating `messages.list` results as if they had subjects — call `gmail_get` for each id.\n- Reading a message through `http_request` or code instead of `gmail_get`.\n- Replying without `threadId` + `inReplyTo`/`references` — Gmail starts a new thread.\n- Sending a placeholder (`<RECIPIENT_EMAIL>`, `recipient@example.com`) — the runtime refuses these.\n- Inventing an address. The runtime refuses any recipient absent from the conversation and from earlier tool results; search Gmail or ask the user.\n- Including an `auth` block by hand — credentials attach automatically; a mistyped key breaks the request.\n",
36
+ "gmail/operations.json": "{\n \"operations\": [\n {\n \"tool\": \"gmail_send\",\n \"description\": \"Send an email through Gmail, or reply in a thread when threadId and inReplyTo are given. Pass the message as plain fields — never build the RFC 2822 message or base64 yourself. Confirm the recipient and body with the user before sending.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"to\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" },\n \"description\": \"Recipient email addresses\"\n },\n \"subject\": { \"type\": \"string\", \"description\": \"Subject line\" },\n \"text\": { \"type\": \"string\", \"description\": \"Plain-text body\" },\n \"html\": { \"type\": \"string\", \"description\": \"HTML body, used when text is absent\" },\n \"cc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"bcc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"replyTo\": { \"type\": \"string\" },\n \"threadId\": {\n \"type\": \"string\",\n \"description\": \"Thread to reply in, from a previous messages.list or messages.get\"\n },\n \"inReplyTo\": {\n \"type\": \"string\",\n \"description\": \"Message-ID header of the message being replied to\"\n },\n \"references\": { \"type\": \"string\", \"description\": \"References header of the thread\" }\n },\n \"required\": [\"to\", \"subject\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/send\",\n \"builder\": \"gmail-send\"\n }\n },\n {\n \"tool\": \"gmail_draft\",\n \"description\": \"Create a Gmail draft without sending it. Pass the message as plain fields — never build the RFC 2822 message or base64 yourself.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"to\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" },\n \"description\": \"Recipient email addresses\"\n },\n \"subject\": { \"type\": \"string\", \"description\": \"Subject line\" },\n \"text\": { \"type\": \"string\", \"description\": \"Plain-text body\" },\n \"html\": { \"type\": \"string\", \"description\": \"HTML body, used when text is absent\" },\n \"cc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"bcc\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n \"replyTo\": { \"type\": \"string\" },\n \"threadId\": { \"type\": \"string\", \"description\": \"Thread the draft replies in\" },\n \"inReplyTo\": {\n \"type\": \"string\",\n \"description\": \"Message-ID header of the message being replied to\"\n },\n \"references\": { \"type\": \"string\", \"description\": \"References header of the thread\" }\n },\n \"required\": [\"to\", \"subject\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/drafts\",\n \"builder\": \"gmail-draft\"\n }\n },\n {\n \"tool\": \"gmail_get\",\n \"description\": \"Read one Gmail message by id: sender, recipients, subject, date, snippet and, with format \\\"full\\\", the message text. Call it for each id that messages.list returned. Never read a message with http_request or code.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\", \"description\": \"Message id from messages.list\" },\n \"format\": {\n \"type\": \"string\",\n \"enum\": [\"metadata\", \"full\"],\n \"description\": \"metadata (default) for lists and summaries; full only when the user needs the message text\"\n }\n },\n \"required\": [\"id\"]\n },\n \"request\": {\n \"method\": \"GET\",\n \"url\": \"https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}\",\n \"query\": { \"metadataHeaders\": \"Subject,From,To,Date,Message-ID,References\" },\n \"builder\": \"gmail-get\"\n },\n \"response\": { \"decode\": \"gmail-message\" }\n }\n ]\n}\n",
37
+ "google-calendar/SKILL.md": "---\nname: google-calendar\ndescription: List, create, update, and delete Google Calendar events via the Google Calendar REST API.\naliases: [calendar, meeting+create, meeting+schedule, meeting+book, meeting+move, meeting+cancel, event+create]\ntools: [http_request, calendar_create_meet_event, calendar_add_meet]\nplatform: [darwin, linux, win32]\ncredentials: [google_calendar_access_token]\nallow_list: [https://www.googleapis.com/calendar/v3/]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Calendar\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_calendar_access_token\",\n \"description\": \"List, create, update, and delete Google Calendar events\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your calendars only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com) to enable the Google Calendar API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Web application\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_calendar_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/calendar\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Calendar\n\nUse `calendar_create_meet_event` and `calendar_add_meet` for Google Meet links, and `http_request` for everything else (list, plain create/update/delete, free/busy). The Google Calendar credential is attached automatically to every `www.googleapis.com/calendar/v3/` request — **never include an `auth` block**.\n\n## Prerequisites\n\nGoogle Calendar must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Calendar from Settings, or to\nset the `google_calendar_access_token` credential.\n\n## Base URL\n\n`https://www.googleapis.com/calendar/v3` — default `calendarId` is `primary` unless the user names another calendar.\n\n## Temporal Accuracy (Critical)\n\n- Resolve relative dates (\"today\", \"tomorrow\", \"next week\", \"in 2 hours\") to absolute ISO 8601 timestamps **with timezone offset** before calling the API.\n- Never infer \"now\" from memory when building `timeMin`, `timeMax`, or event start/end values.\n- If the date or time is ambiguous, ask the user instead of guessing.\n\nExample: annotation `[Current local time: 2026-08-12 14:00:00 (UTC-03:00)]`, user asks \"in 30 minutes for 1 hour\" -> `start.dateTime` = `2026-08-12T14:30:00-03:00`, `end.dateTime` = `2026-08-12T15:30:00-03:00`.\n\n## Common Operations\n\n### List upcoming events\n\nAlways use `singleEvents=true` and `orderBy=startTime` so recurring events expand and sort correctly.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events\",\n \"method\": \"GET\",\n \"query\": {\n \"timeMin\": \"<resolved ISO 8601 with offset>\",\n \"maxResults\": 10,\n \"singleEvents\": true,\n \"orderBy\": \"startTime\"\n }\n}\n```\n\n### Create an event\n\nAsk for missing required fields (summary, start, end) before creating.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events\",\n \"method\": \"POST\",\n \"body\": {\n \"summary\": \"Design review\",\n \"start\": { \"dateTime\": \"2026-06-10T14:00:00-03:00\" },\n \"end\": { \"dateTime\": \"2026-06-10T15:00:00-03:00\" },\n \"attendees\": [{ \"email\": \"colleague@example.com\" }]\n }\n}\n```\n\n### Create an event with a Google Meet link\n\nUse `calendar_create_meet_event` with `summary`, `start`, `end` (ISO 8601 with offset), and optionally `timeZone`, `description`, `location`, `attendees`, `calendarId`.\n\n```json\n{\n \"summary\": \"Sync call\",\n \"start\": \"2026-06-10T14:00:00-03:00\",\n \"end\": \"2026-06-10T14:30:00-03:00\"\n}\n```\n\nTo add a Meet link to an event that already exists, use `calendar_add_meet` with `eventId` (and `calendarId` if not `primary`).\n\n### Update an event\n\n`PATCH` with only the fields to change.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events/{eventId}\",\n \"method\": \"PATCH\",\n \"body\": { \"summary\": \"Updated title\" }\n}\n```\n\n### Delete an event\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/primary/events/{eventId}\",\n \"method\": \"DELETE\"\n}\n```\n\n### Free/busy query\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/freeBusy\",\n \"method\": \"POST\",\n \"body\": {\n \"timeMin\": \"2026-06-10T00:00:00Z\",\n \"timeMax\": \"2026-06-11T00:00:00Z\",\n \"items\": [{ \"id\": \"primary\" }]\n }\n}\n```\n\n### List calendars\n\n```json\n{\n \"url\": \"https://www.googleapis.com/calendar/v3/users/me/calendarList\",\n \"method\": \"GET\"\n}\n```\n\n## Output Policy\n\n- Present events with title, date/time (with timezone), location, and attendees when available.\n- For create/update, report the event `id`, start/end time, and `htmlLink` rendered as a Markdown link — e.g. `[event title](htmlLink)`. Never present the URL bare or wrapped in backticks, or it won't be clickable.\n- Confirm before deleting events; show the event title and time so the user can verify.\n\n## Common Mistakes\n\n- Sending natural-language dates instead of absolute ISO 8601 with timezone offset.\n- Listing without `singleEvents=true` — recurring events come back collapsed and unsorted.\n- Calling get/patch/delete without a real `eventId` — list/search first.\n- Including an `auth` block by hand — credentials attach automatically; a mistyped key breaks the request.\n",
38
38
  "google-calendar/operations.json": "{\n \"operations\": [\n {\n \"tool\": \"calendar_create_meet_event\",\n \"description\": \"Create a Google Calendar event that has a Google Meet link. Use this whenever the user asks for a meeting with a video call; a plain event without a call stays on http_request.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"summary\": { \"type\": \"string\", \"description\": \"Event title\" },\n \"start\": {\n \"type\": \"string\",\n \"description\": \"Start time, ISO 8601 with a UTC offset, e.g. 2026-03-04T15:00:00-03:00\"\n },\n \"end\": {\n \"type\": \"string\",\n \"description\": \"End time, ISO 8601 with a UTC offset\"\n },\n \"timeZone\": {\n \"type\": \"string\",\n \"description\": \"IANA time zone, e.g. America/Sao_Paulo\"\n },\n \"description\": { \"type\": \"string\" },\n \"location\": { \"type\": \"string\" },\n \"attendees\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" },\n \"description\": \"Attendee email addresses\"\n },\n \"calendarId\": { \"type\": \"string\", \"description\": \"Defaults to primary\" }\n },\n \"required\": [\"summary\", \"start\", \"end\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events\",\n \"urlDefaults\": { \"calendarId\": \"primary\" },\n \"builder\": \"calendar-meet-event\"\n }\n },\n {\n \"tool\": \"calendar_add_meet\",\n \"description\": \"Add a Google Meet link to an existing Google Calendar event.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"eventId\": {\n \"type\": \"string\",\n \"description\": \"Event id, from a previous events.list or events.insert\"\n },\n \"calendarId\": { \"type\": \"string\", \"description\": \"Defaults to primary\" }\n },\n \"required\": [\"eventId\"]\n },\n \"request\": {\n \"method\": \"PATCH\",\n \"url\": \"https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events/{eventId}\",\n \"urlDefaults\": { \"calendarId\": \"primary\" },\n \"builder\": \"calendar-add-meet\"\n }\n }\n ]\n}\n",
39
- "google-docs/SKILL.md": "---\nname: google-docs\ndescription: Create, read, and edit Google Docs documents via the Google Docs REST API.\ntools: [http_request, docs_create, docs_append_text]\nplatform: [darwin, linux, win32]\ncredentials: [google_docs_access_token]\nallow_list: [https://docs.googleapis.com/v1/documents, https://www.googleapis.com/drive/v3/files]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Docs\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_docs_access_token\",\n \"description\": \"Create, read, and edit Google Docs documents\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your documents and Drive files only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=docs.googleapis.com,drive.googleapis.com) to enable the Google Docs API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Desktop app\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_docs_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/documents\",\n \"https://www.googleapis.com/auth/drive\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Docs\n\nUse `docs_create` to create a document and `docs_append_text` to add text or\npages to it; use `http_request` for everything else (get, replace, insert at a\nposition, delete ranges, Drive search, trash). For each `http_request`, set\n`auth.tokenCredentialKey` to `google_docs_access_token`.\n\nNever use `exec`, Python, curl, shell flags such as `-H`/`-d`, or a JSON string\nto make a Google Docs request. Call one tool per operation with one complete\nstructured object. In particular, `body` must be an object, not a serialized\nJSON string; the tool JSON-encodes it and sets `Content-Type: application/json`\nautomatically.\n\n## Load the Recipe File First\n\nThis file carries no requests. The working request shapes live in three\nreference files — load the one for the job with the `skill` tool BEFORE calling\n`http_request`, then copy its request and change only the values:\n\nEach load is a real `skill` tool call — printing the call as JSON or text in\nyour reply loads nothing.\n\n- **Reading or finding documents** — \"what does document X say\", \"summarize\n my doc\", \"find / list my documents\": call the `skill` tool with\n `name: \"google-docs\"` and `file: \"references/read.md\"`.\n- **Creating a new document** (no existing document involved; one page or\n many): call the `skill` tool with `name: \"google-docs\"` and\n `file: \"references/create.md\"` — it covers `docs_create` and filling the\n document with `docs_append_text`.\n- **Changing an existing document** — append or insert text, find and\n replace, delete a passage, move it to the trash: call the `skill` tool with\n `name: \"google-docs\"` and `file: \"references/edit.md\"`.\n\nNever write a request from memory. The recipes carry the exact request shapes\n(`batchUpdate` request names, index rules, page limits) that fail in\nnon-obvious ways when improvised; loading the file is one cheap read-only call.\n\n## Prerequisites\n\nGoogle Docs must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Docs from Settings, or to\nset the `google_docs_access_token` credential.\n\nIf Google returns `403 PERMISSION_DENIED` with reason `SERVICE_DISABLED`, the\ncredential is working but the OAuth client's Google Cloud project has not\nenabled the Google Docs API. Do not ask the user to reconnect. Tell the project\nowner to open the response's `activationUrl`, enable the API, and retry after\npropagation.\n\n## Identifiers\n\nDocuments are identified by `documentId` — the alphanumeric string in the URL\n`docs.google.com/document/d/{documentId}/...`, and the `documentId` field of a\ncreate response. Listing and searching documents go through the Google Drive\nAPI (`references/read.md`), never the Docs API.\n\n## Output Policy\n\n- For document reads, extract and present the text content clearly — do not\n echo the raw JSON structure.\n- For edits, confirm the change with the user first (what text is being\n inserted, replaced, or deleted) unless they already spelled it out.\n- Report success only when the `batchUpdate` response contains `replies` with\n no errors, and name the document (title or link) in the answer.\n",
39
+ "google-docs/SKILL.md": "---\nname: google-docs\ndescription: Create, read, and edit Google Docs documents via the Google Docs REST API.\ntools: [http_request, docs_create, docs_append_text]\nplatform: [darwin, linux, win32]\ncredentials: [google_docs_access_token]\nallow_list: [https://docs.googleapis.com/v1/documents, https://www.googleapis.com/drive/v3/files]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Docs\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_docs_access_token\",\n \"description\": \"Create, read, and edit Google Docs documents\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your documents and Drive files only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=docs.googleapis.com,drive.googleapis.com) to enable the Google Docs API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Web application\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_docs_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/documents\",\n \"https://www.googleapis.com/auth/drive\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Docs\n\nUse `docs_create` to create a document and `docs_append_text` to add text or\npages to it; use `http_request` for everything else (get, replace, insert at a\nposition, delete ranges, Drive search, trash). For each `http_request`, set\n`auth.tokenCredentialKey` to `google_docs_access_token`.\n\nNever use `exec`, Python, curl, shell flags such as `-H`/`-d`, or a JSON string\nto make a Google Docs request. Call one tool per operation with one complete\nstructured object. In particular, `body` must be an object, not a serialized\nJSON string; the tool JSON-encodes it and sets `Content-Type: application/json`\nautomatically.\n\n## Load the Recipe File First\n\nThis file carries no requests. The working request shapes live in three\nreference files — load the one for the job with the `skill` tool BEFORE calling\n`http_request`, then copy its request and change only the values:\n\nEach load is a real `skill` tool call — printing the call as JSON or text in\nyour reply loads nothing.\n\n- **Reading or finding documents** — \"what does document X say\", \"summarize\n my doc\", \"find / list my documents\": call the `skill` tool with\n `name: \"google-docs\"` and `file: \"references/read.md\"`.\n- **Creating a new document** (no existing document involved; one page or\n many): call the `skill` tool with `name: \"google-docs\"` and\n `file: \"references/create.md\"` — it covers `docs_create` and filling the\n document with `docs_append_text`.\n- **Changing an existing document** — append or insert text, find and\n replace, delete a passage, move it to the trash: call the `skill` tool with\n `name: \"google-docs\"` and `file: \"references/edit.md\"`.\n\nNever write a request from memory. The recipes carry the exact request shapes\n(`batchUpdate` request names, index rules, page limits) that fail in\nnon-obvious ways when improvised; loading the file is one cheap read-only call.\n\n## Prerequisites\n\nGoogle Docs must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Docs from Settings, or to\nset the `google_docs_access_token` credential.\n\nIf Google returns `403 PERMISSION_DENIED` with reason `SERVICE_DISABLED`, the\ncredential is working but the OAuth client's Google Cloud project has not\nenabled the Google Docs API. Do not ask the user to reconnect. Tell the project\nowner to open the response's `activationUrl`, enable the API, and retry after\npropagation.\n\n## Identifiers\n\nDocuments are identified by `documentId` — the alphanumeric string in the URL\n`docs.google.com/document/d/{documentId}/...`, and the `documentId` field of a\ncreate response. Listing and searching documents go through the Google Drive\nAPI (`references/read.md`), never the Docs API.\n\n## Output Policy\n\n- For document reads, extract and present the text content clearly — do not\n echo the raw JSON structure.\n- For edits, confirm the change with the user first (what text is being\n inserted, replaced, or deleted) unless they already spelled it out.\n- Report success only when the `batchUpdate` response contains `replies` with\n no errors, and name the document (title or link) in the answer.\n",
40
40
  "google-docs/operations.json": "{\n \"operations\": [\n {\n \"tool\": \"docs_create\",\n \"description\": \"Create an empty Google Doc with a title. Call it exactly once per requested document, then append content with docs_append_text using the returned documentId.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\",\n \"description\": \"Document title\"\n }\n },\n \"required\": [\n \"title\"\n ]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://docs.googleapis.com/v1/documents\",\n \"builder\": \"docs-create\"\n },\n \"response\": {\n \"pick\": [\n \"documentId\",\n \"title\"\n ]\n }\n },\n {\n \"tool\": \"docs_append_text\",\n \"description\": \"Append text to the end of a Google Doc, optionally followed by a page break. One page (80 words or fewer) per call; set pageBreak on every page except the last. Never compute indexes yourself.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"documentId\": {\n \"type\": \"string\",\n \"description\": \"Document id, from docs_create or the docs.google.com/document/d/{id} URL\"\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Text to append; end it with a newline\"\n },\n \"pageBreak\": {\n \"type\": \"boolean\",\n \"description\": \"Insert a page break after the text\"\n }\n },\n \"required\": [\n \"documentId\",\n \"text\"\n ]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://docs.googleapis.com/v1/documents/{documentId}:batchUpdate\",\n \"builder\": \"docs-append-text\"\n }\n }\n ]\n}\n",
41
41
  "google-docs/references/create.md": "# Creating a Google Doc\n\nTwo typed tools do the whole job: `docs_create` makes the document,\n`docs_append_text` fills it. Typed tools select their own credential. For each fallback `http_request`,\nset `auth.tokenCredentialKey` to `google_docs_access_token`.\n\n## Create the document\n\nCall `docs_create` with the title:\n\n```json\n{ \"title\": \"My Document\" }\n```\n\nThe result contains the `documentId`. Capture it and keep using it.\nCreate the document exactly once. Do not follow a successful create with a\nDrive search, a GET, or another create just to rediscover the ID. If the result has no\n`documentId`, stop and report that response problem; never append without the\nID.\n\n## Add the content\n\nCall `docs_append_text` once per page. Put the document title as the first\nline of the first page instead of spending a call on a heading, end the text\nwith a newline, and keep each call at 80 words or fewer. The 80-word limit\napplies even when the user asks for only one page. Never expand a one-page\nrequest into a long list in one call.\n\n```json\n{\n \"documentId\": \"<id from docs_create>\",\n \"text\": \"My Document\\n\\nConcise content.\\n\",\n \"pageBreak\": false\n}\n```\n\n## Multi-page documents\n\nOne requested page per `docs_append_text` call, in order, each 80 words or\nfewer, with `\"pageBreak\": true` on every page except the last — five requested\npages therefore take five append calls. Wait for a successful result before\nappending the next page. Do not place several pages of text in one call: if\nthe tool reports the arguments are not valid JSON, the call was too large or\nincomplete, so retry only that page with shorter text.\n\n## Finishing\n\nReport success only when every append result came back without an error.\nAnswer with the document title and the link\n`https://docs.google.com/document/d/{documentId}/edit`; do not read the\ndocument back to \"confirm\".\n\n## Without the typed tools\n\nThe typed tools wrap these two Docs API requests; use them only if the typed\ntools are missing from this chat:\n\n```json\n{\n \"url\": \"https://docs.googleapis.com/v1/documents\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"POST\",\n \"body\": { \"title\": \"My Document\" }\n}\n```\n\n```json\n{\n \"url\": \"https://docs.googleapis.com/v1/documents/{documentId}:batchUpdate\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"POST\",\n \"body\": {\n \"requests\": [\n {\n \"insertText\": {\n \"endOfSegmentLocation\": {},\n \"text\": \"Page heading\\n\\nConcise page content.\\n\"\n }\n },\n { \"insertPageBreak\": { \"endOfSegmentLocation\": {} } }\n ]\n }\n}\n```\n\nThen it is one requested page per `batchUpdate` call, the same 80-word limit,\nand the `insertPageBreak` request omitted on the last page;\nnever call `/v1/documents:batchUpdate` without the ID.\n\n## Common Mistakes\n\n- Recreating or searching for a document after a successful create instead of\n using the returned `documentId` — this produces duplicates and wastes rounds.\n- Sending a whole multi-page document in one call — one page of at most 80\n words per call, `pageBreak` on every page except the last.\n- Computing indexes yourself — `docs_append_text` and `endOfSegmentLocation`\n never need one.\n",
42
42
  "google-docs/references/edit.md": "# Editing an Existing Google Doc\n\nEvery request is one `http_request` call with a structured object. For each request, set `auth.tokenCredentialKey` to `google_docs_access_token`. All content edits go through\n`batchUpdate` — there is no PATCH endpoint for document body changes.\n\nGet the `documentId` from the URL the user gave, from an earlier create\nresponse, or from a Drive search (`references/read.md`).\n\nEvery `http_request` edit is ONE `POST …:batchUpdate` whose body is `{ \"requests\": [ … ] }` —\na bare request object without the `requests` array is rejected with 400. Copy\nthe request shapes below exactly: the field names are fixed by Google, and any\nother name (`searchText`, `replacementText`, `find`, `replace`, `newText`) fails\nwith `Unknown name`. One edit per user request — an edit never needs a\npreceding insert of text that is already in the document.\n\n## Append text\n\nCall `docs_append_text` — it appends at the end of the document with no index\narithmetic and no preliminary GET. End the text with a newline; keep it at 80\nwords or fewer per call.\n\n```json\n{\n \"documentId\": \"<documentId>\",\n \"text\": \"\\nNew paragraph.\\n\",\n \"pageBreak\": false\n}\n```\n\nThe raw equivalent, for when the typed tool is missing from this chat, is a\n`batchUpdate` with `\"insertText\": { \"endOfSegmentLocation\": {}, \"text\": \"…\" }`.\n\n## Insert text at a position\n\nUse an explicit `location.index` only when editing at a known position. A new\nblank document's first valid insertion point is index 1; index 2 is outside its\nempty paragraph. Indexes count UTF-16 code units and change with every edit, so\n`GET` the document first when you need one.\n\n```json\n{\n \"url\": \"https://docs.googleapis.com/v1/documents/{documentId}:batchUpdate\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"POST\",\n \"body\": {\n \"requests\": [\n {\n \"insertText\": {\n \"location\": { \"index\": 1 },\n \"text\": \"Hello, World!\"\n }\n }\n ]\n }\n}\n```\n\n## Replace all text matching a pattern\n\nUse `replaceAllText` to find and replace — exactly these keys:\n`containsText` (with `text` and optional `matchCase`) and `replaceText`.\n`containsText.text` is a regex — escape special characters (`.`, `*`, `+`,\n`?`, `[`, `]`, `(`, `)`, `{`, `}`, `^`, `$`, `|`, `\\`). No `GET` is needed\nfirst, and no `insertText` belongs in the same request.\n\n```json\n{\n \"url\": \"https://docs.googleapis.com/v1/documents/{documentId}:batchUpdate\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"POST\",\n \"body\": {\n \"requests\": [\n {\n \"replaceAllText\": {\n \"containsText\": { \"text\": \"old text\", \"matchCase\": true },\n \"replaceText\": \"new text\"\n }\n }\n ]\n }\n}\n```\n\nThe reply's `occurrencesChanged` says how many matches were replaced; `0` means\nthe text was not found — tell the user instead of retrying with guesses.\n\n## Delete a range of text\n\nFetch the document first, then specify the start and exclusive end index.\n\n```json\n{\n \"url\": \"https://docs.googleapis.com/v1/documents/{documentId}:batchUpdate\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"POST\",\n \"body\": {\n \"requests\": [\n {\n \"deleteContentRange\": {\n \"range\": { \"startIndex\": 10, \"endIndex\": 20 }\n }\n }\n ]\n }\n}\n```\n\n## Move a document to the trash\n\nTrashing goes through the Drive API; the user can restore it from Drive's\ntrash. Only do this when the user explicitly asks to delete or remove the\ndocument.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{documentId}\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"PATCH\",\n \"body\": { \"trashed\": true }\n}\n```\n\n## Finishing\n\nReport success only when the `batchUpdate` response contains `replies` with no\nerrors. Do not read the document back to confirm an edit.\n\n## Common Mistakes\n\n- Using PATCH on the document body instead of `batchUpdate` — PATCH only\n updates metadata like title, not content.\n- Treating indexes like array offsets — they count UTF-16 code units, range\n ends are exclusive, and a blank document's first usable body index is 1.\n- Using `insertionIndex` or placing `location.index` directly under\n `insertText` — the supported shape is `insertText.location.index`; for\n appends, prefer `insertText.endOfSegmentLocation`.\n- Attempting a numeric-position edit without fetching the document first — you\n won't know the current indexes.\n- Not escaping regex special characters in `containsText` patterns.\n- Renaming the fields — `searchText`, `replacementText`, `replacement`, `find`\n all return 400 `Unknown name`; only `containsText` + `replaceText` exist.\n- Sending a request object without the `{ \"requests\": [ … ] }` wrapper.\n- Inserting text that is already in the document before an edit — a replace or\n delete works on the existing content directly.\n- Passing curl/Python text or serialized JSON to `http_request` instead of one\n structured tool-call object.\n",
43
43
  "google-docs/references/read.md": "# Reading and Finding Google Docs\n\nEvery request is one `http_request` call with a structured object. For each request, set `auth.tokenCredentialKey` to `google_docs_access_token`.\n\n## Get a document\n\n```json\n{\n \"url\": \"https://docs.googleapis.com/v1/documents/{documentId}\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"GET\"\n}\n```\n\nReturns the full document structure with all content, styling, and revisions.\nThe text lives in `body.content[]` → `paragraph.elements[]` → `textRun.content`;\nwalk that array and join the `content` strings to reconstruct the text. Present\nthe text, not the JSON. A read-only question (\"what does the doc say\", \"summarize\nit\") ends here — do not follow a read with an edit.\n\n## List or search for documents\n\nUse the Google Drive API with a MIME type filter to find Google Docs by name.\nThe `documentId` you need for a follow-up read or edit is the file `id`.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files\",\n \"auth\": { \"tokenCredentialKey\": \"google_docs_access_token\" },\n \"method\": \"GET\",\n \"query\": {\n \"q\": \"mimeType='application/vnd.google-apps.document' and name contains 'report'\",\n \"pageSize\": 10,\n \"fields\": \"files(id,name,modifiedTime,webViewLink)\"\n }\n}\n```\n\nTo list recent documents without a name filter, drop the `and name contains …`\nclause and add `\"orderBy\": \"modifiedTime desc\"` to the query. Run the search\nONCE; if it returns nothing, say so rather than retrying with variations.\n\n## Common Mistakes\n\n- Searching with the wrong MIME type — it must be\n `application/vnd.google-apps.document`.\n- Echoing the raw JSON to the user instead of the extracted text.\n- Fetching the document through the Drive API — content comes only from the\n Docs API `GET`.\n",
44
- "google-drive/SKILL.md": "---\nname: google-drive\ndescription: Search, list, download metadata, and manage Google Drive files and folders via the Google Drive REST API.\ntools: [http_request, drive_create_folder, drive_trash]\nplatform: [darwin, linux, win32]\ncredentials: [google_drive_access_token]\nallow_list: [https://www.googleapis.com/drive/v3/]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Drive\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_drive_access_token\",\n \"description\": \"Search, list, and manage Google Drive files and folders\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your Drive files only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=drive.googleapis.com) to enable the Google Drive API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Desktop app\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_drive_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/drive\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Drive\n\nUse `drive_create_folder` to create folders and `drive_trash` to trash files; use `http_request` for everything else (list, search, get, export, rename). For each `http_request`, set `auth.tokenCredentialKey` to `google_drive_access_token`.\n\n## Prerequisites\n\nGoogle Drive must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Drive from Settings, or to\nset the `google_drive_access_token` credential.\n\n## Base URL\n\n`https://www.googleapis.com/drive/v3`\n\nAlways pass a `fields` mask on list/get calls — default payloads are huge. Keep `pageSize` small (5–10) unless the user asks for more.\n\n## Common Operations\n\n### List recent files\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": {\n \"pageSize\": 5,\n \"orderBy\": \"modifiedTime desc\",\n \"fields\": \"files(id,name,mimeType,modifiedTime,webViewLink),nextPageToken\"\n }\n}\n```\n\n### Search by name\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": {\n \"q\": \"name contains 'roadmap' and trashed = false\",\n \"pageSize\": 5,\n \"fields\": \"files(id,name,mimeType,modifiedTime,webViewLink),nextPageToken\"\n }\n}\n```\n\nOther useful `q` recipes: folders only → `mimeType = 'application/vnd.google-apps.folder' and trashed = false`; inside a folder → `'{folderId}' in parents and trashed = false`.\n\n### Get file metadata\n\nOnly when the `fileId` is known — list/search first instead of guessing ids.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{fileId}\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": {\n \"fields\": \"id,name,mimeType,size,modifiedTime,owners(displayName,emailAddress),webViewLink\"\n }\n}\n```\n\n### Export Google Doc as plain text\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{fileId}/export\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": { \"mimeType\": \"text/plain\" }\n}\n```\n\n### Rename or move a file to trash\n\n`PATCH` with only the fields to change.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{fileId}\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"PATCH\",\n \"body\": { \"trashed\": true }\n}\n```\n\n## MIME Types Reference\n\n- Google Docs: `application/vnd.google-apps.document` — export as `text/plain`\n- Google Sheets: `application/vnd.google-apps.spreadsheet` — export as `text/csv`\n- Google Slides: `application/vnd.google-apps.presentation` — export as `text/plain`\n- Folder: `application/vnd.google-apps.folder`\n\n## Output Policy\n\n- For lists, show name, type, and last modified; limit to 5 files unless asked for more.\n- When `webViewLink` is available, render the file name as a Markdown link to it — `[name](webViewLink)`. Never present the URL bare or wrapped in backticks, or it won't be clickable.\n- Never attempt to download large binary files; export text content instead.\n- Confirm before trashing files; show the file name so the user can verify.\n\n## Common Mistakes\n\n- Calling `files.list` without a `fields` mask — payloads are huge by default.\n- Calling `files.get` with a guessed file id — list/search first.\n- Downloading binaries instead of exporting text.\n",
44
+ "google-drive/SKILL.md": "---\nname: google-drive\ndescription: Search, list, download metadata, and manage Google Drive files and folders via the Google Drive REST API.\ntools: [http_request, drive_create_folder, drive_trash]\nplatform: [darwin, linux, win32]\ncredentials: [google_drive_access_token]\nallow_list: [https://www.googleapis.com/drive/v3/]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Drive\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_drive_access_token\",\n \"description\": \"Search, list, and manage Google Drive files and folders\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your Drive files only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=drive.googleapis.com) to enable the Google Drive API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Web application\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_drive_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/drive\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Drive\n\nUse `drive_create_folder` to create folders and `drive_trash` to trash files; use `http_request` for everything else (list, search, get, export, rename). For each `http_request`, set `auth.tokenCredentialKey` to `google_drive_access_token`.\n\n## Prerequisites\n\nGoogle Drive must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Drive from Settings, or to\nset the `google_drive_access_token` credential.\n\n## Base URL\n\n`https://www.googleapis.com/drive/v3`\n\nAlways pass a `fields` mask on list/get calls — default payloads are huge. Keep `pageSize` small (5–10) unless the user asks for more.\n\n## Common Operations\n\n### List recent files\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": {\n \"pageSize\": 5,\n \"orderBy\": \"modifiedTime desc\",\n \"fields\": \"files(id,name,mimeType,modifiedTime,webViewLink),nextPageToken\"\n }\n}\n```\n\n### Search by name\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": {\n \"q\": \"name contains 'roadmap' and trashed = false\",\n \"pageSize\": 5,\n \"fields\": \"files(id,name,mimeType,modifiedTime,webViewLink),nextPageToken\"\n }\n}\n```\n\nOther useful `q` recipes: folders only → `mimeType = 'application/vnd.google-apps.folder' and trashed = false`; inside a folder → `'{folderId}' in parents and trashed = false`.\n\n### Get file metadata\n\nOnly when the `fileId` is known — list/search first instead of guessing ids.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{fileId}\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": {\n \"fields\": \"id,name,mimeType,size,modifiedTime,owners(displayName,emailAddress),webViewLink\"\n }\n}\n```\n\n### Export Google Doc as plain text\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{fileId}/export\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"GET\",\n \"query\": { \"mimeType\": \"text/plain\" }\n}\n```\n\n### Rename or move a file to trash\n\n`PATCH` with only the fields to change.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{fileId}\",\n \"auth\": { \"tokenCredentialKey\": \"google_drive_access_token\" },\n \"method\": \"PATCH\",\n \"body\": { \"trashed\": true }\n}\n```\n\n## MIME Types Reference\n\n- Google Docs: `application/vnd.google-apps.document` — export as `text/plain`\n- Google Sheets: `application/vnd.google-apps.spreadsheet` — export as `text/csv`\n- Google Slides: `application/vnd.google-apps.presentation` — export as `text/plain`\n- Folder: `application/vnd.google-apps.folder`\n\n## Output Policy\n\n- For lists, show name, type, and last modified; limit to 5 files unless asked for more.\n- When `webViewLink` is available, render the file name as a Markdown link to it — `[name](webViewLink)`. Never present the URL bare or wrapped in backticks, or it won't be clickable.\n- Never attempt to download large binary files; export text content instead.\n- Confirm before trashing files; show the file name so the user can verify.\n\n## Common Mistakes\n\n- Calling `files.list` without a `fields` mask — payloads are huge by default.\n- Calling `files.get` with a guessed file id — list/search first.\n- Downloading binaries instead of exporting text.\n",
45
45
  "google-drive/operations.json": "{\n \"operations\": [\n {\n \"tool\": \"drive_create_folder\",\n \"description\": \"Create a Google Drive folder, optionally inside a parent folder.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": { \"type\": \"string\", \"description\": \"Folder name\" },\n \"parentId\": {\n \"type\": \"string\",\n \"description\": \"Parent folder id; omit for the Drive root\"\n }\n },\n \"required\": [\"name\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://www.googleapis.com/drive/v3/files\",\n \"builder\": \"drive-create-folder\"\n }\n },\n {\n \"tool\": \"drive_trash\",\n \"description\": \"Move a Google Drive file or folder to the trash. Confirm the file name with the user first; list or search for the id, never guess it.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": { \"type\": \"string\", \"description\": \"File id from a previous files.list\" }\n },\n \"required\": [\"fileId\"]\n },\n \"request\": {\n \"method\": \"PATCH\",\n \"url\": \"https://www.googleapis.com/drive/v3/files/{fileId}\",\n \"builder\": \"drive-trash\"\n }\n }\n ]\n}\n",
46
- "google-sheets/SKILL.md": "---\nname: google-sheets\ndescription: Create, read, and update Google Sheets spreadsheets via the Google Sheets REST API.\ntools: [http_request, sheets_create, sheets_write_values, sheets_append_values]\nplatform: [darwin, linux, win32]\ncredentials: [google_sheets_access_token]\nallow_list:\n [https://sheets.googleapis.com/v4/spreadsheets, https://www.googleapis.com/drive/v3/files]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Sheets\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_sheets_access_token\",\n \"description\": \"Create, read, and update Google Sheets spreadsheets\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your spreadsheets and Drive files only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=sheets.googleapis.com,drive.googleapis.com) to enable the Google Sheets API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Desktop app\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_sheets_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/spreadsheets\",\n \"https://www.googleapis.com/auth/drive\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Sheets\n\nUse `sheets_create`, `sheets_write_values`, and `sheets_append_values` for\ncreating and writing cells — they set `valueInputOption` for you. Use\n`http_request` for everything else (reads, `batchUpdate`, Drive search, trash).\nFor each `http_request`, set `auth.tokenCredentialKey` to\n`google_sheets_access_token`. Call one tool per operation with one complete structured\nobject; `body` and `values` are JSON values, never serialized strings.\n\n## Load the Recipe File First\n\nThis file carries no requests. The working calls live in three reference\nfiles — load the one for the job with the `skill` tool BEFORE calling any tool,\nthen copy its call and change only the values:\n\nEach load is a real `skill` tool call — printing the call as JSON or text in\nyour reply loads nothing.\n\n- **Reading or finding spreadsheets** — \"what's in the sheet\", \"show me the\n values\", \"find / list my spreadsheets\": call the `skill` tool with\n `name: \"google-sheets\"` and `file: \"references/read.md\"`.\n- **Creating a new spreadsheet** (no existing spreadsheet involved; may fill\n it with a first table): call the `skill` tool with `name: \"google-sheets\"`\n and `file: \"references/create.md\"`.\n- **Changing an existing spreadsheet** — write or overwrite cells, append rows,\n clear a range, add a sheet tab, other structural changes, move it to the\n trash: call the `skill` tool with `name: \"google-sheets\"` and\n `file: \"references/edit.md\"`.\n\nNever write a call from memory. The recipes carry the range rules and the\nexact request shapes that fail in non-obvious ways when improvised; loading\nthe file is one cheap read-only call.\n\n## Prerequisites\n\nGoogle Sheets must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Sheets from Settings, or to\nset the `google_sheets_access_token` credential.\n\n## Identifiers and Ranges\n\nSpreadsheets are identified by `spreadsheetId` — the alphanumeric string in the\nURL `docs.google.com/spreadsheets/d/{spreadsheetId}/...`, or the `id` of a\n`sheets_create` result or Drive search hit. Listing and searching use the\nGoogle Drive API, not the Sheets API.\n\nRanges use A1 notation: `Sheet1!A1`, `Sheet1!A1:B10`, `Sheet1!A:A` (entire\ncolumn), `Sheet1!1:1` (entire row); quote sheet names with spaces:\n`'My Sheet'!A1`. The first sheet of a spreadsheet is NOT always called\n`Sheet1` — Google names it in the account's language (`Página1`, `Feuille1`,\n`Tabellenblatt1`, …). A range with no sheet name (`A1:C3`) always targets the\nfirst sheet; the recipe files say when to use it.\n\n## Output Policy\n\n- For reads, present values in a table format with headers when available.\n- For writes, confirm the range and values with the user first unless they\n already spelled them out.\n- Report success only when the response contains the `spreadsheetId` and the\n updated range, and name the spreadsheet (title or link) in the answer.\n",
46
+ "google-sheets/SKILL.md": "---\nname: google-sheets\ndescription: Create, read, and update Google Sheets spreadsheets via the Google Sheets REST API.\ntools: [http_request, sheets_create, sheets_write_values, sheets_append_values]\nplatform: [darwin, linux, win32]\ncredentials: [google_sheets_access_token]\nallow_list:\n [https://sheets.googleapis.com/v4/spreadsheets, https://www.googleapis.com/drive/v3/files]\nmetadata:\n {\n \"openclaw\": {\n \"setup\": {\n \"routes\": [\n {\n \"kind\": \"oauth\",\n \"label\": \"Google Sheets\",\n \"provider\": \"google\",\n \"credentialKey\": \"google_sheets_access_token\",\n \"description\": \"Create, read, and update Google Sheets spreadsheets\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to your spreadsheets and Drive files only.\",\n \"Each Google skill is connected separately, with its own app and its own approval.\"\n ],\n \"configSteps\": [\n \"[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=sheets.googleapis.com,drive.googleapis.com) to enable the Google Sheets API\",\n \"[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \\\"Web application\\\" as the type\",\n \"[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user\"\n ],\n \"fields\": [\n {\n \"key\": \"clientId\",\n \"label\": \"Client ID\",\n \"placeholder\": \"OAuth client ID from your app\"\n },\n {\n \"key\": \"clientSecret\",\n \"label\": \"Client Secret\",\n \"secret\": true\n }\n ],\n \"oauth\": {\n \"stateKey\": \"google_sheets_oauth\",\n \"authUrl\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"tokenUrl\": \"https://oauth2.googleapis.com/token\",\n \"tokenAuth\": \"secret-in-body\",\n \"port\": 18978,\n \"scopes\": [\n \"https://www.googleapis.com/auth/spreadsheets\",\n \"https://www.googleapis.com/auth/drive\"\n ],\n \"extraAuthParams\": {\n \"access_type\": \"offline\",\n \"prompt\": \"consent\"\n }\n }\n }\n ]\n }\n }\n }\n---\n\n# Google Sheets\n\nUse `sheets_create`, `sheets_write_values`, and `sheets_append_values` for\ncreating and writing cells — they set `valueInputOption` for you. Use\n`http_request` for everything else (reads, `batchUpdate`, Drive search, trash).\nFor each `http_request`, set `auth.tokenCredentialKey` to\n`google_sheets_access_token`. Call one tool per operation with one complete structured\nobject; `body` and `values` are JSON values, never serialized strings.\n\n## Load the Recipe File First\n\nThis file carries no requests. The working calls live in three reference\nfiles — load the one for the job with the `skill` tool BEFORE calling any tool,\nthen copy its call and change only the values:\n\nEach load is a real `skill` tool call — printing the call as JSON or text in\nyour reply loads nothing.\n\n- **Reading or finding spreadsheets** — \"what's in the sheet\", \"show me the\n values\", \"find / list my spreadsheets\": call the `skill` tool with\n `name: \"google-sheets\"` and `file: \"references/read.md\"`.\n- **Creating a new spreadsheet** (no existing spreadsheet involved; may fill\n it with a first table): call the `skill` tool with `name: \"google-sheets\"`\n and `file: \"references/create.md\"`.\n- **Changing an existing spreadsheet** — write or overwrite cells, append rows,\n clear a range, add a sheet tab, other structural changes, move it to the\n trash: call the `skill` tool with `name: \"google-sheets\"` and\n `file: \"references/edit.md\"`.\n\nNever write a call from memory. The recipes carry the range rules and the\nexact request shapes that fail in non-obvious ways when improvised; loading\nthe file is one cheap read-only call.\n\n## Prerequisites\n\nGoogle Sheets must be connected. Each Google skill is connected separately, with its\nown app and its own approval — connecting one grants nothing to the others. If\ncredentials are missing, tell the user to connect Google Sheets from Settings, or to\nset the `google_sheets_access_token` credential.\n\n## Identifiers and Ranges\n\nSpreadsheets are identified by `spreadsheetId` — the alphanumeric string in the\nURL `docs.google.com/spreadsheets/d/{spreadsheetId}/...`, or the `id` of a\n`sheets_create` result or Drive search hit. Listing and searching use the\nGoogle Drive API, not the Sheets API.\n\nRanges use A1 notation: `Sheet1!A1`, `Sheet1!A1:B10`, `Sheet1!A:A` (entire\ncolumn), `Sheet1!1:1` (entire row); quote sheet names with spaces:\n`'My Sheet'!A1`. The first sheet of a spreadsheet is NOT always called\n`Sheet1` — Google names it in the account's language (`Página1`, `Feuille1`,\n`Tabellenblatt1`, …). A range with no sheet name (`A1:C3`) always targets the\nfirst sheet; the recipe files say when to use it.\n\n## Output Policy\n\n- For reads, present values in a table format with headers when available.\n- For writes, confirm the range and values with the user first unless they\n already spelled them out.\n- Report success only when the response contains the `spreadsheetId` and the\n updated range, and name the spreadsheet (title or link) in the answer.\n",
47
47
  "google-sheets/operations.json": "{\n \"operations\": [\n {\n \"tool\": \"sheets_create\",\n \"description\": \"Create an empty Google Sheets spreadsheet. The response id is the spreadsheetId for later writes.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": { \"type\": \"string\", \"description\": \"Spreadsheet name\" }\n },\n \"required\": [\"name\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://www.googleapis.com/drive/v3/files\",\n \"builder\": \"sheets-create\"\n }\n },\n {\n \"tool\": \"sheets_write_values\",\n \"description\": \"Overwrite cells in a range with a 2D array of rows. The range must match the data dimensions or be a single top-left cell. valueInputOption defaults to USER_ENTERED (formulas and dates are parsed).\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"spreadsheetId\": { \"type\": \"string\", \"description\": \"Spreadsheet id\" },\n \"range\": {\n \"type\": \"string\",\n \"description\": \"A1 notation. Omit the sheet name to target the first sheet whatever its language (e.g. A1); name a sheet only for another tab, quoted if it has spaces (e.g. 'My Sheet'!A1)\"\n },\n \"values\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": { \"type\": [\"string\", \"number\", \"boolean\", \"null\"] }\n },\n \"description\": \"Rows of cell values\"\n },\n \"valueInputOption\": {\n \"type\": \"string\",\n \"enum\": [\"RAW\", \"USER_ENTERED\"],\n \"description\": \"RAW inserts values as-is; USER_ENTERED parses formulas and dates\"\n }\n },\n \"required\": [\"spreadsheetId\", \"range\", \"values\"]\n },\n \"request\": {\n \"method\": \"PUT\",\n \"url\": \"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/{range}\",\n \"builder\": \"sheets-write-values\"\n }\n },\n {\n \"tool\": \"sheets_append_values\",\n \"description\": \"Append rows after the last row of data in a range. valueInputOption defaults to USER_ENTERED.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"spreadsheetId\": { \"type\": \"string\", \"description\": \"Spreadsheet id\" },\n \"range\": {\n \"type\": \"string\",\n \"description\": \"A1 notation of the table's columns to append to. Omit the sheet name for the first sheet (e.g. A:C); name a sheet only for another tab (e.g. 'My Sheet'!A:C)\"\n },\n \"values\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": { \"type\": [\"string\", \"number\", \"boolean\", \"null\"] }\n },\n \"description\": \"Rows of cell values\"\n },\n \"valueInputOption\": {\n \"type\": \"string\",\n \"enum\": [\"RAW\", \"USER_ENTERED\"]\n }\n },\n \"required\": [\"spreadsheetId\", \"range\", \"values\"]\n },\n \"request\": {\n \"method\": \"POST\",\n \"url\": \"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/{range}:append\",\n \"builder\": \"sheets-append-values\"\n }\n }\n ]\n}\n",
48
48
  "google-sheets/references/create.md": "# Creating a Spreadsheet\n\nTwo tool calls: `sheets_create` makes the file, `sheets_write_values` fills\nit. Typed tools select their own credential.\n\n## Create the spreadsheet\n\n```json\n{ \"name\": \"My Spreadsheet\" }\n```\n\nCall `sheets_create` with that object. The result's `id` is the\n`spreadsheetId`. Capture it and keep using it. Create exactly once — never\nfollow a successful create with a Drive search or another create to rediscover\nthe id.\n\n## Fill it with a first table\n\nCall `sheets_write_values`. `values` is an array of rows; every row is an array\nof cells. Use the range `A1` with NO sheet name: the new file's only sheet is\nnamed in the account's language (`Sheet1`, `Página1`, `Feuille1`, …), and a\nrange without a sheet name always targets the first sheet, so the write cannot\nfail on the name.\n\n```json\n{\n \"spreadsheetId\": \"<id from sheets_create>\",\n \"range\": \"A1\",\n \"values\": [\n [\"Name\", \"Age\"],\n [\"Alice\", 30],\n [\"Bob\", 25]\n ]\n}\n```\n\n`valueInputOption` defaults to `USER_ENTERED` (formulas, numbers and dates are\nparsed like typing them in); pass `\"valueInputOption\": \"RAW\"` only to store\nevery value exactly as given. The response reports `updatedRange` and\n`updatedCells` — that is the confirmation.\n\n## Finishing\n\nAnswer with the spreadsheet name and the link\n`https://docs.google.com/spreadsheets/d/{spreadsheetId}/edit`. Do not read the\nvalues back to \"confirm\" a write that already reported `updatedCells`.\n\n## Common Mistakes\n\n- Writing to `Sheet1!A1` on a freshly created file — if the account's language\n is not English the sheet is not called `Sheet1` and the write fails with\n `Unable to parse range`. Use `A1` without a sheet name.\n- Creating through `http_request` — `sheets_create` is the supported path.\n- Passing `values` as a string, or rows that are not arrays.\n",
49
49
  "google-sheets/references/edit.md": "# Editing an Existing Spreadsheet\n\nCell writes use the typed tools `sheets_write_values` (overwrite) and\n`sheets_append_values` (add rows) — they set `valueInputOption` for you.\nEverything else (clear, structural changes, trash) is one `http_request` with\na structured object. For each `http_request`, set `auth.tokenCredentialKey` to `google_sheets_access_token`. Get the `spreadsheetId` from the URL the user gave, an earlier\n`sheets_create` result, or a Drive search (`references/read.md`).\n\nRanges: a range with no sheet name (`A1`, `A2:B10`) targets the first sheet\nwhatever its language. Name a sheet only for another tab, quoted if it has\nspaces (`'My Sheet'!A1`).\n\n## Overwrite cells\n\nCall `sheets_write_values`. The range is the top-left cell; the values block\ngrows from there. `values` is an array of rows, each an array of cells.\n\n```json\n{\n \"spreadsheetId\": \"<id>\",\n \"range\": \"A1\",\n \"values\": [\n [\"Name\", \"Age\"],\n [\"Alice\", 30],\n [\"Bob\", 25]\n ]\n}\n```\n\nTo change one cell, use that cell as the range with a single-row, single-cell\n`values` array (`\"range\": \"B2\", \"values\": [[\"100\"]]`). Pass\n`\"valueInputOption\": \"RAW\"` only to store values exactly as given; the default\n`USER_ENTERED` parses formulas, numbers and dates.\n\n## Append rows\n\nCall `sheets_append_values` with the range of the table the rows belong to\n(its columns, not the empty row you expect); the API finds the first empty row\nafter that table.\n\n```json\n{\n \"spreadsheetId\": \"<id>\",\n \"range\": \"A:B\",\n \"values\": [\n [\"Charlie\", 28],\n [\"Diana\", 32]\n ]\n}\n```\n\n## Clear a range\n\n```json\n{\n \"url\": \"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/A2:B10:clear\",\n \"auth\": { \"tokenCredentialKey\": \"google_sheets_access_token\" },\n \"method\": \"POST\",\n \"body\": {}\n}\n```\n\n## Structural changes (batchUpdate)\n\nAdding or removing sheets, resizing, formatting, and other structural\noperations use `batchUpdate`; its items are always wrapped in\n`{ \"requests\": [ … ] }`, and cell values never go through it:\n\n```json\n{\n \"url\": \"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}:batchUpdate\",\n \"auth\": { \"tokenCredentialKey\": \"google_sheets_access_token\" },\n \"method\": \"POST\",\n \"body\": {\n \"requests\": [\n {\n \"addSheet\": {\n \"properties\": { \"title\": \"New Sheet\", \"sheetType\": \"GRID\" }\n }\n }\n ]\n }\n}\n```\n\nRequests that target a sheet by `sheetId` (delete, resize, format) need the\nnumeric id from the metadata call in `references/read.md`, not the tab name.\n\n## Move a spreadsheet to the trash\n\nTrashing goes through the Drive API; the user can restore it from Drive's\ntrash. Only do this when the user explicitly asks to delete or remove the\nspreadsheet.\n\n```json\n{\n \"url\": \"https://www.googleapis.com/drive/v3/files/{spreadsheetId}\",\n \"auth\": { \"tokenCredentialKey\": \"google_sheets_access_token\" },\n \"method\": \"PATCH\",\n \"body\": { \"trashed\": true }\n}\n```\n\n## If a range is rejected\n\n`400 Unable to parse range: Sheet1!A1` means the spreadsheet has no sheet\ncalled `Sheet1` — Google names the first sheet in the account's language. Do\nNOT retry the same range: drop the sheet name, or GET the metadata\n(`references/read.md`) for the real title.\n\n## Finishing\n\nA write is confirmed by `updatedRange`/`updatedCells` (or `updates` for\nappend, `replies` for batchUpdate) in the response — do not read the values\nback to check. Name the spreadsheet in the answer.\n\n## Common Mistakes\n\n- Writing cell values with `http_request` PUT or with `batchUpdate` — use the\n typed tools.\n- Naming a sheet that does not exist (`Sheet1!…` on a non-English account) —\n leave the sheet name out for the first sheet.\n- Appending to an empty row range instead of the table's columns.\n- Passing `values` as a string, or rows that are not arrays.\n",
package/hash.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Autogenerated by scripts/build.mjs from skills/. Do not edit.
2
- export const SKILLS_HASH = 'ba65671970b03d38'
2
+ export const SKILLS_HASH = '8ce2fae4970bf369'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/skills",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Skills for the QV.AC app — the SKILL.md tree plus a content-addressed bundle of it.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -47,6 +47,21 @@ metadata:
47
47
  "scopes": [],
48
48
  "extraAuthParams": {
49
49
  "resource": "https://mcp.asana.com/v2"
50
+ },
51
+ "verify": {
52
+ "url": "https://mcp.asana.com/v2/mcp",
53
+ "method": "POST",
54
+ "body": {
55
+ "jsonrpc": "2.0",
56
+ "id": 1,
57
+ "method": "initialize",
58
+ "params": {
59
+ "protocolVersion": "2025-03-26",
60
+ "capabilities": {},
61
+ "clientInfo": { "name": "qvac", "version": "1.0.0" }
62
+ }
63
+ },
64
+ "reject": [{ "status": [401, 403], "error": "asana_not_mcp_app" }]
50
65
  }
51
66
  }
52
67
  }
@@ -2,7 +2,7 @@
2
2
  name: gmail
3
3
  description: Read, search, send, and manage Gmail messages and labels via the Gmail REST API.
4
4
  aliases: [inbox, email+send, email+draft, email+reply, email+forward]
5
- tools: [http_request, gmail_send, gmail_draft]
5
+ tools: [http_request, gmail_get, gmail_send, gmail_draft]
6
6
  platform: [darwin, linux, win32]
7
7
  credentials: [gmail_access_token]
8
8
  allow_list: [https://gmail.googleapis.com/gmail/v1/users/me/]
@@ -23,7 +23,7 @@ metadata:
23
23
  ],
24
24
  "configSteps": [
25
25
  "[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=gmail.googleapis.com) to enable the Gmail API",
26
- "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Desktop app\" as the type",
26
+ "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Web application\" as the type",
27
27
  "[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user"
28
28
  ],
29
29
  "fields": [
@@ -61,7 +61,7 @@ metadata:
61
61
 
62
62
  # Gmail
63
63
 
64
- Use `gmail_send` to send, `gmail_draft` to draft, and `http_request` for everything else (list, search, get, labels, trash). The Gmail credential is attached automatically to every `gmail.googleapis.com` request — **never include an `auth` block**.
64
+ Read with `gmail_get`, send with `gmail_send`, draft with `gmail_draft`. `http_request` is only for list/search, labels and trash. The Gmail credential is attached automatically to every `gmail.googleapis.com` request — **never include an `auth` block**. Never run code.
65
65
 
66
66
  ## Prerequisites
67
67
 
@@ -76,11 +76,13 @@ set the `gmail_access_token` credential.
76
76
 
77
77
  The host is `gmail.googleapis.com` — not `www.googleapis.com`. Send is at `/messages/send`, never `/send`.
78
78
 
79
- ## Common Operations
79
+ ## Read messages
80
80
 
81
- ### List or search messages
81
+ 1. List with `http_request`. Use `q` for Gmail search (`from:`, `subject:`, `is:unread`, `newer_than:7d`, `has:attachment`, `label:work`). Keep `maxResults` ≤ 10. The result is ids only.
82
+ 2. Call `gmail_get` with each `id`. It returns `headers` (From, To, Subject, Date), `snippet` and, with `format: "full"`, `text`.
83
+ 3. Answer from those fields. Never run code.
82
84
 
83
- `messages.list` returns `{id, threadId}` pairs only — no subjects, no snippets. To summarize you need a follow-up `messages.get` per id. Use `q` for Gmail search syntax (`from:`, `subject:`, `is:unread`, `newer_than:7d`, `has:attachment`, `label:work`). Keep `maxResults` ≤ 10.
85
+ `http_request` to list:
84
86
 
85
87
  ```json
86
88
  {
@@ -90,32 +92,15 @@ The host is `gmail.googleapis.com` — not `www.googleapis.com`. Send is at `/me
90
92
  }
91
93
  ```
92
94
 
93
- ### Get a message (metadata)
94
-
95
- For lists and summaries always use `format=metadata` — it skips the body and is far cheaper than `full`.
95
+ `gmail_get` for each id:
96
96
 
97
97
  ```json
98
- {
99
- "url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}",
100
- "method": "GET",
101
- "query": {
102
- "format": "metadata",
103
- "metadataHeaders": "Subject,From,To,Date,Message-ID,References"
104
- }
105
- }
98
+ { "id": "1a0c420faf8b68fe" }
106
99
  ```
107
100
 
108
- ### Get a message (full body)
109
-
110
- Only when the user needs the content. The body is base64url-encoded in `payload.parts[].body.data` (or `payload.body.data`); decode it before presenting. Prefer the `text/plain` part over `text/html`.
101
+ Use `format: "full"` only when the user asks what a message says.
111
102
 
112
- ```json
113
- {
114
- "url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}",
115
- "method": "GET",
116
- "query": { "format": "full" }
117
- }
118
- ```
103
+ ## Common Operations
119
104
 
120
105
  ### Send a message
121
106
 
@@ -141,7 +126,7 @@ Use `gmail_draft` with the same envelope as `gmail_send`.
141
126
 
142
127
  A reply is `gmail_send` with the original's `threadId`, `inReplyTo`, and `references`. Without these Gmail starts a new thread.
143
128
 
144
- 1. Get the original with `format=metadata` and headers `Message-ID,References,Subject,From,Reply-To`; capture its `threadId`.
129
+ 1. `gmail_get` the original; capture its `threadId` and its `Message-ID` and `References` headers.
145
130
  2. Send with `to` <- original From (or Reply-To), `subject` <- `Re: ` + original (don't double-prefix), `inReplyTo` <- original Message-ID, `references` <- original References then that Message-ID. Keep angle brackets.
146
131
 
147
132
  ```json
@@ -178,15 +163,15 @@ System labels: `INBOX`, `UNREAD`, `STARRED`, `IMPORTANT`, `SPAM`, `TRASH`. Mark
178
163
 
179
164
  ## Output Policy
180
165
 
181
- - Lists: up to 5 entries with subject, sender, and a human-readable date. Fetch full bodies only when asked.
182
- - Decode base64 message bodies before presenting; never include raw base64 blobs.
166
+ - Lists: up to 5 entries with subject, sender, and a human-readable date. Fetch `text` only when asked.
183
167
  - Modify/trash: state the user-facing effect ("marked 3 messages as read"), not the label diff.
184
168
  - Confirm destructive or outgoing actions (send, reply, trash) with the user first.
185
169
 
186
170
  ## Common Mistakes
187
171
 
188
172
  - Using `www.googleapis.com` for Gmail, or building send/draft/reply through `http_request` instead of `gmail_send`/`gmail_draft`.
189
- - Treating `messages.list` results as if they had subjects — they need a follow-up `messages.get`.
173
+ - Treating `messages.list` results as if they had subjects — call `gmail_get` for each id.
174
+ - Reading a message through `http_request` or code instead of `gmail_get`.
190
175
  - Replying without `threadId` + `inReplyTo`/`references` — Gmail starts a new thread.
191
176
  - Sending a placeholder (`<RECIPIENT_EMAIL>`, `recipient@example.com`) — the runtime refuses these.
192
177
  - Inventing an address. The runtime refuses any recipient absent from the conversation and from earlier tool results; search Gmail or ask the user.
@@ -66,6 +66,29 @@
66
66
  "url": "https://gmail.googleapis.com/gmail/v1/users/me/drafts",
67
67
  "builder": "gmail-draft"
68
68
  }
69
+ },
70
+ {
71
+ "tool": "gmail_get",
72
+ "description": "Read one Gmail message by id: sender, recipients, subject, date, snippet and, with format \"full\", the message text. Call it for each id that messages.list returned. Never read a message with http_request or code.",
73
+ "parameters": {
74
+ "type": "object",
75
+ "properties": {
76
+ "id": { "type": "string", "description": "Message id from messages.list" },
77
+ "format": {
78
+ "type": "string",
79
+ "enum": ["metadata", "full"],
80
+ "description": "metadata (default) for lists and summaries; full only when the user needs the message text"
81
+ }
82
+ },
83
+ "required": ["id"]
84
+ },
85
+ "request": {
86
+ "method": "GET",
87
+ "url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}",
88
+ "query": { "metadataHeaders": "Subject,From,To,Date,Message-ID,References" },
89
+ "builder": "gmail-get"
90
+ },
91
+ "response": { "decode": "gmail-message" }
69
92
  }
70
93
  ]
71
94
  }
@@ -23,7 +23,7 @@ metadata:
23
23
  ],
24
24
  "configSteps": [
25
25
  "[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com) to enable the Google Calendar API",
26
- "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Desktop app\" as the type",
26
+ "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Web application\" as the type",
27
27
  "[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user"
28
28
  ],
29
29
  "fields": [
@@ -22,7 +22,7 @@ metadata:
22
22
  ],
23
23
  "configSteps": [
24
24
  "[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=docs.googleapis.com,drive.googleapis.com) to enable the Google Docs API",
25
- "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Desktop app\" as the type",
25
+ "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Web application\" as the type",
26
26
  "[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user"
27
27
  ],
28
28
  "fields": [
@@ -22,7 +22,7 @@ metadata:
22
22
  ],
23
23
  "configSteps": [
24
24
  "[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=drive.googleapis.com) to enable the Google Drive API",
25
- "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Desktop app\" as the type",
25
+ "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Web application\" as the type",
26
26
  "[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user"
27
27
  ],
28
28
  "fields": [
@@ -23,7 +23,7 @@ metadata:
23
23
  ],
24
24
  "configSteps": [
25
25
  "[Open Google Cloud Console](https://console.cloud.google.com/flows/enableapi?apiid=sheets.googleapis.com,drive.googleapis.com) to enable the Google Sheets API",
26
- "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Desktop app\" as the type",
26
+ "[Open OAuth clients](https://console.cloud.google.com/auth/clients) to create an OAuth client — choose \"Web application\" as the type",
27
27
  "[Open OAuth audience](https://console.cloud.google.com/auth/audience) and add your Google account as a test user"
28
28
  ],
29
29
  "fields": [