@qvac/skills 0.1.4 → 0.1.5
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 +2 -2
- package/hash.js +1 -1
- package/package.json +1 -1
- package/skills/spotify/SKILL.md +26 -21
- package/skills/weather/SKILL.md +39 -23
package/bundled.js
CHANGED
|
@@ -72,8 +72,8 @@ export const SKILLS = {
|
|
|
72
72
|
"presentations/references/create.md": "# Building a New Deck (python-pptx)\n\nCreate a new `.pptx` from scratch by running Python through the `exec` tool.\nA new deck needs **no** `inputs` — do not invent attachment ids. **Exactly\none** `exec` call per user request when that call succeeds.\n\n**A deck that already exists in this chat is never rebuilt here.** \"Add a\nslide\", \"change a title\", \"revise the deck\" — any request that starts from an\nexisting `.pptx` is an EDIT: load `references/edit.md` and stage the deck by\nits `attachmentId`. Building a fresh deck for an edit request throws away\nevery slide the user already has.\n\n## The exec call\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-pptx==1.0.2\"],\n \"outputs\": [\"deck.pptx\"],\n \"command\": \"...\"\n}\n```\n\n- `language` (required) — always `\"python\"`.\n- `packages` (required) — `[\"python-pptx==1.0.2\"]` on every call. Pin the\n version; an unpinned install resolves a potentially different library\n version. This exact version ships with the app and installs with no network;\n any other version has to be downloaded, which fails on a device that is\n offline.\n- `outputs` — `[\"deck.pptx\"]`. `save(\"deck.pptx\")` must match the declared\n output name exactly. A file you write but do not declare here is discarded.\n- `command` — the multi-line Python source, with real newline characters.\n Never collapse it to one line joined by `;` — a `for`/`if`/`with` after a\n semicolon is a `SyntaxError`. `command` is the program: its first line is\n the first line of Python that runs. There is no shell and no interpreter to\n invoke, and no installer — packages are declared in `packages`.\n- No `inputs` key at all for a new deck.\n- `maxOutputChars` (stdout cap, default 8192, max 65536) is never needed on a\n build call — it prints one line.\n\n## Embedding images\n\nTwo kinds of image input, told apart by where the image came from:\n\n**Tool-produced files** (`generate_image` output, a prior deck return): stage\nthem with the exact `attachmentId` from the tool result — never placeholders\nlike `att_deck`, `att_image`, or any id you made up.\n\n**Images the user uploaded** (\"use this image\", a photo attached to their\nmessage): there is no id to copy — an uploaded image shows none. Stage them\nwith `path` only and **no `attachmentId` key**; the first id-less entry is the\nfirst image of the user's latest message, the second is its second image, and\nso on — never more id-less entries than that message has images. When it has\nnone, a single id-less entry resolves to the chat's most recent image instead.\n\nSeeing the image in your context is not the same as staging it: the deck is\nbuilt by Python, which reads the working directory and never your context, so\nan uploaded image reaches a slide only through an id-less `inputs` entry. Do\nnot call `generate_image` to recreate what the user attached, and do not tell\nthem the image cannot be used — the id-less entry is how it is used.\n\n**Files the user uploaded that are not images** (a `.pptx` to revise, any\ndocument): these *do* show an id, on the `[Attached file \"…\" — attachmentId:\n…]` line of the message that carried them. Copy it verbatim into\n`attachmentId`, exactly as for a tool-produced file. The id-less form never\nreaches them. (Revising an existing deck is its own flow — load\n`references/edit.md`.)\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-pptx==1.0.2\"],\n \"inputs\": [\n { \"attachmentId\": \"<id from generate_image or prior deck>\", \"path\": \"slide1.png\" },\n { \"path\": \"uploaded.png\" }\n ],\n \"outputs\": [\"deck.pptx\"],\n \"command\": \"...\"\n}\n```\n\nStaged files land in the working directory under the bare `path` names —\nreference `slide.shapes.add_picture(\"slide1.png\", …)` by that name only.\nPaths must be unique bare filenames. The working directory is fresh on every\ncall, so a file written by an earlier call is gone unless it is staged again\nas a chat attachment; an attachment from an earlier turn can be used when its\nattachment id is available in the conversation — from a tool result or an\n`[Attached file …]` line — otherwise ask the user to attach the file again.\n\n`attachment … not found in this chat` means you invented an id or the file is\nnot attached. If the file you meant is an image the user uploaded, drop the\n`attachmentId` key; if it came from a tool result or an `[Attached file …]`\nline, re-copy the exact id; for a new deck drop `inputs` entirely; otherwise\nask the user to re-attach.\n\nIf an image was staged in `inputs`, embed it in **that** single build with\n`slide.shapes.add_picture` — never deliver a deck and then rebuild to add the\nimage. Soft-failing (`try`/`except` around the picture) and saving without it\nis a failed turn, not a success.\n\n## Image URLs do not work — never download\n\nYour Python code has **no network access**: `requests`, `urllib`, and `socket`\nall fail with a network error, and `http_request` returns truncated text, never\nimage bytes. When the user gives an image URL, do not try to fetch it from Python\nand do not retry through other tools — that is a dead end. Say the link cannot be\ndownloaded and ask the user to attach the image itself, or offer `generate_image`\nfor a similar visual. Then build the deck with the staged attachment as above.\n\nSay it in the reply, every time. A deck that quietly ships without the image the\nuser linked is a failed turn: they asked for that image, and silence reads as\nthough it is on the slide. Name the URL you could not fetch and what you need\ninstead.\n\n## Writing the Deck\n\nStart from this. It is a complete, working deck — a title slide and a bullet slide,\n16:9, every paragraph sized, saved under the declared output name. Copy it and change\nthe content; do not assemble a deck from memory.\n\n**Keep the source multi-line.** A `for`/`if`/`with` after a semicolon is a\n`SyntaxError` — paste the block with real newlines, not `stmt; for x in y: …`.\n\n**Every length is a typed length — `Inches(...)`, `Pt(...)` or `Emu(...)`, never\na bare number.** This holds for every position and size anywhere in the deck:\nboth pairs of `add_textbox(left, top, width, height)`, the `left`/`top` and the\n`width`/`height` of `add_picture(...)`, `prs.slide_width` and `prs.slide_height`,\ntable column widths and row heights, and every margin or offset.\n\npython-pptx reads a bare number as **EMU**, and there are 914400 EMU to the inch.\nSo `add_textbox(0, 0, 12, 0)` is not \"12 wide\" — it is a box 0.000013in wide and\n0in tall, pinned to the top-left corner. Nothing raises: `exitCode` is `0`, there\nis no traceback and no warning, and the deck is delivered looking broken. Write\n`add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))` instead.\n\nRecognise the symptom, because it is the only signal you get: **text crammed\ninto the top-left corner, or a box that has no size**, means a raw number reached\nan argument that required a typed length. Do not tune the numbers — wrap them.\n\n```python\nfrom pptx.util import Inches\n\nbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1.5))\n# NOT add_textbox(1, 1, 8, 2) — that is 8 EMU wide, an invisible box\n```\n\n**Keep every underscore in API names.** `text_frame`, `add_slide`, `slide_layouts`,\n`slide_width`, `word_wrap`, `add_paragraph`, `add_picture`, `add_textbox`, `PP_ALIGN`\n— stripping them to `textframe` / `addslide` / `addpicture` fails. Copy identifiers\nexactly as written below:\n\n```python\nfrom pptx import Presentation\nfrom pptx.util import Inches, Pt\nfrom pptx.enum.text import PP_ALIGN\n\nprs = Presentation()\nprs.slide_width = Inches(13.333) # 16:9 is not the default\nprs.slide_height = Inches(7.5)\n\n# Title slide — layout 0 owns a title and a subtitle\nslide = prs.slides.add_slide(prs.slide_layouts[0])\nslide.shapes.title.text = \"Why the Sky Is Blue\"\nslide.shapes.title.text_frame.paragraphs[0].font.size = Pt(44)\nsubtitle = slide.placeholders[1].text_frame\nsubtitle.text = \"Rayleigh scattering, in four points\"\nsubtitle.paragraphs[0].font.size = Pt(24)\n\n# Content slide — layout 1 owns a title and a body\nslide = prs.slides.add_slide(prs.slide_layouts[1])\nslide.shapes.title.text = \"What Happens\"\nslide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36)\ntf = slide.placeholders[1].text_frame\ntf.word_wrap = True\nfor index, point in enumerate([\n \"Sunlight arrives carrying every visible wavelength\",\n \"Air molecules scatter short wavelengths hardest\",\n \"Blue scatters far more than red\",\n \"So the daytime sky reads blue in every direction\",\n]):\n p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()\n p.text = point\n p.font.size = Pt(20)\n\nprs.save(\"deck.pptx\") # must match the declared output exactly\nprint(f\"{len(prs.slides)} slides\")\n```\n\nLayouts `0` and `1` own the placeholders that example writes to.\n\n**Choosing a layout has one rule, and it depends on where the deck came from:**\n\n- **Adding to a deck the user gave you** — reuse the layout its own slides\n already use: read a comparable existing slide and pass its `.slide_layout` to\n `add_slide`. That is the only way the new slide inherits the deck's theme.\n That flow is `references/edit.md` — load it.\n- **Building a new deck** — index the bundled template, which commonly uses `0`\n (title), `1` (title + content), `5` (title only), and `6` (blank). Those\n indices belong to *that* template and mean nothing on an uploaded deck.\n\nA layout only owns the placeholders it declares, and python-pptx returns `None` for\nthe rest — on the blank layout `shapes.title` is `None`, so `shapes.title.text = …`\nraises `AttributeError: 'NoneType' object has no attribute 'text'`, and\n`placeholders[1]` raises `KeyError`. So each slide is one of exactly two kinds, never\na mix:\n\n| slide kind | layout | how you write text |\n| --- | --- | --- |\n| title / title + body | the deck's own layout, or `0`, `1`, `5` in a new deck | `shapes.title`, `placeholders[1]` |\n| hand-designed | `6` (blank) | `shapes.add_textbox(...)` for **every** box, title included |\n\nOn layout `6` there is no title to reach for — the title is a text box you add.\n\nThe blank layout is **not** a co-equal way to write a content slide. It is for a\nslide you are genuinely designing by hand — a full-bleed image, a diagram, a\ncustom split — and it inherits no font, size, colour or position from the\ntemplate. Using it plus `add_textbox` to hold ordinary title-and-bullets content\non a deck the user uploaded produces a slide that visibly does not belong: wrong\ntypeface, wrong sizes, wrong margins, and no bullets. Placeholders exist so you\ndo not have to reproduce a theme you cannot see.\n\nBullets — set `tf.text` for the first bullet, then `add_paragraph()` for the rest.\nUsing `add_paragraph()` for the first one leaves a blank leading line.\n\nA new text frame holds exactly **one** paragraph, and `tf.paragraphs` is a tuple, so\n`tf.paragraphs[1]` raises `IndexError: tuple index out of range` until you have added\nit. Grow the frame with `p = tf.add_paragraph()`, which returns the new paragraph, and\nwrite through that. A paragraph owns `.text`, `.font` and `.alignment` and nothing\nelse — it has no `.paragraphs` and no `.add_paragraph()`, so never reassign your frame\nvariable to a paragraph.\n\nThe template's body placeholder inherits 28pt, so size every paragraph you add —\nincluding sub-levels — or it renders far larger than intended:\n\n```python\nfrom pptx.util import Pt\n\nslide = prs.slides.add_slide(prs.slide_layouts[1])\nslide.shapes.title.text = \"Agenda\"\ntf = slide.placeholders[1].text_frame\ntf.word_wrap = True\nfor index, point in enumerate([\"first point\", \"second point\"]):\n p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()\n p.text = point\n p.font.size = Pt(20) # an unsized paragraph inherits 28pt\n```\n\nEvery deck returned through `outputs` is fitted before it reaches the user, so text\nthat would overflow its box is shrunk to fit automatically. That is a safety net, not\na licence to overfill: shrinking below about 16pt is unreadable from a room. Budget\neach slide at no more than five bullets of about 100 characters. A sixth bullet is a\nsecond slide titled `... (cont.)`, never a smaller font — and prose belongs in the\nchat reply, not on a slide.\n\nFree text — the only way to add text outside a placeholder is\n`shapes.add_textbox(left, top, width, height)` — all four are required, and all\nfour are typed lengths, never bare numbers — then write into its `.text_frame`.\nThere is no `add_text_frame`, no `add_text`, and no `add_paragraph` on `shapes`:\n\n```python\nslide = prs.slides.add_slide(prs.slide_layouts[6])\nbox = slide.shapes.add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))\ntf = box.text_frame\ntf.word_wrap = True\ntf.text = \"Why the sky is blue\"\ntf.paragraphs[0].font.size = Pt(40) # from pptx.util import Pt\n```\n\nText always lives on the `.text_frame`, never on the shape: `box.paragraphs`,\n`box.add_paragraph()`, `box.word_wrap`, and `box.font` all raise AttributeError. Go\nthrough `tf = box.text_frame` first — `tf.text`, `tf.paragraphs[0]`,\n`tf.add_paragraph()`, `tf.word_wrap`. `shape.text` is the one shortcut that reads\nthrough to the frame; there is no matching `shape.font`.\n\nFormatting lives one level lower still — on a paragraph or a run, never on a shape or\na frame. A title is sized through its paragraph:\n\n```python\ntitle = slide.shapes.title\ntitle.text = \"Why the Sky is Blue\"\ntitle.text_frame.paragraphs[0].font.size = Pt(44) # not title.font.size\ntitle.text_frame.paragraphs[0].font.bold = True\ntitle.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER\n```\n\nSlides are added with `prs.slides.add_slide(layout)` — `prs.add_slide` does not\nexist. Alignment comes from an enum import, not an attribute path:\n\n```python\nfrom pptx.enum.text import PP_ALIGN\n\ntf.paragraphs[0].alignment = PP_ALIGN.CENTER\n```\n\nBullet characters are not needed — a placeholder body renders bullets itself. Give\neach bullet its own paragraph; never pack several `\\n`-joined bullets into one.\nNever type the marker into the text: `\"1. \"`, `\"2. \"`, `\"- \"` and `\"• \"` prefixes\nrender *next to* the bullet the placeholder already draws, in the wrong font.\nBullets belong in a body placeholder for exactly this reason — a bare\n`add_textbox` draws none, and typing them by hand to compensate is the wrong fix.\nPut the content in a placeholder instead.\n\nFont color — the type is `RGBColor` with **RGB in all caps**. Not `RgbColor`,\n`rgbColor`, or `rgb_color`:\n\n```python\nfrom pptx.dml.color import RGBColor\n\ntitle.text_frame.paragraphs[0].font.color.rgb = RGBColor(0x1A, 0x73, 0xE8)\n```\n\nBackgrounds and fills — `background` hangs off the slide itself, never off\n`slide.shapes` or a shape, and the fill is set in two steps: `solid()` first,\nthen the color:\n\n```python\nfrom pptx.dml.color import RGBColor\n\nfill = slide.background.fill # slide.shapes has no background\nfill.solid()\nfill.fore_color.rgb = RGBColor(0x0B, 0x1F, 0x3A)\n\nbox.fill.solid() # a shape is tinted through its own .fill\nbox.fill.fore_color.rgb = RGBColor(0xF2, 0xF2, 0xF2)\n```\n\nA paragraph has no `.fill` at all — coloring text goes through the font,\n`paragraph.font.color.rgb = RGBColor(...)`, as above. `fill` exists on a shape\nand on `slide.background`, nowhere else you will need.\n\nImages — go through the shapes collection: `slide.shapes.add_picture(...)`. A\n`Slide` has no picture or text-box methods of its own — `slide.add_picture`,\n`slide.addpicture`, `slide.add_textbox`, and `slide.addtextbox` all raise\n`AttributeError: 'Slide' object has no attribute '…'`. Fix: put `.shapes` between\n`slide` and the method. Pass only one of `width`/`height`; passing both distorts\nthe picture. Generate image-slide visuals at 1024×512 so they fill the content box;\na 512×512 square letterboxes with wide empty bands either side.\n\n**Do not soft-fail images or imports.** Never wrap `add_picture` or color imports in\n`try`/`except` that prints a warning and continues. A missing file or\n`cannot import name 'RgbColor'` must raise so you fix it and rerun — a deck that\nsaves without the requested image is a failed turn, not a success.\n\n```python\nslide = prs.slides.add_slide(prs.slide_layouts[6])\n# correct: slide.shapes.add_picture — never slide.add_picture / slide.addpicture\nslide.shapes.add_picture(\"slide1.png\", Inches(0.75), Inches(1.0), width=Inches(11.83))\nprs.save(\"deck.pptx\")\nprint(f\"{len(prs.slides)} slides\")\n```\n\n## Errors\n\n- Never print the deck's bytes or base64 — stdout is capped (8 KB by default)\n and the file travels through `outputs`. A build call prints only the slide\n count line (e.g. `7 slides`). No \"Presentation created successfully\", no\n try/except warnings on stdout.\n- `cannot import name 'RgbColor' from 'pptx.dml.color'` means the name is wrong —\n use `RGBColor` (all-caps RGB). Do not catch the ImportError and save anyway.\n- Never pass an absolute path to `save()`.\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 (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`\n or change the package pin.\n- `outputs declare a .pptx but command does not build one` means the source never\n calls `Presentation(...).save(...)` — paste the skill sample (edited for content),\n not a diagnostic `os.listdir` or shell wrapper.\n- `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or\n wrong — add `[\"python-pptx==1.0.2\"]` 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 like `att_deck`). For a new deck, omit\n `inputs` entirely and rerun. Only stage real ids from prior tool results.\n- Text crammed into the top-left corner, or a shape with no visible size, means a\n bare number reached an argument that required a typed length — python-pptx reads\n it as EMU (914400 to the inch), so `add_textbox(0, 0, 12, 0)` is an invisible box\n in the corner. Nothing raises and `exitCode` is `0`, so this only ever shows up in\n the delivered deck. Wrap every position and size in `Inches(...)` / `Pt(...)` /\n `Emu(...)` and rerun — do not tune the raw numbers.\n- On an `AttributeError` from python-pptx 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 shell.\n- `AttributeError: 'Slide' object has no attribute 'add_picture'` (or `addpicture`,\n `add_textbox`, `addtextbox`) means the call skipped `.shapes` — use\n `slide.shapes.add_picture(...)` / `slide.shapes.add_textbox(...)`, never\n `slide.add_*`.\n- `AttributeError: 'SlideShapes' object has no attribute 'background'` means the\n background was reached through the shapes collection — it lives on the slide:\n `slide.background.fill.solid()` then `fill.fore_color.rgb = RGBColor(...)`.\n- `AttributeError: '_Paragraph' object has no attribute 'fill'` means a fill was\n asked of text — paragraphs have none. Color text with\n `paragraph.font.color.rgb = RGBColor(...)`; `.fill` belongs to a shape or to\n `slide.background`.\n\n## Finish\n\nWhen `exitCode` is `0` and `attachments` lists the `.pptx`, stop tool use and\nanswer with one line: file name + the slide count from stdout. Exactly one\nsuccessful build `exec` per request — re-running the same build is spam, not\nquality.\n",
|
|
73
73
|
"presentations/references/edit.md": "# Editing an Existing Deck (python-pptx)\n\nEditing means opening the deck that already exists and changing only what the\nuser asked for.\n\n**The first line of an edit is always `Presentation(\"<staged path>\")`.** A bare\n`Presentation()` is only ever for a brand-new deck — it opens the bundled blank\ntemplate, not the user's file, so retyping the slides regenerates their text and\nthrows away the original content and design. A rebuilt deck is a failed turn.\nStage the deck as an input by its real `attachmentId` and open **that staged\nfile**. If no attachment id for the deck is available, ask the user to attach it\nagain.\n\n## Staging the deck\n\nThe id comes from wherever the deck entered the chat: the `exec` result that\ndelivered it, or — for a deck the **user uploaded** — the `[Attached file …]`\nline on their message, which names every non-image upload:\n\n```\n[Attached file \"quarterly.pptx\" (application/vnd.openxmlformats-officedocument.presentationml.presentation) — attachmentId: 4f9c2ab1]\n```\n\nCopy that id verbatim — never placeholders like `att_deck` or any id you made\nup. A `.pptx` is never staged id-less: the id-less form resolves to an uploaded\n*image*, so it cannot reach a deck. An attachment from an earlier turn can be\nused when its attachment id is available in the conversation — from a tool\nresult or an `[Attached file …]` line. Otherwise, ask the user to attach the\nfile again.\n\n## Read first, then edit\n\n**An edit that writes new prose is two `exec` calls, in this order:** a **read**\nthat stages the deck, prints what is on the slides and declares **no `outputs`**;\nthen the **edit** that stages the same deck, makes the change and declares the\noutput.\n\nThe read has to be its own call, because one call cannot inform itself. The words\nyou put on a new slide are in the source you submit — fixed before the program\nruns — so a `print` in that same program reports the deck back to you only after\nthe slide was already written and saved. A single call can still *compute*\nagainst the deck (`prs.slides[1].slide_layout`, `len(prs.slides)`), because that\nis code the runtime evaluates against the real file. What it cannot do is let you\n**write** from what the deck says.\n\nA mechanical edit needs no read: a font size, a colour, a slide whose text the\nuser already gave you. Read first when the new text has to agree with the deck —\na conclusion, a summary, a \"what changed\" slide — and go straight to the edit\nwhen it does not.\n\nStill forbidden, and unchanged: an `exec` opened *after* the deck is delivered to\ncheck what you sent. The result you already hold is the whole account of that\nrun — that is the loop `Success = stop` closes.\n\nThe read call declares **no `outputs`** — it builds nothing, it only reports:\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-pptx==1.0.2\"],\n \"inputs\": [{ \"attachmentId\": \"<id of the deck in this chat>\", \"path\": \"deck.pptx\" }],\n \"maxOutputChars\": 24000,\n \"command\": \"...\"\n}\n```\n\n`maxOutputChars` raises the stdout cap so the whole deck comes back in one\nresult — without it stdout is capped at 8 KB. Keep the sample's 24000 (the cap's\nmaximum is 65536). A build call prints one line and never needs it.\n\nThe edit call is the one that builds, and there is **exactly one** of those:\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-pptx==1.0.2\"],\n \"inputs\": [{ \"attachmentId\": \"<id of the deck in this chat>\", \"path\": \"deck.pptx\" }],\n \"outputs\": [\"deck-v2.pptx\"],\n \"command\": \"...\"\n}\n```\n\n- `packages` — pin exactly `python-pptx==1.0.2` on every call; an unpinned\n install resolves a potentially different library version. This exact version\n ships with the app and installs with no network; any other version has to be\n downloaded, which fails on a device that is offline.\n- `inputs` — the staged deck. Paths must be unique bare filenames; staged files\n land in the working directory under those names — reference\n `Presentation(\"deck.pptx\")` by that name only.\n- `outputs` — the file to deliver. A file you write but do not declare here is\n discarded. Omit on a read call — a read builds nothing.\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\nBoth stage the same deck by the same `attachmentId`: the working directory is\nfresh on every call, so the read leaves nothing behind for the edit to reuse.\n\nName the output after the deck you opened: keep its stem and bump a version —\n`deck.pptx` → `deck-v2.pptx`, and an edit of that one → `deck-v3.pptx`. The\nshared stem reads as one document's history in the chat, and the new name leaves\nthe version you opened still openable. Never overwrite the staged input.\n\n## The read call\n\nThe read call's whole program is the loop — layout name and every line, so the\nedit that follows can be written against real content:\n\n```python\nfrom pptx import Presentation\n\nprs = Presentation(\"deck.pptx\") # the staged input — never Presentation()\nfor index, slide in enumerate(prs.slides):\n lines = [s.text_frame.text.replace(\"\\n\", \" \") for s in slide.shapes if s.has_text_frame]\n print(f\"{index}: [{slide.slide_layout.name}] {' | '.join(lines)}\")\n```\n\nThe `replace` is load-bearing: a multi-paragraph body embeds `\"\\n\"` between its\nbullets, and an embedded newline would split one slide across several printed\nlines. Flattened, every printed line is exactly one slide, starting with its\nindex and layout name.\n\nIt saves nothing and declares no `outputs`. Read its result before writing the\nedit: the layout names decide which layout the new slide copies, and the lines\ndecide what it can truthfully say.\n\n**Read the deck before you write into it.** New content has to agree with what\nis already on the slides, and you cannot write a conclusion, a summary, or a\n\"what changed\" slide from the titles alone — the titles are headings, and the\nsubstance is in the bodies underneath them. Loop every slide and print every\nshape with `shape.has_text_frame` in the **read** call, then write the edit\nagainst what came back. A slide written from titles only reads as though it\nbelongs to a different deck: it restates the headings, invents specifics the\ndeck never claimed, and contradicts the bullets it is supposed to close.\n\nRead through `slide.shapes` **only**. `slide.placeholders` is not a second place\nto look — every placeholder is already in `slide.shapes`, the same shape reached\nby a narrower door, so looping both prints the whole deck twice and doubles what\nyou have to read back. Nor can you dedupe your way out of it: python-pptx builds\na fresh wrapper on each access, so the title reached through `shapes` and the\ntitle reached through `placeholders` are `==`-distinct objects over one XML\nelement — `in`, `is` and `set()` all fail to spot the repeat. One loop over\n`slide.shapes`, guarded by `shape.has_text_frame`, is the whole read.\n\nPrint `slide.slide_layout.name`, never the layout object — `print(slide.slide_layout)`\ngives `<pptx.slide.SlideLayout object at 0x…>`, which tells you nothing and leaves\nthe layout choice to guesswork. The name is the template's own label, like\n`Title Slide`, `Title and Content`, or `Section Header`.\n\nThat same read locates the slide to change: match on the text you printed, and\nedit through the shape you matched. python-pptx has no API to delete or reorder\nslides — say so instead of hacking at the XML.\n\n## The edit call\n\nThe edit call then opens the same staged file, changes it in place, and saves\nunder the versioned name — never over the staged input:\n\n```python\nfrom pptx import Presentation\nfrom pptx.util import Pt\nfrom pptx.dml.color import RGBColor\n\nprs = Presentation(\"deck.pptx\") # the staged input — never Presentation()\nbefore = len(prs.slides)\n\n# Retitle the first slide in place — every other shape keeps its text\ntitle = prs.slides[0].shapes.title\ntitle.text = \"Why the Sky Is Blue — Revised\"\ntitle.text_frame.paragraphs[0].font.size = Pt(44)\n\n# Recolor existing text through its paragraph font\nfirst_body = prs.slides[1].placeholders[1].text_frame\nfirst_body.paragraphs[0].font.color.rgb = RGBColor(0x1A, 0x73, 0xE8)\n\n# One new slide, on the layout a comparable BODY slide uses — never the cover's\nmodel = prs.slides[1] # a content slide; slide 0 is usually the cover\nslide = prs.slides.add_slide(model.slide_layout)\nslide.shapes.title.text = \"What Changed\"\ntf = slide.placeholders[1].text_frame\ntf.word_wrap = True\ntf.text = \"One new closing slide, nothing else touched\"\ntf.paragraphs[0].font.size = Pt(20)\n\nprs.save(\"deck-v2.pptx\") # the declared output — not deck.pptx\nprint(f\"{before} -> {len(prs.slides)} slides\")\n```\n\n**Reuse the deck's own layout — never the blank one.** `prs.slide_layouts[…]`\nindexes the *template's* layout list, and on an uploaded deck those indices mean\nwhatever that template says; a slide's own `.slide_layout` is the layout it is\nalready built on, so passing that to `add_slide` gives the new slide the same\nplaceholders, fonts, colours and positions as its neighbours. Choose the index\nby reading the deck — pick the existing slide that most resembles the one you\nare adding, a content slide for a content slide — and fill the placeholders it\nhands you. The index above is that choice, not a constant: a one-slide deck has\nonly `prs.slides[0]`, and `prs.slides[1]` raises `IndexError`.\n\n**Slide 0 is almost always the cover**, on a `Title Slide` layout that owns a big\ncentred title and a subtitle and nothing else. Copying *that* layout for a\nconclusion produces a second cover page in the middle of the deck — placeholders\nthat fit one line, no bullet body, and title styling that shouts. Take the layout\nfrom a slide that carries real content — typically `Title and Content` — and reach\nfor the cover's layout only when you are genuinely adding another cover. Reaching\nfor `slide_layouts[6]` (blank) and hand-placing text boxes on a themed deck\ninherits none of the theme and **guarantees a visual mismatch** with the slides\nbeside it.\n\n**Every length is a typed length — `Inches(...)`, `Pt(...)` or `Emu(...)`, never\na bare number** — for every `add_textbox(left, top, width, height)` argument,\nevery `add_picture(...)` position and size, and every margin or offset.\npython-pptx reads a bare number as EMU (914400 to the inch), nothing raises, and\nthe deck is delivered with text crammed into the top-left corner or a box that\nhas no size. Do not tune the numbers — wrap them:\n\n```python\nfrom pptx.util import Inches\n\nbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1.5))\n# NOT add_textbox(1, 1, 8, 2) — that is 8 EMU wide, an invisible box\n```\n\nSize every paragraph you add (`p.font.size = Pt(20)`, as in the sample) — the\ntemplate's body placeholder inherits 28pt, so an unsized paragraph renders far\nlarger than intended. Font color goes through `RGBColor` with **RGB in all\ncaps** (never `RgbColor`), on a paragraph's font — a paragraph has no `.fill`.\nFor the full slide-authoring API — placeholders vs. text boxes, bullets,\nformatting, backgrounds, pictures — load `references/create.md`.\n\nIf an image was staged in `inputs`, embed it in **that** single build with\n`slide.shapes.add_picture` — never deliver a deck and then rebuild to add the\nimage. Soft-failing (`try`/`except` around the picture) and saving without it\nis a failed turn, not a success.\n\n## Verify the slide count\n\n**Verify an added slide by the slide count.** Print `before` and `after` as the\nsample does, then read the number back: the delta has to be exactly what the user\nasked for — one added slide is `2 -> 3 slides`, and `2 -> 4 slides` means the\nslide got appended twice. A delta that does not match the request is a **failed\nturn to diagnose, not a result to report**: find the second `add_slide` (or the\none that never ran) and rerun. Note the count can only ever grow, since there is\nno API to delete a slide. This check is about slides you add — an edit that only\nchanges text on existing slides leaves the count flat, and that is correct.\n\nThat check lives inside the build, so it takes no extra call: the counts come\nfrom one `print` in the same `exec` that does the edit. A delivered deck is still\nnever reopened to \"verify\" it.\n\n**This is the one exception to `Success = stop`, and it is not a second call.**\nWhen the edit added slides, `exitCode: 0` plus an attachment cannot tell the\nedit you were asked for apart from one that fired twice or not at all — every\none of those produces both. Read the before/after count printed by that same\nrun before you reply. The fix is a corrected build, never an `exec` opened to\ninspect what was already delivered.\n\nThat corrected build goes out under the **next** version: `deck-v3.pptx` after a\nbroken `deck-v2.pptx`. The name you already delivered is refused on a second\nattach — `\"deck-v2.pptx\" was already attached this turn` — so reusing it turns a\nrecoverable turn into a dead one. The broken version stays in the chat either\nway, so name the good file in your reply.\n\n## Errors\n\n- `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or\n wrong — add `[\"python-pptx==1.0.2\"]` and rerun. Never try to install it.\n- `attachment … not found in this chat` means `inputs` listed an id that is not\n in this chat (often a copied placeholder like `att_deck`). Re-copy the exact\n id from the tool result or the `[Attached file …]` line that names the deck;\n if neither exists, ask the user to attach it again instead of retrying.\n- Every line showing up twice in the read output means the loop walked\n `slide.shapes` *and* `slide.placeholders`. Placeholders are already shapes, so\n the second pass re-reads what the first one found. Drop it — one loop over\n `slide.shapes` is the whole read.\n- A read result ending in `… [truncated]` means the deck outgrew the cap:\n answer from what came back and say the answer covers the deck up to that\n point. Do not rerun the read — it prints the same beginning again.\n- A slide count whose delta does not match the request (`2 -> 4 slides` when one\n slide was asked for) means `add_slide` ran twice, even at `exitCode 0`. Read the\n printed before/after count before replying — see Verify the slide count.\n- A new slide that does not match the deck around it — different font, size or\n colour, bullets missing — was added on the blank layout instead of the deck's\n own. Read a comparable existing slide, pass its `.slide_layout` to `add_slide`,\n and fill its placeholders.\n- Text crammed into the top-left corner, or a shape with no visible size, means a\n bare number reached an argument that required a typed length — wrap every\n position and size in `Inches(...)` / `Pt(...)` / `Emu(...)` and rerun.\n- `cannot import name 'RgbColor' from 'pptx.dml.color'` means the name is wrong —\n use `RGBColor` (all-caps RGB). Do not catch the ImportError and save anyway.\n- Never pass an absolute path to `save()`.\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 (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`\n or change the package pin.\n- `outputs declare a .pptx but command does not build one` means the source never\n calls `Presentation(...).save(...)` — paste the skill sample (edited for content),\n not a diagnostic `os.listdir` or shell wrapper.\n- On an `AttributeError` from python-pptx 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 shell.\n- `AttributeError: 'Slide' object has no attribute 'add_picture'` (or `addpicture`,\n `add_textbox`, `addtextbox`) means the call skipped `.shapes` — use\n `slide.shapes.add_picture(...)` / `slide.shapes.add_textbox(...)`, never\n `slide.add_*`.\n- Never print the deck's bytes or base64 — stdout is capped and the file travels\n through `outputs`. A build call prints only the before/after slide count line;\n only a read call prints slide text.\n\n## Finish\n\nWhen `exitCode` is `0`, `attachments` lists the `.pptx`, and the printed\nbefore/after count matches the request, stop tool use and answer with one line:\nfile name + slide count from stdout. Exactly one successful build `exec` per\nrequest — the no-`outputs` read attaches nothing and is not that call.\n",
|
|
74
74
|
"presentations/references/read.md": "# Reading a Deck to Answer in Chat (python-pptx)\n\nWhen the user asks what an attached `.pptx` *says* — a summary, a question\nanswered, specific content pulled out — the deliverable is your reply in the\nchat, not a file. This is a **read request**: one no-`outputs` read call,\nstaged by the deck's real `attachmentId`, is the only `exec` of the turn — no\nbuild call follows it.\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 slides; the summarizing happens in your reply, after the\nresult comes back.\n\n## Staging the deck\n\nThe id comes from wherever the deck entered the chat: the `exec` result that\ndelivered it, or — for a deck the **user uploaded** — the `[Attached file …]`\nline on their message, which names every non-image upload:\n\n```\n[Attached file \"quarterly.pptx\" (application/vnd.openxmlformats-officedocument.presentationml.presentation) — attachmentId: 4f9c2ab1]\n```\n\nCopy that id verbatim — never placeholders like `att_deck` or any id you made\nup. A `.pptx` is never staged id-less: the id-less form resolves to an uploaded\n*image*, so it cannot reach a deck. If no attachment id for the deck is\navailable anywhere in the chat, ask the user to attach it again — the working\ndirectory is fresh on every call, so a file from an earlier call is gone unless\nstaged again by its id.\n\n## The exec call\n\nThe read call declares **no `outputs`** — it builds nothing, it only reports:\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-pptx==1.0.2\"],\n \"inputs\": [{ \"attachmentId\": \"<id of the deck in this chat>\", \"path\": \"deck.pptx\" }],\n \"maxOutputChars\": 24000,\n \"command\": \"...\"\n}\n```\n\n- `packages` — pin exactly `python-pptx==1.0.2`; this exact version ships with\n the app and installs with no network; any other version has to be downloaded,\n which fails on a device that is offline.\n- `inputs` — the staged deck, under a unique bare filename; reference\n `Presentation(\"deck.pptx\")` by that name only.\n- `maxOutputChars` — raises the stdout cap so the whole deck comes back in one\n result — without it stdout is capped at 8 KB. Keep the sample's 24000\n (default 8192, max 65536). Only a read call prints slide text.\n- `command` — the multi-line Python source, with real newline characters.\n Never collapse it to one line joined by `;`.\n\n## The recipe\n\nThe read call's whole program is the loop — layout name and every line:\n\n```python\nfrom pptx import Presentation\n\nprs = Presentation(\"deck.pptx\") # the staged input — never Presentation()\nfor index, slide in enumerate(prs.slides):\n lines = [s.text_frame.text.replace(\"\\n\", \" \") for s in slide.shapes if s.has_text_frame]\n print(f\"{index}: [{slide.slide_layout.name}] {' | '.join(lines)}\")\n```\n\nThe `replace` is load-bearing: a multi-paragraph body embeds `\"\\n\"` between its\nbullets, and an embedded newline would split one slide across several printed\nlines. Flattened, every printed line is exactly one slide, starting with its\nindex and layout name.\n\nA bare `Presentation()` opens the bundled blank template, not the user's file —\nthe first line is always the staged path. Loop every slide and print every\nshape guarded by `shape.has_text_frame`: the titles are headings, and the\nsubstance is in the bodies underneath them.\n\nRead through `slide.shapes` **only**. `slide.placeholders` is not a second place\nto look — every placeholder is already in `slide.shapes`, the same shape reached\nby a narrower door, so looping both prints the whole deck twice and doubles what\nyou have to read back. Nor can you dedupe your way out of it: python-pptx builds\na fresh wrapper on each access, so the title reached through `shapes` and the\ntitle reached through `placeholders` are `==`-distinct objects over one XML\nelement — `in`, `is` and `set()` all fail to spot the repeat. One loop over\n`slide.shapes`, guarded by `shape.has_text_frame`, is the whole read.\n\n## Finish: answer in the chat\n\n**A successful read ends tool use.** When the result prints the slides, reply\nwith the summary or the answer as chat text. **Scale the reply to the deck**: a\nsummary is much shorter than what it summarizes — a handful of slides earns\nthree to five sentences, and only a long deck earns sections. Restating every\nslide is not a summary. Do **not**:\n\n- call `exec` again to \"re-check\", \"read more\", or read the same deck a second\n time;\n- build a summary `.pptx` the user never asked for — an unrequested file is a\n failed turn, not a bonus.\n\nIf stdout ends with `… [truncated]`, the deck is longer than the cap: answer\nfrom what came back and say the answer covers the deck up to that point. Do not\nrerun 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 first, then one build call that writes the new deck from\nthe slides you actually read. The build call follows `references/create.md` —\nload it; the read still declares no `outputs`.\n\n## Errors\n\n- `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or\n wrong — add `[\"python-pptx==1.0.2\"]` and rerun. Never try to install it.\n- `attachment … not found in this chat` means `inputs` listed an id that is not\n in this chat (often a copied placeholder like `att_deck`). Re-copy the exact\n id from the tool result or the `[Attached file …]` line that names the deck;\n if neither exists, ask the user to attach it again instead of retrying.\n- Every line showing up twice in the read output means the loop walked\n `slide.shapes` *and* `slide.placeholders`. Placeholders are already shapes —\n drop the second loop; one loop over `slide.shapes` is the whole read.\n- A read result ending in `… [truncated]` means the deck outgrew the cap:\n answer from what came back — do not rerun the read.\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 (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`\n or change the package pin.\n",
|
|
75
|
-
"spotify/SKILL.md": "---\nname: spotify\ndescription: Play, search, and control music on Spotify — songs, artists, albums, playlists, and playback.\nemoji: 🎵\ntools: [http_request]\nplatform: [darwin, linux, win32, ios, android]\ncredentials: [spotify_access_token]\nallow_list: [https://api.spotify.com/v1/]\nmetadata:\n {\n \"openclaw\":\n {\n \"requires\":\n {\n \"credentials\": [\"spotify_access_token\"],\n \"credentialChecks\":\n { \"spotify_access_token\": { \"url\": \"https://api.spotify.com/v1/me\" } }\n }\n }\n }\n---\n\n# Spotify\n\nUse `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block
|
|
76
|
-
"weather/SKILL.md": "---\nname: weather\ndescription: Get current weather and short forecasts for cities via wttr.in.\ntools: [
|
|
75
|
+
"spotify/SKILL.md": "---\nname: spotify\ndescription: Play, search, and control music on Spotify — songs, artists, albums, playlists, and playback.\nemoji: 🎵\ntools: [http_request]\nplatform: [darwin, linux, win32, ios, android]\ncredentials: [spotify_access_token]\nallow_list: [https://api.spotify.com/v1/]\nmetadata:\n {\n \"openclaw\":\n {\n \"requires\":\n {\n \"credentials\": [\"spotify_access_token\"],\n \"credentialChecks\":\n { \"spotify_access_token\": { \"url\": \"https://api.spotify.com/v1/me\" } }\n }\n }\n }\n---\n\n# Spotify\n\nUse `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block**.\n\n## Playing a song takes two calls. Always two.\n\n\"Play X\" is not answered until **both** have run:\n\n1. `GET /v1/search` — find the track.\n2. `PUT /v1/me/player/play` — start it.\n\nSearch alone plays nothing. If you have searched and not yet called play, you are not finished: make the play call now.\n\n```json\n{\n \"url\": \"https://api.spotify.com/v1/search\",\n \"method\": \"GET\",\n \"query\": { \"q\": \"Radiohead Creep\", \"type\": \"track\", \"limit\": 3, \"market\": \"from_token\" }\n}\n```\n\n```json\n{\n \"url\": \"https://api.spotify.com/v1/me/player/play\",\n \"method\": \"PUT\",\n \"body\": { \"uris\": [\"spotify:track:70LcF31zb1H0PyJoS1Sx1r\"] }\n}\n```\n\nTake `tracks.items[0].uri` from the search response and paste it into `uris`. A 204 means it started.\n\n## Hard rules\n\n- **`q` carries every word the user named — the title and the artist.** \"play Creep by Radiohead\" searches `q=Radiohead Creep`, never `q=Creep`. Drop the artist and the top hit is a different band's song with the same title.\n- **Before playing, check the item you picked.** Compare its `artists[0].name` with the artist the user named. If they do not match, take the first result that does. A title match under the wrong artist is the wrong song, and the user hears it immediately.\n- **Never write a URI, a JSON block, or \"I'll play it now\" to the user in place of calling play.** Describing the call is not making it.\n- **Every URI you send is one you copied from a search response in this turn.** Never type a `spotify:track:` id from memory or from an earlier turn. A well-formed id that is not real stops what was playing and starts nothing.\n- **A track goes in `uris`. Only `uris`.** `context_uri` takes an album, artist or playlist URI — a track URI there plays nothing. URIs go in the JSON `body`, never in query parameters.\n- **One call per intent.** A 204 means the call landed; do not send it again. Repeating a queue or play call burns the turn and changes nothing.\n- **Name what actually played, read back from the item you used** — its `name` and `artists[0].name`. A 204 says the call was accepted, not which song it was, so never report a title you did not read out of the response.\n- A bare song/artist/album/playlist name is enough — search immediately, take the top match, and tell the user what you picked. Do not ask \"which one?\" before searching.\n- Do not ask about tokens or setup up front. A **401** means Spotify isn't connected (tell the user to run `/connect spotify`). A **404** from `/me/player` means no active device (tell them to open Spotify).\n\n## Other operations\n\nAll paths are under `https://api.spotify.com/v1`.\n\n| Ask | Method and path |\n| --------------- | ----------------------------------------------------------------------- |\n| What's playing? | `GET /me/player/currently-playing` |\n| Pause | `PUT /me/player/pause` |\n| Resume | `PUT /me/player/play` (no body — resumes only, never starts a new song) |\n| Next track | `POST /me/player/next` |\n| Add to queue | `POST /me/player/queue` with `query`: `{ \"uri\": \"spotify:track:<id>\" }` |\n| Play an album/artist/playlist | `PUT /me/player/play` with `body`: `{ \"context_uri\": \"<uri>\" }` |\n| My playlists | `GET /me/playlists` |\n| Top tracks | `GET /me/top/tracks` with `query`: `{ \"time_range\": \"medium_term\" }` |\n| Recently played | `GET /me/player/recently-played` |\n| List devices | `GET /me/player/devices` |\n\nQueueing is the same two calls as playing: search for the track, then `POST /me/player/queue` with the `uri` you just read. Queueing does not start playback.\n\n## Notes\n\n- Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at 3 and **always pass `market`**: a search without it spends most of the budget on `available_markets`, and the results behind the first one are cut off before you can read them. `/search` has no `fields` param, so `market` is the only lever there. On playlist/track endpoints request only what you need with `fields` (e.g. `query`: `{ \"fields\": \"items(track(name,artists(name),uri))\" }`). Read just the top item unless the user asked for a list.\n- Present results as a short numbered list — track, artist, album, duration — and devices as `1. Name (active/idle)`. Never dump raw JSON to the user.\n- Playback commands return **204 No Content** on success (empty body). A **204** from currently-playing means nothing is playing.\n- **403** with `PREMIUM_REQUIRED` → user needs Spotify Premium. Other **403**s are usually app-scope/Development Mode limits — report 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",
|
|
76
|
+
"weather/SKILL.md": "---\nname: weather\ndescription: Get current weather and short forecasts for cities via wttr.in.\ntools: [weather_lookup]\nplatform: [darwin, linux, win32, ios, android]\nversion: 4\n# Tuned on Qwen3.5-2B with an offline eval (35 prompts, mention route), QVAC-24701.\n# v2: few-shot table. 2 repeats: overall 51/132 -> 77/130, \"Nassau, Bahamas\" 1/4 -> 4/4, city+country 2/24 -> 18/24.\n# v3: + country row. 3 repeats: v2 110/198 -> v3 121/198 (city+country 25 -> 31/36, forecast 17 -> 23/30).\n# Aliases, a second ambiguity row and dropping ?T were tried and did not help (73, 74, 54 of ~130).\n# v4: weather_lookup instead of http_request, after v3 answered a 200 for a place that does not exist.\n# 5 repeats, all variants in one sweep so they share a baseline: v3 97/171 -> v4 120/169. city+country\n# 87 -> 93%, unambiguous 57 -> 91%, unknown 57 -> 84%, forecast 68 -> 88%, casual 65 -> 80%, the ticket\n# prompt 80 -> 100%. Context is a wash (2335 -> 2378) though the body is 387 bytes smaller: the tool owns\n# the URL, so three rules about spelling one went away.\n# Not fixed. \"Ask which Nassau\" is 3/25 on v3 and 0/25 here, and asking when no place is named is 1/10 for\n# both: a one-argument call is easy, so the model calls rather than asks. Rules that tell the model not to\n# act have never cleared ~17% on a 2B in four versions, and a variant that made the tool refuse an\n# ambiguous name measured 13% against 17%, so it was dropped rather than shipped.\n---\n\n# Weather\n\nOne `weather_lookup` call, then answer from the result. Always call it exactly like this:\n\n```json\n{ \"location\": \"Nassau, Bahamas\" }\n```\n\nMatch the user's request to a row for the call:\n\n| User asks | Call |\n| --- | --- |\n| \"what's the weather?\" — no place named | **no call.** Ask which city |\n| \"weather in Nassau, Bahamas\" | `{ \"location\": \"Nassau, Bahamas\" }` |\n| \"weather in London\" / \"London today\" | `{ \"location\": \"London\" }` |\n| \"Berlin tomorrow\" / \"this weekend\" | `{ \"location\": \"Berlin\" }` |\n| \"Rome for the next 3 days\" | `{ \"location\": \"Rome\" }` |\n| \"how hot is it in Georgia, the country\" | `{ \"location\": \"Tbilisi, Georgia\" }` (a country → its capital) |\n| \"weather in Nassau\" | `{ \"location\": \"Nassau\" }` — the result names both, ask which |\n\nThe result is the place on the first line, then the weather now, then one line per day:\n\n```\nNassau, New Providence, Bahamas\nNow: 29°C / 84°F, Patchy rain nearby, feels like 33°C, humidity 71%, wind 21 km/h\n2026-09-16 (today): 29-29°C / 84-85°F, Partly Cloudy\n2026-09-17 (tomorrow): 28-29°C / 83-85°F, Moderate or heavy rain shower\n```\n\n## Rules\n\n1. **There is no default place.** If the user named none, do not call: ask which city. Never use a place from this file.\n2. **The location is exactly what the user wrote.** Keep a country or state they gave (`Nassau, Bahamas`, not `Nassau`). Never add one they did not give. A country → its capital (`Tbilisi`).\n3. **Name the place from the first line of the result, not the words the user used.** If they ask for Rome and the first line says `Lome, Maritime, Togo`, say it is Lome in Togo.\n4. **If the result is not a weather report, say what it says, in those words.** `No such place: \"Xyzzyville, Atlantis\"` → tell the user that place does not exist and ask for a real one. Do not look it up again under another name, and never state a temperature you did not receive.\n5. Forecasts stop at 3 days: for \"next week\" say so and give the 3 days you have.\n\nAnswer in one plain sentence with the temperature and the condition.\n",
|
|
77
77
|
"word/SKILL.md": "---\nname: word\ndescription: Create, edit, or read Word (.docx) documents with python-docx — deliver documents as chat attachments, or read an attached one to summarize it or answer questions in the chat. Can embed images generated in the chat. Opens in Pages and Google Docs too.\naliases: [docx, word-document, memo]\npreload_on_name: false\ntools: [exec(python)]\nplatform: [darwin, linux, win32]\nmetadata:\n {\n \"openclaw\":\n {\n \"setup\":\n {\n \"summary\": \"Runs python-docx 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# Word\n\nBuild, change, or read `.docx` documents. This file holds no Python and no\nrecipe: it only says which reference file to load. Load exactly one with the\n`skill` tool, then do what that file says.\n\n## Which File to Load\n\nPick the row by **what the user wants done**, then make that exact `skill`\ncall. \"Edit\", \"update\", \"modify\", \"change\", \"replace\", \"rewrite\" and \"fix\" all\nmean the same thing here — the verb never picks the row, the change does.\n\n| The user wants | The `skill` call |\n| ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |\n| A new document — \"write a report\", \"make a doc with 30 fun facts about cats\", \"draft a letter\" | `{\"name\": \"word\", \"file\": \"references/create.md\"}` |\n| Some paragraphs of an existing document changed — \"replace the first 10 facts with dog facts\", \"change fact 3\", \"reword paragraph 7\", \"swap these bullets for those\" | `{\"name\": \"word\", \"file\": \"references/paragraphs.md\"}` |\n| Anything else done to an existing document — add a section, remove or rewrite a whole section, make the text bigger, put an image in it | `{\"name\": \"word\", \"file\": \"references/rework.md\"}` |\n| An answer in the chat from an attached document — \"summarize this\", \"what does it say about X\" | `{\"name\": \"word\", \"file\": \"references/read.md\"}` |\n\n- A document that already exists in this chat is never rebuilt with\n `create.md` — that throws away everything the user has. Its `attachmentId`\n is in the `exec` result that produced it or on the `[Attached file …]` line.\n- A summary delivered as a file is `read.md` first, then `create.md`.\n- `paragraphs.md` runs two scripts bundled with this skill and contains no\n Python. `rework.md` and `create.md` carry the python-docx recipes to copy.\n\nEach load is a real `skill` tool call — printing the call as JSON or text in\nyour reply loads nothing. Never write the Python from memory: the recipes carry\nrules (exact version pins, attachment staging, the only working removal idiom)\nthat fail in non-obvious ways when improvised, and loading the file is one\ncheap read-only call.\n\n## When to Use\n\n- The user asks for a document, report, letter, memo, `.docx`, or Word file.\n- The user attaches a `.docx` and wants its content changed, replaced in part,\n extended, trimmed, or reworked.\n- The user attaches a `.docx` and asks what it says — a summary, a question\n answered, or content pulled out into the chat.\n- The user wants a document that embeds images generated in this chat.\n\n## When NOT to Use\n\n- The user wants text in the chat and no document is involved — just write it.\n Summarizing or answering from an attached `.docx` **is** this skill: load\n `references/read.md`.\n- The user wants slides or a deck — that is the presentations skill.\n- The user wants a spreadsheet or a PDF — python-docx writes only `.docx`.\n\n## What This Skill Cannot Do\n\nSay so instead of faking these; a fake is worse than a clear \"not supported\":\n\n- **No table of contents.** A real TOC is a Word field that Word itself computes;\n python-docx cannot insert one. Do not fake a TOC by typing headings and page\n numbers — the page numbers would be wrong. Offer headings (`Heading 1..9`)\n instead; Word can generate a TOC from them later.\n- **No tracked changes or comments.** There is no revisions API. Edits land as\n plain content; say that when the user asks for a redline.\n- **No legacy `.doc`.** Only `.docx`. A `.doc` output name is rejected — name it\n `.docx`.\n- **No PDF export and no rendering.** The runtime cannot convert or preview the\n document; it can only write the file.\n\n## Rules for Every Job\n\n**You build it, not the user.** Deliver the document, never the recipe. Do NOT\nprint the python source in chat, do NOT tell the user to install python-docx,\nrun a script, or open a terminal — they have no terminal in this chat and the\ncode would not run there. The document exists only if an `exec` call with\n`outputs` succeeds and returns the attachment; falling back to \"here is the\nscript, run it yourself\" is a failed turn.\n\n**Success = stop.** When `exitCode` is `0` and the result's `attachments`\nlists the `.docx`, the document is done. Do not call `exec` again for the same\nrequest — not to \"confirm\", not to \"improve\", not to \"add the image\" after the\nfact. Exactly one successful _build_ `exec` per document request — a\nno-`outputs` read that precedes a build delivers nothing and is not one of\nthem, but it belongs before the build, never after it. Reply with a single\nline: file name + the count line from stdout. If the result has\n`missingOutputs` instead, the file was never written: read stderr first — an\n`AssertionError` there means a guard stopped the save on purpose (see the\nrework recipe); only when stderr is clean check the `save()` name matches the\ndeclared output and rerun once.\n\n**Failures are fixed in the code, not around it.** An error in your code is\nnever a fault in python-docx or in the runtime; fix the Python against the\nloaded reference file's recipes and Errors and call `exec` again. A bundled\nscript that stops with a message is fixed by correcting its arguments and\nrerunning the same script — never by writing Python in its place. If two\nconsecutive calls fail with the same error, re-read the traceback\nline-by-line before a third — retrying the identical `command`, or a version\nwith only cosmetic changes, is a loop, not a fix. Do not switch package pins\n(keep `python-docx==1.2.0`), do not wrap source in `python -c` / `pip` /\nshell, do not \"debug\" with `os.listdir` or no-op scripts while `outputs`\nstill lists the document, and do not write the document as markdown/chat text\ninstead of a `.docx`. Never search the web about an error; the answer is\nalways in the `exec` result you already have.\n\n**The runtime is sealed.** There is no shell — `ls`, `cat`, and `file` raise\n`SyntaxError` because `command` is Python source — and no network:\n`requests`, `urllib`, and `socket` all fail. The working directory starts\nempty on every call: a file from an earlier call is gone unless staged again,\nand a file you write but do not declare in `outputs` is discarded. The `exec`\nresult is the only account of what happened — there is no filesystem to check\nand no shell to check it with.\n\n**Never overwrite a staged input.** Changes always save a new output name,\nderived from the document changed — `report.docx` becomes `report_revised.docx`,\nnever a fresh name taken from the new content.\n\n**A change happens inside the document.** `add_paragraph` and `add_heading`\nappend at the end and nowhere else, so replacing content that is already there\nmeans rewriting those paragraphs, not adding new ones. Delivering the original\nwith the new version appended, or a fresh document holding only the new\ncontent, is a failed turn.\n",
|
|
78
78
|
"word/references/create.md": "# Creating a Word Document (python-docx)\n\nCreate a new `.docx` from scratch by running Python through the `exec` tool.\nA new document needs **no** `inputs` — do not invent attachment ids — unless\nit embeds an image (see Embedding Images). **Exactly one** `exec` call per\nuser request when that call succeeds.\n\n**A document that already exists in this chat is never rebuilt here.** \"Replace\nthe first 10 facts\", \"reword this\", \"add a section\" — any request that starts\nfrom an existing `.docx` is a change to that document: some of its paragraphs\nis `references/paragraphs.md`, anything else is `references/rework.md`, and\neither one stages the document by its `attachmentId`. Building a fresh\ndocument for such a request throws away everything the user already has.\n\n## The exec call\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-docx==1.2.0\"],\n \"outputs\": [\"report.docx\"],\n \"command\": \"...\"\n}\n```\n\n- `language` — always `\"python\"`.\n- `packages` — `[\"python-docx==1.2.0\"]` on every call. The PyPI package is\n `python-docx` but the import is `docx`; never list `docx` as the package —\n that resolves a different, abandoned library. Pin the version; an unpinned\n install resolves a potentially different library version. This exact version\n ships with the app and installs with no network; any other version has to be\n downloaded, which fails on a device that is offline.\n- `outputs` — `[\"report.docx\"]`. `save(\"report.docx\")` must match the declared\n output name. A file you write but do not declare here is discarded. A `.doc`\n output name is rejected — name it `.docx`.\n- `command` — the multi-line Python source, with real newline characters.\n Never collapse it to one line joined by `;` — a `for`/`if`/`with` after a\n semicolon is a `SyntaxError`. Its first line is the first line of Python\n that runs: there is no shell and no interpreter to invoke, and no\n installer — packages are declared in `packages`.\n\n## Embedding Images\n\nTwo kinds of image input, told apart by where the file came from:\n\n**Tool-produced images** (`generate_image` output): stage them with the exact\n`attachmentId` from the tool result — never placeholders like `att_image` or\nany id you made up.\n\n**Images the user uploaded** (\"use this photo\"): there is no id to copy — an\nuploaded image never shows one. Stage it with `path` only and **no\n`attachmentId` key**; the first id-less entry is the first image of the user's\nlatest message, the second is its second image, and so on. Id-less entries\nresolve _images only_.\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-docx==1.2.0\"],\n \"inputs\": [{ \"path\": \"photo.png\" }],\n \"outputs\": [\"report.docx\"],\n \"command\": \"...\"\n}\n```\n\nStaged files land in the working directory under the bare `path` names —\nreference `doc.add_picture(\"photo.png\", …)` by that name only. Paths must be\nunique bare filenames. `attachment … not found in this chat` means you\ninvented an id or the file is not attached: re-copy the exact id from the tool\nresult, or for a document with no image drop `inputs` entirely.\n\nIf the image was staged in `inputs`, embed it in **that** single build with\n`doc.add_picture` — never deliver a document and then rebuild to add the\nimage. Soft-failing (`try`/`except` around the picture) and saving without it\nis a failed turn, not a success.\n\n**Image URLs do not work — never download.** Your Python code has **no\nnetwork access**: `requests`, `urllib`, and `socket` all fail with a network\nerror, and `http_request` returns truncated text, never image bytes. When the\nuser gives an image URL, do not try to fetch it from Python and do not retry\nthrough other tools — that is a dead end. Say the link cannot be downloaded\nand ask the user to attach the image itself, or offer `generate_image` for a\nsimilar visual. Then build the document with the staged attachment as above.\n\n## Which Shape\n\n| The user asks for | Shape |\n| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| \"30 fun facts about cats\", \"10 tips for …\", \"a list of …\", any number of items or points | **List** — a title, then exactly N `List Bullet` paragraphs and nothing else: no intro sentence, no section headings, no numbers typed into the text |\n| a report, memo, letter, plan — anything with sections | **Report** — the recipe under The Recipe below |\n\n### The list shape\n\n```python\nfrom docx import Document\n\ndoc = Document()\ndoc.add_heading(\"30 Fun Facts About Cats\", level=0)\nfacts = [\n \"Cats sleep for about 70 percent of their lives.\",\n \"A group of cats is called a clowder.\",\n \"A cat's nose print is unique, like a fingerprint.\",\n] # one plain string per item — write all N here\nfor fact in facts:\n doc.add_paragraph(fact, style=\"List Bullet\")\ndoc.save(\"cat_facts.docx\") # must match the declared output exactly\nprint(f\"{len(facts)} items, {len(doc.paragraphs)} paragraphs, {len(doc.tables)} table(s)\")\n```\n\nOne string per item, as many as the user asked for. No headings between\ngroups of items and no introductory sentence: each of those is a paragraph the\nuser did not ask for, and a later \"change the first 10 items\" then lands on the\nwrong lines. The count line it prints is the reply — a delivered list is done,\nwhatever the count says; never rebuild it to fix the number.\n\n## The Recipe\n\nStart from this for a report. It is a complete, working document — a title, headings,\nparagraphs with bold and italic runs, a bulleted list, and a table — saved\nunder the declared output name. Copy it and change the content; do not\nassemble a document from memory.\n\n**Keep the source multi-line.** A `for`/`if`/`with` after a semicolon is a\n`SyntaxError` — paste the block with real newlines, not `stmt; for x in y: …`.\n\n**Hold content in plain lists of strings, and walk them.** Every list of bullets\nis a flat `[\"…\", \"…\"]`, and every table is a list of row lists. Do not reach for\na dict, a tuple of mixed widths, or a nested comprehension to hold document\ncontent — those are where a `SyntaxError` or a\n`ValueError: too many values to unpack` comes from, and they buy nothing here.\n\n**Keep every underscore in API names.** `add_heading`, `add_paragraph`,\n`add_run`, `add_table`, `add_row`, `add_picture`, `add_page_break` — stripping\nthem to `addheading` / `addparagraph` fails. Copy identifiers exactly as written\nbelow:\n\n```python\nfrom docx import Document\nfrom docx.shared import Inches, Pt, RGBColor # one import line covers sizes, widths, colors\n\ndoc = Document()\n\ndoc.add_heading(\"Quarterly Report\", level=0)\ndoc.add_paragraph(\"Prepared by the finance team.\")\n\ndoc.add_heading(\"Summary\", level=1)\np = doc.add_paragraph(\"Revenue grew \")\nstrong = p.add_run(\"18 percent\")\nstrong.bold = True\np.add_run(\" against a \")\nemphasis = p.add_run(\"flat\")\nemphasis.italic = True\np.add_run(\" cost base.\")\n\ndoc.add_heading(\"Highlights\", level=1)\nfor point in [\n \"New retail partners in two regions\",\n \"Churn down for the third quarter\",\n \"Support backlog cleared\",\n]:\n doc.add_paragraph(point, style=\"List Bullet\")\n\ndoc.add_heading(\"Key Figures\", level=1)\nfigures = [\n [\"Metric\", \"Q3\", \"Q4\"], # first list is the header row\n [\"Revenue\", \"$1.2M\", \"$1.4M\"],\n [\"Costs\", \"$0.9M\", \"$0.9M\"],\n]\ntable = doc.add_table(rows=1, cols=len(figures[0]))\ntable.style = \"Table Grid\"\nfor index, cells in enumerate(figures):\n row = table.rows[0].cells if index == 0 else table.add_row().cells\n for column, value in enumerate(cells):\n row[column].text = value\n\ndoc.save(\"report.docx\") # must match the declared output exactly\nprint(f\"{len(doc.paragraphs)} paragraphs, {len(doc.tables)} table(s)\")\n```\n\n## Write a document, not markdown\n\nA `.docx` carries real styles, so the structure is the style — never the\npunctuation. Markdown written into text stays there verbatim and reads as a\ntypo in the finished document:\n\n- **No markdown characters in any string.** `#`, `##`, `-`, `*`, `1.`, `**bold**`\n and backticks all render literally. `add_heading(\"Security\", level=2)` — never\n `add_heading(\"- Security\", level=2)` or `\"## Security\"`. A numbered list is\n `style=\"List Number\"`, which numbers itself; a typed `\"1. \"` prefix double-numbers.\n- **No typed rules or line breaks.** A row of dashes or underscores as a section\n divider is just those characters on the page, and a leading `\"\\n\"` is a blank\n line inside the paragraph. Headings already separate sections.\n- **Every section title is a heading.** A first section called \"Introduction\" or\n \"Overview\" goes through `add_heading(..., level=1)` like every other one; as a\n plain `add_paragraph` it renders as body text and the document looks unstructured.\n- **No blank paragraphs for spacing.** `add_paragraph(\"\")` leaves a visible gap —\n the heading and body styles already carry their own space before and after.\n- **Bold is for a few words, not a sentence.** A fully bold paragraph reads as a\n formatting mistake; bold the term, then continue in a normal run.\n\n## One paragraph, one string\n\n`add_paragraph` takes a single text string, optionally with `style=` — nothing\nelse. Several sentences passed positionally raise\n`TypeError: Document.add_paragraph() takes from 1 to 3 positional arguments but 4\nwere given`. Join them into one string, or open the paragraph with the first\npiece and add the rest as runs:\n\n```python\np = doc.add_paragraph(\"As of 2026, Bitcoin is widely held. \")\np.add_run(\"Adoption keeps growing.\")\n```\n\n**The text you pass to `add_paragraph` is already the paragraph's first run.** A\nrun added afterwards _appends_ — repeating any of those words writes them twice\ninto the document (`\"…finite supplyfinite supply\"`). Each run carries the next\nwords and only those, so give a mixed-format paragraph an empty start and add\nevery piece as its own run:\n\n```python\np = doc.add_paragraph()\np.add_run(\"Digital scarcity \")\ntail = p.add_run(\"and a finite supply\")\ntail.italic = True\n```\n\n## Bold and italic live on runs, never on paragraphs\n\n`paragraph.bold = True` raises no error and changes **nothing** in the file — a\nparagraph has no bold; the assignment lands on the Python object and is silently\ndiscarded on save. Formatting belongs to runs:\n\n```python\np = doc.add_paragraph(\"normal, then \")\nstrong = p.add_run(\"bold\")\nstrong.bold = True\np.add_run(\" and \")\nemphasis = p.add_run(\"italic\")\nemphasis.italic = True\n```\n\nTwo rules make that shape the only one to write:\n\n- **`add_run` takes the text and nothing else.** `p.add_run(\"x\", bold=True)`\n raises `TypeError: Paragraph.add_run() got an unexpected keyword argument\n'bold'` — create the run, then set the attribute.\n- **Never chain an attribute onto the `add_run(...)` call.** Name the run on one\n line and format it on the next, as above. A run that needs no formatting is a\n bare `p.add_run(\"plain text\")` and the line ends there — a trailing `.` left\n over from a half-written chain is `SyntaxError: invalid syntax`.\n- **Runs join with no gap between them.** The next run starts exactly where the\n last one ended, so the separating space belongs inside one of the strings —\n `\"…without intermediaries. \"` then `\"It was invented\"`, never\n `\"…intermediaries.\"` followed by `\"It was invented\"`.\n\n**`add_run` belongs to the paragraph, not to a run.** Keep the paragraph in a\nvariable and call `p.add_run(...)` for every run in it — chaining a second run off\nthe first raises `AttributeError: 'Run' object has no attribute 'add_run'`. A run\nowns `.text`, `.bold`, `.italic` and `.font`, and nothing else: it has no\n`add_run`, no `add_paragraph`, and no `.style`.\n\nA run is also not a string: `p.add_run(\" \") * 2` raises\n`TypeError: unsupported operand type(s) for *: 'Run' and 'int'`. Put any repeated\ntext inside the string itself — and reach for neither, since spacing is the\nstyle's job, not padding you type.\n\nCharacter detail goes through `run.font` — size, color:\n\n```python\nfrom docx.shared import Pt, RGBColor\n\np = doc.add_paragraph()\nrun = p.add_run(\"Key finding\")\nrun.font.size = Pt(14)\nrun.font.color.rgb = RGBColor(0x1A, 0x73, 0xE8) # RGB in all caps\n```\n\n`Pt`, `Inches`, and `RGBColor` all import from `docx.shared` — there is no\n`docx.util` and no `docx.dml.color`; those are python-pptx paths and fail here.\n\n## Styles must exist in the document\n\n`style=\"List Bullet\"` names a style **inside the document**. A missing name\nraises `KeyError: \"no style with name 'List Bullet'\"` at `add_paragraph` time.\n\nA **new** `Document()` ships these styles — safe to use without checking:\n`Title`, `Heading 1` … `Heading 9`, `Normal`, `List Bullet` (+ ` 2`, ` 3`),\n`List Number` (+ ` 2`, ` 3`), `Intense Quote`, and the table style `Table Grid`.\nDo not invent other names for a new document. (An uploaded document carries\nonly its own styles — when editing one, load `references/rework.md` for the\nguard.)\n\n## Headings and lists\n\n- `doc.add_heading(text, level=N)` — level `0` is the document title style,\n `1`–`9` map to `Heading 1`–`Heading 9`. Any other level raises\n `ValueError: level must be in range 0-9`.\n- Bullets: one `add_paragraph(point, style=\"List Bullet\")` per point, over a flat\n list of plain strings. Never pack several points into one paragraph with `\\n` —\n a `\\n` is a soft line break inside the same list item, not a new bullet. A\n bullet that needs a label and a detail is one string (`\"Limited supply — 21\nmillion coins\"`), never a dict entry or a tuple.\n- Numbered lists: `style=\"List Number\"`. Indent a level with `List Bullet 2` /\n `List Number 2`.\n\n## Tables\n\nWrite the whole table as a list of row lists — header first — then let the code\nabove derive everything from it. **Always `rows=1` and `cols=len(rows[0])`**:\n\n```python\nrows = [\n [\"Item\", \"Status\"], # header\n [\"Search\", \"Shipped\"],\n [\"Export\", \"In review\"],\n]\ntable = doc.add_table(rows=1, cols=len(rows[0]))\ntable.style = \"Table Grid\" # borders; omit for invisible grid\nfor index, cells in enumerate(rows):\n row = table.rows[0].cells if index == 0 else table.add_row().cells\n for column, value in enumerate(cells):\n row[column].text = value\n```\n\nThat shape exists because the two hand-written alternatives both fail:\n\n- **`rows=` is a count of blank rows created immediately, not a maximum.**\n `add_table(rows=4, …)` followed by `add_row()` per entry leaves three empty\n rows sitting between the header and the data, plainly visible in the finished\n document. `rows=1` is the header; every other row comes from `add_row()`.\n- **Unpacking a row into fixed names breaks the moment a row is a different\n width.** `for name, q3, q4 in data:` raises\n `ValueError: too many values to unpack (expected 3, got 4)`, and hand-counting\n `cols=` against the data is the same mistake one step earlier. Index the cells\n instead, and take the column count from the header.\n\nAddress cells as `table.cell(row, col)` or `table.rows[r].cells[c]` — they are\nthe same cell. Rows only grow at the bottom: there is no insert-at.\n`table.rows[9]` on a 4-row table raises `IndexError`. Write text with\n`cell.text = \"…\"`; for formatting inside a cell go through `cell.paragraphs[0]`\nand its runs like any other paragraph.\n\n## Images and page breaks\n\n`doc.add_picture(name, width=…)` appends the image in its own paragraph. Pass\nonly one of `width`/`height`; passing both distorts the picture.\n\n```python\nfrom docx.shared import Inches\n\ndoc.add_picture(\"figure1.png\", width=Inches(5.5))\ndoc.add_page_break()\n```\n\n**Do not soft-fail images or imports.** Never wrap `add_picture` or an import in\n`try`/`except` that prints a warning and continues. A missing file must raise so\nyou fix it and rerun — a document saved without the requested image is a failed\nturn, not a success.\n\n## Errors\n\n- `ModuleNotFoundError: No module named 'docx'` means `packages` was missing or\n wrong — add `[\"python-docx==1.2.0\"]` and rerun. Never try to install it, and\n never \"fix\" it by importing `python_docx`; the import stays `docx`.\n- `TypeError: 'Table' object is not subscriptable` — a table was indexed\n directly (`table[0]`). Cells are reached through `table.rows[r].cells[c]` or\n `table.cell(r, c)`; a whole row of cells is `table.add_row().cells`.\n- `KeyError: \"no style with name '…'\"` — the style is not in this document. For\n a new document use only the names listed under Styles.\n- `NameError: name 'RGBColor' is not defined` (or `Pt`, `Inches`) — the import\n line is missing that name. Keep the sample's single\n `from docx.shared import Inches, Pt, RGBColor` rather than importing one at a time.\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 (`add_paragraph`, not `addparagraph`) must stay. Do not switch to\n `python -c` or change the package pin.\n- On an `AttributeError` from python-docx the API name is wrong; on a `TypeError`\n about positional arguments the call passes the wrong number of them — usually\n several strings where one is allowed. Fix either against this file's examples,\n reading the line number in the traceback. Do not retry the same call, and do\n not switch to a shell.\n- `attachment … not found in this chat` means `inputs` listed an id that is not\n in this chat (often a copied placeholder like `att_doc`). For a new document,\n omit `inputs` entirely and rerun. Only stage real ids from prior tool results.\n- Never print the document's bytes or base64 — stdout is capped and the file\n travels through `outputs`. A build call prints exactly one line (e.g. `9\nparagraphs, 1 table(s)`).\n- Never pass an absolute path to `save()`.\n\n## Finish\n\nWhen `exitCode` is `0` and `attachments` lists the `.docx`, the document is\ndone — the `exec` result carries\n`attachments: [{ attachmentId, fileName, byteLength }]` and the file is already\nattached to the chat for the user to open or save, exactly like a\n`generate_image` result. Stop tool use and reply with a single line: file name\n\n- the count line from stdout. Exactly one successful `exec` per request. If\n the result has `missingOutputs` instead, the file was never written: check the\n `save()` name matches the declared output and rerun once.\n",
|
|
79
79
|
"word/references/paragraphs.md": "# Changing Some Paragraphs of a Document (bundled scripts)\n\n\"Replace the first 10 facts\", \"change fact 3\", \"swap these bullets for those\",\n\"reword paragraph 7\": two `exec` calls, both running a script bundled with this\nskill. **Write no Python.** There is no `command` in this job — a call with\n`command` is the wrong call. Pass `skill`, `script`, and `scriptArgs` exactly as\nshown, with `inputs` staging the document by its `attachmentId` (from the\nearlier `exec` result or the `[Attached file …]` line — copy it verbatim, never\ninvent one).\n\n| Step | The `exec` call |\n| --------------------------------------- | ---------------------------------------------------------------- |\n| 1. see the paragraphs and their indexes | `scripts/list_paragraphs.py`, `inputs` staged, no `outputs` |\n| 2. replace exactly the chosen indexes | `scripts/replace_paragraphs.py`, `inputs` staged, one `outputs` |\n\n## Step 0 — find the document's `attachmentId`\n\nThe id is in the chat already, never invented: a document built earlier in\nthis chat has it in the `attachments` of the `exec` result that produced it —\n`{\"attachmentId\":\"922bd4e17517b90593be1c5ae4f12fbd\",\"fileName\":\"cat_facts.docx\"}`\n— and a document the user uploaded has it on the `[Attached file …]` line of\ntheir message. Copy that exact id into `inputs`. An `inputs` entry with a\n`path` and no `attachmentId` is an _image_ upload and is refused for a\ndocument:\n\n```json\n{ \"inputs\": [{ \"path\": \"existing.docx\" }] }\n```\n\n## Step 1 — list the paragraphs (no `outputs`)\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-docx==1.2.0\"],\n \"inputs\": [{ \"attachmentId\": \"<real id>\", \"path\": \"existing.docx\" }],\n \"skill\": \"word\",\n \"script\": \"scripts/list_paragraphs.py\",\n \"scriptArgs\": [\"existing.docx\"]\n}\n```\n\nEvery key above is required — `inputs` with the document's real\n`attachmentId`, `skill`, `script`, `scriptArgs`. A call missing `skill` or\n`inputs` is refused.\n\nIt prints one line per paragraph — `index`, style, text — then a count line.\nPick the indexes to replace from that list:\n\n- Only body paragraphs (`Normal`, `List Bullet`, `List Number`) are facts,\n points, or bullets. `Title`, `Heading N`, and an intro sentence are never\n counted as one.\n- \"The first 10 facts\" = the first 10 body-paragraph indexes after the heading\n or sentence that introduces them — not indexes 0–9.\n- Fewer facts in the document than asked for: replace the ones that exist and\n say so in the reply.\n\n## Step 2 — replace exactly those paragraphs (one `outputs` entry)\n\n`scriptArgs` is: input name, output name, then `index, new text` pairs — one\npair per replaced paragraph, as many pairs as facts requested. The output name\nkeeps the input's stem plus `_revised`.\n\nCount the pairs before sending: \"the first 10 facts\" is 10 pairs — 20 strings\nafter the two file names, 10 different sentences, the last index being\nstart + 9.\n\n```json\n{\n \"language\": \"python\",\n \"packages\": [\"python-docx==1.2.0\"],\n \"inputs\": [{ \"attachmentId\": \"<real id>\", \"path\": \"existing.docx\" }],\n \"outputs\": [\"existing_revised.docx\"],\n \"skill\": \"word\",\n \"script\": \"scripts/replace_paragraphs.py\",\n \"scriptArgs\": [\n \"existing.docx\", \"existing_revised.docx\",\n \"3\", \"Dogs have about 1,700 taste buds.\",\n \"4\", \"A dog's nose print is unique, like a fingerprint.\"\n ]\n}\n```\n\nWrong, for this job — a `command` instead of a `script`:\n\n```json\n{ \"command\": \"from docx import Document\\ndoc = Document(\\\"existing.docx\\\")\\nfor i in range(1, 11): ...\" }\n```\n\nEach new text is one complete plain sentence, no markdown, each different. The\nscript keeps each paragraph's paragraph style, refuses a heading index,\nrefuses text that already reads the same, and prints\n`K of N paragraphs replaced`.\n\n## Finish\n\n`exitCode 0` plus an attachment = done. Reply with one line: the file name and\nthe printed count line. Do not call `exec` again for this request.\n\n## Errors\n\nThe script stops with a message that names the fix; correct the arguments and\nrerun the **same script** — never switch to writing Python.\n\n- `usage: replace_paragraphs.py …` — the pairs are incomplete: after the two\n file names, arguments alternate `index`, `text`.\n- `scriptArgs name \"existing_revised.docx\" but the working directory starts\n empty` — the call has no `outputs`; add `\"outputs\": [\"existing_revised.docx\"]`\n (the same name as in `scriptArgs`) and rerun the same script.\n- `script runs need the owning skill name in skill` — add `\"skill\": \"word\"`.\n- `index N is the heading '…'` — that paragraph is a heading, not a fact. Pick\n body indexes from the Step 1 list.\n- `index N is outside the document's M paragraphs` — re-read the Step 1 list;\n indexes run from 0 to M-1.\n- `index N already reads exactly that` — the new text equals the old one; write\n a different sentence.\n- `output … must be a new name` — the output name equals the input's; use\n `existing_revised.docx`.\n- `an id-less input stages an uploaded image` — the `inputs` entry has no\n `attachmentId`; add the document's id from Step 0 and rerun the same script.\n- `usage: list_paragraphs.py <input.docx>` — `scriptArgs` was left out; pass\n the staged path, `[\"existing.docx\"]`.\n- `PackageNotFoundError` / `attachment … not found` / `does not exist in the\n working directory` — `inputs` is missing or carries an invented id; stage the\n document by its real `attachmentId`.\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 = '
|
|
2
|
+
export const SKILLS_HASH = '8357531791b7cdb8'
|
package/package.json
CHANGED
package/skills/spotify/SKILL.md
CHANGED
|
@@ -22,37 +22,25 @@ metadata:
|
|
|
22
22
|
|
|
23
23
|
# Spotify
|
|
24
24
|
|
|
25
|
-
Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block**.
|
|
25
|
+
Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block**.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
"url": "https://api.spotify.com/v1/search",
|
|
30
|
-
"method": "GET",
|
|
31
|
-
"query": { "q": "Radiohead Creep", "type": "track", "limit": 5 }
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Hard rules
|
|
27
|
+
## Playing a song takes two calls. Always two.
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
- Always search before playing by name. Play carries URIs only in the JSON `body` (`"uris": ["…"]`), never as query parameters.
|
|
39
|
-
- A bare `PUT /me/player/play` with no body only resumes paused playback — it never plays a requested song. For an album/artist/playlist use `{ "context_uri": "<uri>" }` instead of `uris`.
|
|
40
|
-
- Do not ask about tokens or setup up front. A **401** means Spotify isn't connected (tell the user to run `/connect spotify`). A **404** from `/me/player` means no active device (tell them to open Spotify).
|
|
29
|
+
"Play X" is not answered until **both** have run:
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
1. `GET /v1/search` — find the track.
|
|
32
|
+
2. `PUT /v1/me/player/play` — start it.
|
|
43
33
|
|
|
44
|
-
|
|
34
|
+
Search alone plays nothing. If you have searched and not yet called play, you are not finished: make the play call now.
|
|
45
35
|
|
|
46
36
|
```json
|
|
47
37
|
{
|
|
48
38
|
"url": "https://api.spotify.com/v1/search",
|
|
49
39
|
"method": "GET",
|
|
50
|
-
"query": { "q": "Radiohead Creep", "type": "track", "limit":
|
|
40
|
+
"query": { "q": "Radiohead Creep", "type": "track", "limit": 3, "market": "from_token" }
|
|
51
41
|
}
|
|
52
42
|
```
|
|
53
43
|
|
|
54
|
-
2. Copy `tracks.items[0].uri` into the play body:
|
|
55
|
-
|
|
56
44
|
```json
|
|
57
45
|
{
|
|
58
46
|
"url": "https://api.spotify.com/v1/me/player/play",
|
|
@@ -61,6 +49,20 @@ Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential
|
|
|
61
49
|
}
|
|
62
50
|
```
|
|
63
51
|
|
|
52
|
+
Take `tracks.items[0].uri` from the search response and paste it into `uris`. A 204 means it started.
|
|
53
|
+
|
|
54
|
+
## Hard rules
|
|
55
|
+
|
|
56
|
+
- **`q` carries every word the user named — the title and the artist.** "play Creep by Radiohead" searches `q=Radiohead Creep`, never `q=Creep`. Drop the artist and the top hit is a different band's song with the same title.
|
|
57
|
+
- **Before playing, check the item you picked.** Compare its `artists[0].name` with the artist the user named. If they do not match, take the first result that does. A title match under the wrong artist is the wrong song, and the user hears it immediately.
|
|
58
|
+
- **Never write a URI, a JSON block, or "I'll play it now" to the user in place of calling play.** Describing the call is not making it.
|
|
59
|
+
- **Every URI you send is one you copied from a search response in this turn.** Never type a `spotify:track:` id from memory or from an earlier turn. A well-formed id that is not real stops what was playing and starts nothing.
|
|
60
|
+
- **A track goes in `uris`. Only `uris`.** `context_uri` takes an album, artist or playlist URI — a track URI there plays nothing. URIs go in the JSON `body`, never in query parameters.
|
|
61
|
+
- **One call per intent.** A 204 means the call landed; do not send it again. Repeating a queue or play call burns the turn and changes nothing.
|
|
62
|
+
- **Name what actually played, read back from the item you used** — its `name` and `artists[0].name`. A 204 says the call was accepted, not which song it was, so never report a title you did not read out of the response.
|
|
63
|
+
- A bare song/artist/album/playlist name is enough — search immediately, take the top match, and tell the user what you picked. Do not ask "which one?" before searching.
|
|
64
|
+
- Do not ask about tokens or setup up front. A **401** means Spotify isn't connected (tell the user to run `/connect spotify`). A **404** from `/me/player` means no active device (tell them to open Spotify).
|
|
65
|
+
|
|
64
66
|
## Other operations
|
|
65
67
|
|
|
66
68
|
All paths are under `https://api.spotify.com/v1`.
|
|
@@ -69,17 +71,20 @@ All paths are under `https://api.spotify.com/v1`.
|
|
|
69
71
|
| --------------- | ----------------------------------------------------------------------- |
|
|
70
72
|
| What's playing? | `GET /me/player/currently-playing` |
|
|
71
73
|
| Pause | `PUT /me/player/pause` |
|
|
72
|
-
| Resume | `PUT /me/player/play` (no body)
|
|
74
|
+
| Resume | `PUT /me/player/play` (no body — resumes only, never starts a new song) |
|
|
73
75
|
| Next track | `POST /me/player/next` |
|
|
74
76
|
| Add to queue | `POST /me/player/queue` with `query`: `{ "uri": "spotify:track:<id>" }` |
|
|
77
|
+
| Play an album/artist/playlist | `PUT /me/player/play` with `body`: `{ "context_uri": "<uri>" }` |
|
|
75
78
|
| My playlists | `GET /me/playlists` |
|
|
76
79
|
| Top tracks | `GET /me/top/tracks` with `query`: `{ "time_range": "medium_term" }` |
|
|
77
80
|
| Recently played | `GET /me/player/recently-played` |
|
|
78
81
|
| List devices | `GET /me/player/devices` |
|
|
79
82
|
|
|
83
|
+
Queueing is the same two calls as playing: search for the track, then `POST /me/player/queue` with the `uri` you just read. Queueing does not start playback.
|
|
84
|
+
|
|
80
85
|
## Notes
|
|
81
86
|
|
|
82
|
-
- Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at
|
|
87
|
+
- Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at 3 and **always pass `market`**: a search without it spends most of the budget on `available_markets`, and the results behind the first one are cut off before you can read them. `/search` has no `fields` param, so `market` is the only lever there. On playlist/track endpoints request only what you need with `fields` (e.g. `query`: `{ "fields": "items(track(name,artists(name),uri))" }`). Read just the top item unless the user asked for a list.
|
|
83
88
|
- Present results as a short numbered list — track, artist, album, duration — and devices as `1. Name (active/idle)`. Never dump raw JSON to the user.
|
|
84
89
|
- Playback commands return **204 No Content** on success (empty body). A **204** from currently-playing means nothing is playing.
|
|
85
90
|
- **403** with `PREMIUM_REQUIRED` → user needs Spotify Premium. Other **403**s are usually app-scope/Development Mode limits — report and stop.
|
package/skills/weather/SKILL.md
CHANGED
|
@@ -1,43 +1,59 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: weather
|
|
3
3
|
description: Get current weather and short forecasts for cities via wttr.in.
|
|
4
|
-
tools: [
|
|
4
|
+
tools: [weather_lookup]
|
|
5
5
|
platform: [darwin, linux, win32, ios, android]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
# v2: few-shot table. 2 repeats: overall 51/132 -> 77/130, "Nassau, Bahamas" 1/4 -> 4/4, city+country 2/24 -> 18/24,
|
|
10
|
-
# ambiguous city 0/20 -> 6/20; 4B control 41/66 -> 59/66. Rules-first layouts lost on suffix slips (?3, ?1T).
|
|
6
|
+
version: 4
|
|
7
|
+
# Tuned on Qwen3.5-2B with an offline eval (35 prompts, mention route), QVAC-24701.
|
|
8
|
+
# v2: few-shot table. 2 repeats: overall 51/132 -> 77/130, "Nassau, Bahamas" 1/4 -> 4/4, city+country 2/24 -> 18/24.
|
|
11
9
|
# v3: + country row. 3 repeats: v2 110/198 -> v3 121/198 (city+country 25 -> 31/36, forecast 17 -> 23/30).
|
|
12
|
-
# Aliases, a second ambiguity row
|
|
10
|
+
# Aliases, a second ambiguity row and dropping ?T were tried and did not help (73, 74, 54 of ~130).
|
|
11
|
+
# v4: weather_lookup instead of http_request, after v3 answered a 200 for a place that does not exist.
|
|
12
|
+
# 5 repeats, all variants in one sweep so they share a baseline: v3 97/171 -> v4 120/169. city+country
|
|
13
|
+
# 87 -> 93%, unambiguous 57 -> 91%, unknown 57 -> 84%, forecast 68 -> 88%, casual 65 -> 80%, the ticket
|
|
14
|
+
# prompt 80 -> 100%. Context is a wash (2335 -> 2378) though the body is 387 bytes smaller: the tool owns
|
|
15
|
+
# the URL, so three rules about spelling one went away.
|
|
16
|
+
# Not fixed. "Ask which Nassau" is 3/25 on v3 and 0/25 here, and asking when no place is named is 1/10 for
|
|
17
|
+
# both: a one-argument call is easy, so the model calls rather than asks. Rules that tell the model not to
|
|
18
|
+
# act have never cleared ~17% on a 2B in four versions, and a variant that made the tool refuse an
|
|
19
|
+
# ambiguous name measured 13% against 17%, so it was dropped rather than shipped.
|
|
13
20
|
---
|
|
14
21
|
|
|
15
22
|
# Weather
|
|
16
23
|
|
|
17
|
-
One `
|
|
24
|
+
One `weather_lookup` call, then answer from the result. Always call it exactly like this:
|
|
18
25
|
|
|
19
26
|
```json
|
|
20
|
-
{ "
|
|
27
|
+
{ "location": "Nassau, Bahamas" }
|
|
21
28
|
```
|
|
22
29
|
|
|
23
|
-
Match the user's request to a row for the
|
|
30
|
+
Match the user's request to a row for the call:
|
|
24
31
|
|
|
25
32
|
| User asks | Call |
|
|
26
33
|
| --- | --- |
|
|
27
|
-
| "weather
|
|
28
|
-
| "weather in
|
|
29
|
-
| "
|
|
30
|
-
| "
|
|
31
|
-
| "
|
|
32
|
-
| "
|
|
33
|
-
| "
|
|
34
|
+
| "what's the weather?" — no place named | **no call.** Ask which city |
|
|
35
|
+
| "weather in Nassau, Bahamas" | `{ "location": "Nassau, Bahamas" }` |
|
|
36
|
+
| "weather in London" / "London today" | `{ "location": "London" }` |
|
|
37
|
+
| "Berlin tomorrow" / "this weekend" | `{ "location": "Berlin" }` |
|
|
38
|
+
| "Rome for the next 3 days" | `{ "location": "Rome" }` |
|
|
39
|
+
| "how hot is it in Georgia, the country" | `{ "location": "Tbilisi, Georgia" }` (a country → its capital) |
|
|
40
|
+
| "weather in Nassau" | `{ "location": "Nassau" }` — the result names both, ask which |
|
|
41
|
+
|
|
42
|
+
The result is the place on the first line, then the weather now, then one line per day:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Nassau, New Providence, Bahamas
|
|
46
|
+
Now: 29°C / 84°F, Patchy rain nearby, feels like 33°C, humidity 71%, wind 21 km/h
|
|
47
|
+
2026-09-16 (today): 29-29°C / 84-85°F, Partly Cloudy
|
|
48
|
+
2026-09-17 (tomorrow): 28-29°C / 83-85°F, Moderate or heavy rain shower
|
|
49
|
+
```
|
|
34
50
|
|
|
35
51
|
## Rules
|
|
36
52
|
|
|
37
|
-
1. **
|
|
38
|
-
2. **
|
|
39
|
-
3. **
|
|
40
|
-
4. **
|
|
41
|
-
5.
|
|
53
|
+
1. **There is no default place.** If the user named none, do not call: ask which city. Never use a place from this file.
|
|
54
|
+
2. **The location is exactly what the user wrote.** Keep a country or state they gave (`Nassau, Bahamas`, not `Nassau`). Never add one they did not give. A country → its capital (`Tbilisi`).
|
|
55
|
+
3. **Name the place from the first line of the result, not the words the user used.** If they ask for Rome and the first line says `Lome, Maritime, Togo`, say it is Lome in Togo.
|
|
56
|
+
4. **If the result is not a weather report, say what it says, in those words.** `No such place: "Xyzzyville, Atlantis"` → tell the user that place does not exist and ask for a real one. Do not look it up again under another name, and never state a temperature you did not receive.
|
|
57
|
+
5. Forecasts stop at 3 days: for "next week" say so and give the 3 days you have.
|
|
42
58
|
|
|
43
|
-
|
|
59
|
+
Answer in one plain sentence with the temperature and the condition.
|