@qvac/skills 0.1.8 → 0.1.9

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 \"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",
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://developers.asana.com/docs/integrating-with-asanas-mcp-server\",\n \"steps\": [\n \"Click Connect and approve access in the browser. Approving grants access to the Asana workspace you allow.\"\n ],\n \"configSteps\": [\n \"[Create an Asana MCP app](https://developers.asana.com/docs/integrating-with-asanas-mcp-server). External MCP is not the required option.\",\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]\nversion: 2\n# Tuned on Qwen3.5-2B with an offline eval (33 prompts, mention + prose routes, reasoning on as the app\n# runs it, 12k context, 180 s turn budget), judged by the app's own mermaid parser. QVAC-24436, QVAC-24502.\n# A run passes when the diagram asked for parses and reaches the reply with no tool JSON beside it, or,\n# for the three prompts that want a picture or a plot instead, when no diagram is drawn.\n# v1: router + references/<type>.md, harness 0.2.8. 3 repeats: mention 54/99, prose 20/36, 29 skill calls\n# per run - the 2B loops on the second load (hallucinated files, repeats) until the turn budget ends,\n# and the app shows the raw call or \"Diagram unavailable\". 71-77% of drawn blocks parsed.\n# v2: every recipe inline, nothing to load. Same harness, 3 repeats: mention 72/99, prose 31/36; 91-97%\n# of drawn blocks parse; context 4156 / 4787 tokens against 5497 / 6317. Skill calls per run 23-35: the\n# model still asks for a body it has, which @qvac/harness answers (a repeat load is acknowledged, not\n# re-run; a misspelt file resolves to its nearest match). With those fixes, 3 repeats: 76/99 and 30/36,\n# ~2 calls per run, context 4002 / 3771.\n# Tried and dropped: the per-type size budgets from the reference files (12 nodes, 8 states, ...).\n# They measured the same (79/99, 33/36 on harness main) but on a phone they cut a 40-node mind map to\n# 10 nodes: the eval scores what parses, not what was left out. The state alias for a spaced name and\n# the gantt `axisFormat` / `excludes weekends` headers from the same files stayed: one clause each.\n# This text, harness main, 3 repeats: mention 84/99, prose 28/36; 93-96% of drawn blocks parse; context\n# 3305 / 3730; ~2 skill calls per run; the ticket prompt 6 of 6.\n# Not fixed. The three route-away prompts (a logo, a photo, a revenue plot) get a diagram 6-9 times\n# in 9 on v2 against 4 in 9 on v1: a page that opens with \"draw\" draws. Four variants that reworded\n# the \"do not load\" rule, or cut the recipes to 300 words, measured 25-55% against v1's 55% and were\n# dropped.\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, pick its recipe below, write the code block, close\nits fence, 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[\"First step\"] --> B{\"Condition?\"}\n B -->|yes| C[\"Second step\"]\n B -->|no| D[\"Other step\"]\n```\n\nOne sentence about what it shows. Copy the SHAPE of this block, never its\nwords - the labels come from what the user asked about.\n\n## Pick the Type and Draw - Nothing to Load\n\nEvery recipe is here. Pick the row that fits the ask, copy its shape, draw.\nDo not call any tool: the recipe you need is already on this page.\n\n**Steps, a process, a pipeline, an org chart** - `flowchart TD` (top-down) or `flowchart LR`:\n```mermaid\nflowchart LR\n A[\"Request\"] --> B{\"Valid?\"}\n B -->|yes| C[\"Process\"]\n B -->|no| D[\"Reject with error\"]\n```\nNodes `id[\"Label\"]`, decisions `id{\"Question?\"}`, arrows always `-->` with `-->|yes|` labels. Ids never reused. Never lowercase `end` - write `\"End\"`.\n\n**Who calls whom over time** - `sequenceDiagram`:\n```mermaid\nsequenceDiagram\n participant App\n participant Server\n App->>Server: POST /login\n Server-->>App: 200 with token\n```\nDeclare every `participant` first. Call `A->>B: text`, reply `B-->>A: text`. Never `-->` here.\n\n**Modes and transitions** - `stateDiagram-v2`:\n```mermaid\nstateDiagram-v2\n [*] --> Idle\n Idle --> Running : start\n Running --> [*] : shutdown\n```\n`[*]` starts and ends. `From --> To : label`. State names are single words or `snake_case`; for a spaced name write `state \"Waiting for input\" as Waiting` once, then `Waiting`.\n\n**Tables and relations** - `erDiagram`:\n```mermaid\nerDiagram\n USER ||--o{ ORDER : \"places\"\n ORDER ||--|{ LINE_ITEM : \"contains\"\n```\nEntities UPPER_CASE. `||--o{` one-to-many, `||--||` one-to-one. Label in plain quotes after the colon.\n\n**Classes and inheritance** - `classDiagram`:\n```mermaid\nclassDiagram\n class Animal {\n +String name\n +speak()\n }\n Animal <|-- Dog\n```\nMembers inside `class Name { }`. Inheritance `Parent <|-- Child`. No quotes on class names.\n\n**A schedule with durations** - `gantt`:\n```mermaid\ngantt\n title Delivery\n dateFormat YYYY-MM-DD\n section Planning\n Requirements :a1, 2026-09-01, 5d\n Design :a2, after a1, 7d\n```\nHeaders are only `title`, `dateFormat`, `axisFormat`, `excludes weekends`, `section`. EVERY other line is `Name :id, start, duration`.\n\n**Dated events in order** - `timeline`:\n```mermaid\ntimeline\n title Company history\n 2019 : Founded\n 2021 : First release\n 2024 : Series A\n```\nOne `date : event` per line, indented under the title.\n\n**Shares of a whole** - `pie`:\n```mermaid\npie title Time spent\n \"Coding\" : 60\n \"Review\" : 25\n \"Meetings\" : 15\n```\nEvery slice is `\"Label\" : number` - quoted label, plain number, no `%`.\n\n**A brainstorm or idea tree** - `mindmap`:\n```mermaid\nmindmap\n root[Launch plan]\n Marketing\n Blog post\n Engineering\n Release build\n```\n`root[Topic]` once, then plain unquoted text, two more spaces per level. A second line at the root's depth fails.\n\nUse `gitGraph` ONLY when the user names it. Never invent a keyword not shown above.\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 a type keyword from the recipes above. Default to\n ONE block; use two only for an overview plus one 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-read the type's recipe above and\nfix by 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 recipes above |\n| `Duplicate id` | Give every node a fresh unique id |\n",
21
21
  "excel/SKILL.md": "---\nname: excel\ndescription: Create, edit, or read Excel spreadsheets (.xlsx) with openpyxl — deliver workbooks as chat attachments, or read an attached one to summarize it or answer questions in the chat. Computes values in Python; can also write live formulas and embed images. Opens in Numbers and Google Sheets too.\naliases: [xlsx, spreadsheet, workbook]\ntools: [exec(python)]\nplatform: [darwin, linux, win32]\n# Routing tuned on Qwen3.5-4B for QVAC-24106 (\"@excel embed this image into a spreadsheet\" after a\n# generate_image turn). The bullet list sent 13/14 runs to edit.md, and 6/14 ended with no image in\n# a workbook, most by asking the user to attach one. As a request -> file table: 5/5 to create.md and\n# 5/5 embedded the image; \"add a fourth row\" to a workbook built in the chat still loads edit.md, 3/3.\nmetadata:\n {\n \"openclaw\":\n {\n \"setup\":\n {\n \"summary\": \"Runs openpyxl in the in-process Python runtime, from packages that ship with the app. The first use waits for the runtime to start.\"\n }\n }\n }\n---\n\n# Excel\n\nBuild or edit an `.xlsx` workbook by running openpyxl through the `exec` tool with\n`language: \"python\"`. Declare the workbook in `outputs` and it comes back as a chat\nattachment the user can save. To answer *from* a workbook instead of building one,\nrun a read call — no `outputs` — and reply in the chat.\n\n## Load the Recipe File First\n\nThis file contains no Python. The working recipes live in three reference files —\nload the one for the job with the `skill` tool BEFORE writing any Python, then\ncopy its recipe and change the content:\n\nEach load is a real `skill` tool call — printing the call as JSON or text in\nyour reply loads nothing.\n\nPick the file by whether an `.xlsx` is already in this chat:\n\n| The request | Load |\n| ----------------------------------------------------------------------------------- | ----------------------------------------------- |\n| \"make a spreadsheet of …\", \"embed this image into a spreadsheet\" — no `.xlsx` yet | `references/create.md` |\n| \"add a row\", \"change B2\", \"add this image to my budget.xlsx\" — an `.xlsx` is here | `references/edit.md` |\n| \"what is the total in this sheet?\", \"summarize this workbook\" — the answer is a reply | `references/read.md` |\n| \"summarize this workbook into a new file\" | `references/read.md` first, then create or edit |\n\nLoad it with the `skill` tool: `name: \"excel\"` and that `file`.\n\n**An image this chat generated is already attached.** A `generate_image` result in\nany earlier turn is the image \"this image\" means, and its `attachmentId` stages it.\nNever ask the user to attach an image the conversation already has.\n\nNever write the Python from memory. The recipes carry required patterns (the\nfill-in template, staging rules, guard asserts) that fail in non-obvious ways\nwhen improvised; loading the file is one cheap read-only call.\n\n## When to Use\n\n- The user asks for a spreadsheet, workbook, `.xlsx`, Excel, or Numbers/Sheets-openable file.\n- The user attaches a spreadsheet and wants cells changed, rows/columns/sheets added or removed, or data extracted from it.\n- The user attaches a spreadsheet and asks what it holds — a summary, a question\n answered, or values pulled out into the chat.\n- The user wants a workbook that embeds an image — one generated in this chat or\n one they uploaded. Both recipe files carry it.\n\n## When NOT to Use\n\n- The user wants a table in the chat built from content already in the\n conversation — no workbook involved — write a markdown table. Answering or\n summarizing from an attached workbook **is** this skill: load\n `references/read.md`.\n- The user wants a comma-separated text file only — write the `.csv` directly with Python's `csv` module (`.csv` is an allowed output), no openpyxl needed.\n\n## Values vs Formulas — decide before writing\n\nThis runtime has no spreadsheet engine: openpyxl writes a formula as text and\ncomputes nothing. A formula cell has **no value** until the user opens the file\nin a spreadsheet app and it recalculates. So pick the mode from what the user\nwants:\n\n- **They want numbers** (a report, totals, statistics, cleaned data): compute in\n Python and write **literal values**. This is the default.\n- **They want a live spreadsheet** (totals that update when they edit cells):\n write formulas — and say so in the reply, because the formula cells look empty\n in the chat preview: \"the total is a live formula, so it shows up once you\n open the file in Excel, Numbers, or Sheets.\"\n- Never write a formula and then read it back expecting a number, and never\n \"verify\" a formula by reloading the file — there is nothing to verify.\n\nWriting both is fine: literal values everywhere, plus a `=SUM(...)` total row if\nthe user wants it to stay live.\n\n## Rules for Every Job\n\n**You build it, not the user.** Deliver the workbook, never the recipe. Do NOT\nprint the python source in chat, do NOT tell the user to install openpyxl, run\na script, or open a terminal — they have no terminal in this chat and the code\nwould not run there. The workbook exists only if an `exec` call with `outputs`\nsucceeds and returns the attachment.\n\n**Success = stop.** When `exitCode` is `0` and `attachments` lists the `.xlsx`,\nthe workbook is done — do not call `exec` again, not to \"confirm\", not to\n\"improve\", not to reload the file to \"check the formulas\". Exactly one\nsuccessful *build* call per request (a no-`outputs` read that precedes a build\ndelivers nothing and is not one of them, but it belongs before the build, never\nafter). Reply with a single line: file name + the sheet/row summary from stdout.\nIf the result has `missingOutputs`, read stderr first: an `AssertionError` there\nmeans a guard stopped the save on purpose and its message names what to fix;\nonly when stderr is clean check the `save()` name matches the declared output\nand rerun once.\n\n**Failures are fixed in the code, not around it.** If a run fails, fix the\nPython against the loaded reference file's recipes and Errors table and call\n`exec` again. If two consecutive calls fail with the same error, re-read the\ntraceback line-by-line before a third. An error is never a fault in openpyxl or\nthe runtime — keep `packages: [\"openpyxl==3.1.5\"]`, never wrap source in\n`python -c` or shell, never \"debug\" with `os.listdir` or no-op scripts.\n\n**The runtime is sealed.** No shell (`ls`, `cat` raise `SyntaxError` — the\n`command` is Python source) and no network (`requests` and `urllib` fail — a\nURL to a spreadsheet cannot be downloaded; ask the user to attach the file).\nThe working directory starts empty on every call: a file from an earlier call\nis gone unless staged again via `inputs`, and a file you write but do not\ndeclare in `outputs` is discarded.\n\n**Never overwrite a staged input.** Edits always save under a new output name.\n",
22
22
  "excel/references/create.md": "# Creating a Workbook (openpyxl)\n\nCreate a new `.xlsx` from scratch by running Python through the `exec` tool.\nA new workbook needs **no** `inputs` — do not invent attachment ids. **Exactly\none** `exec` call per user request when that call succeeds.\n\n**A workbook that already exists in this chat is never rebuilt here.** \"Add a\nrow\", \"change a cell\", \"extend the sheet\" — any request that starts from an\nexisting `.xlsx` is an EDIT: load `references/edit.md` and stage the workbook\nby its `attachmentId`. Building a fresh workbook for an edit request throws\naway everything the user already has.\n\n## The exec call\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"openpyxl==3.1.5\"],\n \"outputs\": [\"report.xlsx\"],\n \"command\": \"...\"\n}\n```\n\n- `packages` — `[\"openpyxl==3.1.5\"]` on every call. Pin the version; this exact\n version ships with the app and installs with no network; any other version\n has to be downloaded, which fails on a device that is offline. A workbook that\n embeds an image also needs `\"pillow\"`, **deliberately unpinned** — the runtime owns\n its version and `\"pillow==12.2.0\"` matches nothing, which sends openpyxl to PyPI\n with it. `[\"openpyxl==3.1.5\", \"pillow\"]` ships whole and installs offline.\n- `outputs` — `[\"report.xlsx\"]`. `wb.save(\"report.xlsx\")` must match the\n declared output name. `.xlsx` and `.csv` are allowed; `.xlsm` is not. Never\n pass an absolute path to `save()`.\n- `command` — the multi-line Python source, with real newline characters. Never\n collapse it to one line joined by `;` — a `for`/`if`/`with` after a semicolon\n is a `SyntaxError`.\n- No `inputs` key at all — **except** to embed an image, the one file a create\n call stages. See Embedding an Image.\n\n## The Recipe\n\nStart from this. It is a complete, working workbook — bold headers, data rows,\nnumber formats, column widths — saved under the declared output name. Copy it\nand change the content; do not assemble a workbook from memory.\n\n**Write the program flat — no `def`, no helper functions, no classes.** Top-level\nstatements only, in the order the sample shows. Every real failure of this skill\nhas come from a model writing a generator function and then wiring it up wrong: a\nlist filled inside a function and read outside it (`NameError: name 'data' is not\ndefined`), or a function that was never called, which saves an empty sheet and\ndelivers a blank file to the user.\n\n**Type the data out as literal rows, however long the series.** Ten years is ten\ntuples — write them. Never build periods with `datetime`/`timedelta` arithmetic\nand never increment a month by hand: `month + 1` past December raises\n`ValueError: month must be in 1..12, not 13`. A year is the plain number `2016`\nand a month is the plain string `\"2016-03\"` — neither needs a date object.\n\n**Give the granularity the user asked for.** \"Prices from 2016-2025\" is one row\nper year — ten rows. Do not expand it into 120 monthly rows they did not ask for.\n\n**Import once, then use that exact name.** After\n`from openpyxl import Workbook` the constructor is `Workbook()` — writing\n`openpyxl.Workbook()` raises `NameError: name 'openpyxl' is not defined`, because\nthat import never binds the module. Copy the sample's import block as-is.\n\n**Keep every underscore in API names.** `load_workbook`, `create_sheet`,\n`number_format`, `column_dimensions`, `column_letter`, `iter_rows`, `max_row`,\n`merge_cells` — stripping them to `loadworkbook` / `createsheet` fails.\n\n**Fill the template in — do not rewrite it.** The block below is the whole\nprogram. Change only its named slots: `SHEET_TITLE`, `HEADERS`, `ROWS`,\n`MONEY_COLUMNS`, and `OUTPUT` (which must match the declared output). Every other\nline stays character for character. In practice every failed run has come from\ncode invented *around* this template — an extra per-row styling loop, a\nhand-built summary row, a second unpack of the same data — not from the template\nitself. If a row needs a number format, put its column letter in\n`MONEY_COLUMNS`; that is the only knob.\n\nNothing goes after `print(...)`, and no comparison, `sorted`, `min`/`max` or\nrunning total goes between the lines. Values that need working out are worked out\nas you type `ROWS`, and anything the user should notice about the numbers belongs\nin your chat reply, not in more Python. Comparing a year to a label is where the\nlast run died: `TypeError: '<=' not supported between instances of 'int' and 'str'`.\n\n```python\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font\n\nSHEET_TITLE = \"Q1 Sales\"\nHEADERS = [\"Region\", \"Units\", \"Unit Price\", \"Revenue\"]\nROWS = [ # every tuple already complete, one per row\n (\"North\", 120, 9.99, 1198.80),\n (\"South\", 80, 12.50, 1000.00),\n (\"East\", 200, 7.25, 1450.00),\n]\nMONEY_COLUMNS = [\"C\", \"D\"] # money-formatted columns; [] for none\nOUTPUT = \"report.xlsx\" # must match the declared output exactly\n\nwb = Workbook()\nws = wb.active # a new workbook already has one sheet\nws.title = SHEET_TITLE\n\nws.append(HEADERS)\nfor cell in ws[1]: # ws[N] is row NUMBER N — never ws[a_list]\n cell.font = Font(bold=True)\n ws.column_dimensions[cell.column_letter].width = 14\nws.freeze_panes = \"A2\" # header stays visible while scrolling\n\nfor row in ROWS: # append whole rows — never unpack them\n ws.append(list(row))\n\nfor letter in MONEY_COLUMNS:\n for cell in ws[letter][1:]: # ws[\"C\"] is a column; [1:] skips its header\n cell.number_format = '#,##0.00'\n\nassert ws.max_row > 1, \"no data rows — fix the code, never deliver an empty sheet\"\nwb.save(OUTPUT)\nprint(f\"{len(wb.sheetnames)} sheets, {ws.max_row} rows\")\n```\n\nCompute derived columns while writing `ROWS`, not in a loop over them: the\nrevenue above is typed into each tuple. Unpacking rows to build other rows\n(`for a, b in ROWS:`) is what raises\n`ValueError: not enough values to unpack` the moment one tuple is a different\nlength.\n\nOnly when the user wants a **live** total, append this after the loop — nothing\nelse changes:\n\n```python\nlast_data_row = ws.max_row # capture BEFORE appending the total row\nws.append([\"Total\", None, None, f\"=SUM(D2:D{last_data_row})\"])\n```\n\nCapture `ws.max_row` **before** appending a row whose formula refers to the data —\n`max_row` grows with every append, so building the range afterwards produces a\nformula that includes its own cell (a circular reference the user sees as an error\nin Excel). Remember this runtime computes nothing: the formula cell looks empty\nin the chat preview, so say in the reply that the total shows up once the file\nis opened in a spreadsheet app.\n\nEvery data table gets the three lines the sample shows — bold header row,\n`ws.freeze_panes = \"A2\"`, and column widths. Without them the user opens a wall\nof `####` columns and loses the header the moment they scroll.\n\nWhen the user asks for made-up, sample, or random data, generate it so it holds\ntogether: a high is above its close, a low is below it, dates run in order,\npercentages sum to about 100. Drawing each cell independently from a random range\nputs a low of 64,000 next to a high of 21,000 in the same row, and the user reads\nthat as a broken file rather than as placeholder data.\n\nNumber formats are strings on the cell: `'#,##0.00'` for money-style decimals,\n`'0.0%'` for percentages (store the fraction, e.g. `0.31`, not `31`), `'yyyy-mm-dd'`\nfor dates.\n\n## Dates\n\nOnly when the user needs real date sorting or filtering — a year or a month label\nis a plain number or string, and needs none of this. Copy both lines together;\nthe import is the half that gets forgotten:\n\n```python\nfrom datetime import date # NOT `import datetime`, NOT `from datetime import datetime`\n\nws[\"A2\"] = date(2016, 1, 1) # a bare date(...) call — never datetime.date(...)\nws[\"A2\"].number_format = 'yyyy-mm-dd'\n```\n\n`datetime.date(2016, 1, 1)` after `from datetime import datetime` raises\n`TypeError: descriptor 'date' for 'datetime.datetime' objects doesn't apply to a\n'int' object`, and with no import at all it raises `NameError`. Both mean the same\nthing: use the two lines above exactly as written.\n\n## Several Sheets\n\nWhen the user asks for N sheets — \"a tab per region\", \"ten sheets of facts\" — write\n**one flat list of rows** and let the code group it. Each row names its own sheet as\nits first value, exactly like `ROWS` above, so there are no nested lists to author:\n\n```python\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Alignment, Font\n\nHEADERS = [\"#\", \"Fact\"] # the columns AFTER the sheet name\nWIDTHS = [6, 70] # one width per header column\nROWS = [ # (sheet, then one value per header column)\n (\"Vision\", 1, \"Parrots see ultraviolet light that humans cannot.\"),\n (\"Vision\", 2, \"Their color vision uses four cone types, not three.\"),\n (\"Vision\", 3, \"UV patterns on feathers help them choose mates.\"),\n (\"Speech\", 1, \"Parrots mimic sound with a syrinx, not vocal cords.\"),\n (\"Speech\", 2, \"African greys can attach meaning to words.\"),\n (\"Speech\", 3, \"Wild flocks carry regional dialects.\"),\n]\nOUTPUT = \"facts.xlsx\"\n\nwb = Workbook()\nspare = wb.active # removed at the end; every sheet is created below\nwritten = {}\nfor row in ROWS:\n title = row[0]\n if title in written:\n ws = wb[title]\n else:\n ws = wb.create_sheet(title)\n ws.append(HEADERS)\n for cell in ws[1]:\n cell.font = Font(bold=True)\n ws.column_dimensions[cell.column_letter].width = WIDTHS[cell.column - 1]\n ws.freeze_panes = \"A2\"\n written[title] = 0\n ws.append(list(row[1:])) # row[1:] drops the sheet name — never append row\n written[title] += len(row) - 1\n ws.cell(row=ws.max_row, column=len(HEADERS)).alignment = Alignment(wrap_text=True)\n\nwb.remove(spare)\nassert written, \"no rows were written\"\nfor title in written:\n rows_written = written[title] // len(HEADERS)\n assert rows_written >= 3, f\"sheet {title} holds {rows_written} row(s) — write at least 3\"\nwb.save(OUTPUT)\nprint(f\"{len(wb.sheetnames)} sheets: \" + \", \".join(f\"{t}={written[t]}v\" for t in written))\n```\n\n**Append `list(row[1:])`, never `row` and never `[row]`.** `row` still carries the\nsheet name, and `[row]` puts the whole tuple in one cell — that is what\n`ValueError: Cannot convert (1, 'Parrots see …') to Excel` means. The slice is the\nonly unpacking this program does; do not add `for a, b in ROWS` loops of your own,\nwhich raise `ValueError: too many values to unpack`.\n\nA formula that references a sheet whose name contains a space must quote it:\n`=SUM('Raw Data'!B1:B2)`.\n\n**Do not invent a nested shape.** A list of `(title, [rows…])` pairs is the single\nmost common way this call fails: the inner list reaches a cell and openpyxl refuses\nit. One flat list, sheet name first.\n\nThe count printed per sheet is **written values**, not rows. A row count proves\nnothing: `ws.max_row` grows when a cell is merely styled, so a sheet that got\nalignment but no values still reports four rows while holding nothing.\n\n## Embedding an Image\n\n**The image must be staged in `inputs` in the same `exec` call.** The working\ndirectory starts empty, so a file name that is not in `inputs` does not exist.\n`XLImage(\"photo.png\")` without an `inputs` entry whose `path` is `photo.png` raises\n`FileNotFoundError`, and rerunning the same command fails the same way.\n\n**`path` is a name you choose, not a name you derive.** It has nothing to do with\nthe attachment id: `photo.png` is a fine `path` for an attachment whose id is\n`9f2c4ab1…`. Two mistakes to never make:\n\n- Turning an id into a file name — `XLImage(\"9f2c4ab1….png\")`, or opening the bare\n id. An id goes in `attachmentId`, never in a path and never in the Python.\n- Opening a name that merely appeared in the conversation. A `generate_image` result\n carries a `fileName` — what the picture was called when it was made, not a file on\n disk. Take the `attachmentId` from that result and ignore the rest; nothing is\n readable until an `inputs` entry stages it under a `path` you chose.\n\n**A `generate_image` result anywhere in the conversation IS the image the user\nmeans.** \"Add this image\" points at the picture this chat already produced, in an\nearlier turn as much as this one; the user's own message carries no attachment and\ndoes not need to. Stage that id and build. Do not ask for an image the conversation\nalready has, and never generate a replacement to embed in its place.\n\nAn image the user **uploaded** has no id to copy. Stage it with `path` only and no\n`attachmentId` key; the first id-less entry is the first image of their latest\nmessage.\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"openpyxl==3.1.5\", \"pillow\"],\n \"inputs\": [{ \"attachmentId\": \"<id from the generate_image result>\", \"path\": \"photo.png\" }],\n \"outputs\": [\"report.xlsx\"],\n \"command\": \"...\"\n}\n```\n\nThe class lives in `openpyxl.drawing.image` and collides with PIL's `Image`, so\nalias it. `add_image` anchors the picture's top-left corner at one cell:\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\") # A1 is the top-left corner, not a range\n```\n\n`add_image` is a **worksheet** method — `wb.add_image(...)` raises `AttributeError`.\nThe picture floats above the grid rather than filling a cell, so leave the rows it\ncovers empty.\n\n**One `add_image` call, on one sheet.** \"Add this image\" means the workbook carries\nthe picture once — put it on the first sheet, or on a sheet of its own. Calling\n`add_image` inside the loop that builds the sheets embeds a fresh copy per sheet.\n\n**Check the call before sending it.** Every image file name in `command` must also\nbe the `path` of an `inputs` entry in that same call, and `packages` must include\n`\"pillow\"`. The reverse holds too: if `inputs` stages an image, `command` must call\n`add_image`. Never wrap `XLImage` or the import in a `try`/`except` that saves\nanyway, and never draw a substitute with PIL — the image already exists.\n\nMerged cells: after `ws.merge_cells(\"A1:C1\")` only the top-left anchor is\nwritable — `ws[\"A1\"] = \"Title\"` works, while writing `B1` or `C1` raises\n`AttributeError: 'MergedCell' object attribute 'value' is read-only`. Write the\nanchor, always.\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 (e.g. `2 sheets, 5 rows`).\n- `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was\n collapsed — restore multi-line newlines from the sample and rerun. Underscores\n in names (`load_workbook`, not `loadworkbook`) must stay. Do not switch to\n `python -c` or change the package pin.\n- `ModuleNotFoundError: No module named 'openpyxl'` means `packages` was missing\n or wrong — add `[\"openpyxl==3.1.5\"]` and rerun. Never try to install it.\n- `attachment … not found in this chat` means `inputs` listed an id that is not in\n this chat (often a copied placeholder). For a new workbook, omit `inputs`\n entirely and rerun.\n- `ValueError: Cannot convert [...] to Excel` means a list or tuple reached a cell:\n the code appended `row` or `[row]` instead of `list(row[1:])`, or invented a\n nested `(title, [rows…])` shape. Flatten it — see Several Sheets.\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. Add it\n and rerun.\n- `ModuleNotFoundError: No module named 'PIL'` means the code imported PIL itself,\n usually to draw a picture that already exists as an attachment. Stage the\n attachment through `inputs` instead.\n- `AttributeError: 'Workbook' object has no attribute 'add_image'` — `add_image` is\n a worksheet method: `ws.add_image(img, \"A1\")`.\n- `FileNotFoundError` on a 32-character hex name means an attachment id was opened\n as a path. The id belongs in `attachmentId`; open the `path` you chose.\n- A delivered workbook with no picture in it, when the user asked for one, means the\n image was never staged or `add_image` was never called. Rebuild it in one call.\n- `ImportError: cannot import name 'NumberFormat' from 'openpyxl.styles'` — there\n is no such class. A number format is a plain string on the cell:\n `cell.number_format = '#,##0.00'`. The only names to import from\n `openpyxl.styles` are `Font`, `Alignment`, `PatternFill`, `Border`, `Side`.\n- `TypeError: expected string or bytes-like object, got 'list'` means a worksheet\n was indexed with a list — `ws[row]` where `row` holds the values just appended.\n `ws[...]` takes a row number (`ws[1]`) or a range string (`ws[\"A1:C1\"]`). To\n style the row you just appended, index it by number: `ws[ws.max_row]`.\n- `NameError: name 'openpyxl' is not defined` means the code called\n `openpyxl.Workbook()` after `from openpyxl import Workbook` — call `Workbook()`.\n- `NameError` on any other name means a variable was defined inside a `def` and\n read outside it. Delete the function and inline its body at top level.\n- `ValueError: month must be in 1..12, not 13` means the code did month\n arithmetic. Type the periods out as literal values instead.\n- `AssertionError: sheet <name> holds N row(s) — write at least 3` means a sheet got\n fewer facts than the request implies. Add rows to that sheet and rerun; do not lower\n the assert. **Row counts do not prove content**: `ws.max_row` counts a cell that was\n merely styled, and is 1 on a truly empty sheet and never 0, so a header-only sheet\n can report `4 rows` while holding nothing. Append values before styling anything.\n- A bare `Sheet` among the sheet names is the untouched default sheet, left as a\n blank first tab in front of the data. Either is a failed turn even at `exitCode 0`\n — the user gets a blank or near-blank workbook. Rebuild it with literal rows.\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- `'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- A formula cell that reads `None` or shows empty in a preview is not a bug —\n this runtime computes nothing; the value appears when the user opens the file.\n Do not rewrite the workbook to \"fix\" it.\n\n## Finish\n\nWhen `exitCode` is `0` and `attachments` lists the `.xlsx`, stop tool use and\nanswer with one line: file name + the sheet/row summary from stdout. Exactly one\nsuccessful `exec` per request.\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 = '0eb3f5d497a4b6b8'
2
+ export const SKILLS_HASH = '548f8fbd37914a47'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/skills",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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",
@@ -17,12 +17,12 @@ metadata:
17
17
  "provider": "asana",
18
18
  "credentialKey": "asana_mcp_access_token",
19
19
  "description": "Tasks, projects, and comments",
20
- "helpUrl": "https://app.asana.com/0/my-apps",
20
+ "helpUrl": "https://developers.asana.com/docs/integrating-with-asanas-mcp-server",
21
21
  "steps": [
22
22
  "Click Connect and approve access in the browser. Approving grants access to the Asana workspace you allow."
23
23
  ],
24
24
  "configSteps": [
25
- "[Open the Asana developer console](https://app.asana.com/0/my-apps) and create an MCP app",
25
+ "[Create an Asana MCP app](https://developers.asana.com/docs/integrating-with-asanas-mcp-server). External MCP is not the required option.",
26
26
  "In OAuth settings, add the exact redirect URI shown below",
27
27
  "In \"Manage distribution\", allow the workspace you want this connection to access"
28
28
  ],