@qvac/skills 0.1.11 → 0.1.12

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/hash.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Autogenerated by scripts/build.mjs from skills/. Do not edit.
2
- export const SKILLS_HASH = '5bc567ac3a7803c0'
2
+ export const SKILLS_HASH = 'b97470e51b8846e3'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/skills",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Skills for the QV.AC app — the SKILL.md tree plus a content-addressed bundle of it.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,127 +1,162 @@
1
1
  ---
2
2
  name: excel
3
- description: Create, edit, or read Excel spreadsheets (.xlsx) with openpyxl — deliver workbooks as chat attachments, or read an attached one to summarize it or answer questions in the chat. Computes values in Python; can also write live formulas and embed images. Opens in Numbers and Google Sheets too.
3
+ description: Create, edit, or read Excel spreadsheets (.xlsx) — deliver workbooks as chat attachments, or read an attached one to summarize it or answer questions in the chat. Writes values and live formulas and can embed images. Opens in Numbers and Google Sheets too.
4
4
  aliases: [xlsx, spreadsheet, workbook]
5
- tools: [exec(python)]
6
- platform: [darwin, linux, win32]
7
- # Routing tuned on Qwen3.5-4B for QVAC-24106 ("@excel embed this image into a spreadsheet" after a
8
- # generate_image turn). The bullet list sent 13/14 runs to edit.md, and 6/14 ended with no image in
9
- # a workbook, most by asking the user to attach one. As a request -> file table: 5/5 to create.md and
10
- # 5/5 embedded the image; "add a fourth row" to a workbook built in the chat still loads edit.md, 3/3.
11
- metadata:
12
- {
13
- "openclaw":
14
- {
15
- "setup":
16
- {
17
- "summary": "Runs openpyxl in the in-process Python runtime, from packages that ship with the app. The first use waits for the runtime to start."
18
- }
19
- }
20
- }
5
+ tools: [xlsx]
6
+ platform: [darwin, linux, win32, ios, android]
21
7
  ---
22
8
 
23
9
  # Excel
24
10
 
25
- Build or edit an `.xlsx` workbook by running openpyxl through the `exec` tool with
26
- `language: "python"`. Declare the workbook in `outputs` and it comes back as a chat
27
- attachment the user can save. To answer *from* a workbook instead of building one,
28
- run a read call — no `outputs` — and reply in the chat.
11
+ Every workbook job is one or two `xlsx` tool calls. A call with `ops` saves a
12
+ new file named `output` and attaches it to the chat; a call without `ops`
13
+ changes nothing and returns what the workbook holds.
14
+
15
+ ## Pick the Call
16
+
17
+ | The user wants | The calls |
18
+ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
19
+ | "make a spreadsheet of …", "embed this image into a spreadsheet" — no `.xlsx` here yet | one call: `create` + `ops` + `output` |
20
+ | "change B2 to 125", "add this image to my budget.xlsx" — an `.xlsx` is here | one call: `attachmentId` + `ops` + `output` |
21
+ | "add a total row", "fix the wrong prices" — the change depends on what is in it | read (`attachmentId` alone), then one edit call |
22
+ | "what is the total in this sheet?", "summarize this workbook" | read (`attachmentId` alone), then reply in the chat — no second call |
23
+ | "summarize this workbook into a new file" | read, then one `create` call written from what the read returned |
24
+
25
+ A workbook already in this chat is never rebuilt with `create` — that throws
26
+ away everything the user has. Its `attachmentId` is in the tool result that
27
+ produced it, or on the user's `[Attached file …]` line:
28
+
29
+ ```
30
+ [Attached file "budget.xlsx" (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) — attachmentId: 4f9c2ab1]
31
+ ```
32
+
33
+ Copy the id verbatim. Never make one up.
34
+
35
+ **An image this chat generated is already attached.** A `generate_image` result
36
+ in any earlier turn is the image "this image" means, and its `attachmentId` is
37
+ the one to use. Never ask the user to attach an image the conversation already
38
+ has.
39
+
40
+ ## A New Workbook
41
+
42
+ ```json
43
+ {
44
+ "create": { "sheets": ["Sales"] },
45
+ "ops": [
46
+ { "op": "setCells", "sheet": "Sales", "cells": {
47
+ "A1": { "value": "Quarter", "bold": true },
48
+ "B1": { "value": "Units", "bold": true },
49
+ "C1": { "value": "Price", "bold": true },
50
+ "D1": { "value": "Revenue", "bold": true }
51
+ } },
52
+ { "op": "appendRows", "sheet": "Sales", "rows": [
53
+ ["Q1", 120, 9.5, "=B2*C2"],
54
+ ["Q2", 135, 9.5, "=B3*C3"],
55
+ ["Q3", 150, 10, "=B4*C4"],
56
+ ["Total", "=SUM(B2:B4)", null, "=SUM(D2:D4)"]
57
+ ] },
58
+ { "op": "setColumnWidth", "sheet": "Sales", "column": "A", "width": 14 }
59
+ ],
60
+ "output": "sales.xlsx"
61
+ }
62
+ ```
63
+
64
+ ## Values and Formulas
65
+
66
+ Text that starts with `=` is a formula. Nothing computes it here — the
67
+ spreadsheet app does, the moment the user opens the file. So:
68
+
69
+ - For a total, an average, or anything derived from other cells, write the
70
+ formula (`"=SUM(D2:D4)"`). It stays right when the user edits the numbers.
71
+ - For numbers the user gave you or that you know, write the number.
72
+ - A formula cell shows no value in the chat preview. Say so in the reply: "the
73
+ totals are live formulas, so they appear when you open the file."
74
+
75
+ ## Reading a Workbook
76
+
77
+ ```json
78
+ { "attachmentId": "4f9c2ab1" }
79
+ ```
80
+
81
+ It returns every sheet — its name and used range, its rows of values, and a map
82
+ of the formula in each formula cell. A formula's value is the result the file
83
+ last saved, or `null` if nothing computed it yet — a `null` total is not missing
84
+ data: compute it from the rows the formula names. Dates come back as ISO text.
85
+ You cannot write a summary or a fix in the same call that reads, because the
86
+ words are fixed before the workbook is opened. Read, then write from what came
87
+ back. If the result says it was cut short, answer from what came back and say
88
+ the answer covers the workbook up to that point.
89
+
90
+ ## Editing a Workbook
91
+
92
+ ```json
93
+ {
94
+ "attachmentId": "4f9c2ab1",
95
+ "ops": [
96
+ { "op": "setCells", "sheet": "Sales", "cells": { "B3": 125, "A7": "Note", "D7": "=SUM(D3:D5)" } },
97
+ { "op": "appendRows", "sheet": "Sales", "rows": [["Q4", 80, 10.5, "=B8*C8"]] }
98
+ ],
99
+ "output": "budget_revised.xlsx"
100
+ }
101
+ ```
102
+
103
+ A `sheet` is its name or its index from 0. Cells are A1 references and rows
104
+ count from 1, as in Excel.
105
+
106
+ | The change | The op |
107
+ | -------------------------- | ------------------------------------------------------------------------------------------- |
108
+ | Set cells | `{ "op": "setCells", "sheet": "Sales", "cells": { "B3": 125, "C3": "=B3*2" } }` |
109
+ | Clear a cell | `{ "op": "setCells", "sheet": "Sales", "cells": { "B3": null } }` |
110
+ | Format a cell | `{ "op": "setCells", "sheet": "Sales", "cells": { "F4": { "value": 0.3, "format": "0.0%", "bold": true, "fill": "FFF2CC" } } }` |
111
+ | A date | `{ "op": "setCells", "sheet": "Sales", "cells": { "G4": { "date": "2026-06-30" } } }` |
112
+ | Add rows at the end | `{ "op": "appendRows", "sheet": "Sales", "rows": [["Q4", 80, 10.5]] }` |
113
+ | Insert rows | `{ "op": "insertRows", "sheet": "Sales", "at": 3, "rows": [["Q1b", 60, 9.5]] }` |
114
+ | Delete rows | `{ "op": "deleteRows", "sheet": "Sales", "at": 5, "count": 2 }` |
115
+ | Delete columns | `{ "op": "deleteColumns", "sheet": "Sales", "column": "E", "count": 1 }` |
116
+ | Column width | `{ "op": "setColumnWidth", "sheet": "Sales", "column": "A", "width": 20 }` |
117
+ | Add a sheet | `{ "op": "addSheet", "name": "Notes" }` |
118
+ | Rename a sheet | `{ "op": "renameSheet", "sheet": "Notes", "name": "Commentary" }` |
119
+ | Delete a sheet | `{ "op": "deleteSheet", "sheet": "Scratch" }` |
120
+ | Add a picture | `{ "op": "addImage", "sheet": "Sales", "image": { "attachmentId": "9be07c12" }, "cell": "F2" }` |
121
+
122
+ Renaming a sheet rewrites every formula that names it. `insertRows` and
123
+ `deleteRows` are refused on a sheet with formulas, merges or tables that would
124
+ not move with the rows — the error names which; use `setCells` and
125
+ `appendRows` there instead.
126
+
127
+ Name the output after the workbook you opened: `budget.xlsx` →
128
+ `budget_revised.xlsx`. Never the input's own name. A macro-enabled `.xlsm` is
129
+ saved as `.xlsx`: add `{ "op": "removeMacros" }` and tell the user the macros
130
+ were not kept.
131
+
132
+ ## Images
133
+
134
+ - An image this chat generated: `"image": { "attachmentId": "<id from the generate_image result>" }`.
135
+ - An image the user attached to their latest message: `"image": {}` — an
136
+ uploaded image shows no id, and the empty form picks it up.
137
+ - An image behind a URL cannot be downloaded. Say so, and ask the user to attach
138
+ it or offer `generate_image`.
29
139
 
30
- ## Load the Recipe File First
31
-
32
- This file contains no Python. The working recipes live in three reference files —
33
- load the one for the job with the `skill` tool BEFORE writing any Python, then
34
- copy its recipe and change the content:
35
-
36
- Each load is a real `skill` tool call — printing the call as JSON or text in
37
- your reply loads nothing.
38
-
39
- Pick the file by whether an `.xlsx` is already in this chat:
40
-
41
- | The request | Load |
42
- | ----------------------------------------------------------------------------------- | ----------------------------------------------- |
43
- | "make a spreadsheet of …", "embed this image into a spreadsheet" — no `.xlsx` yet | `references/create.md` |
44
- | "add a row", "change B2", "add this image to my budget.xlsx" — an `.xlsx` is here | `references/edit.md` |
45
- | "what is the total in this sheet?", "summarize this workbook" — the answer is a reply | `references/read.md` |
46
- | "summarize this workbook into a new file" | `references/read.md` first, then create or edit |
47
-
48
- Load it with the `skill` tool: `name: "excel"` and that `file`.
49
-
50
- **An image this chat generated is already attached.** A `generate_image` result in
51
- any earlier turn is the image "this image" means, and its `attachmentId` stages it.
52
- Never ask the user to attach an image the conversation already has.
140
+ ## Rules for Every Job
53
141
 
54
- Never write the Python from memory. The recipes carry required patterns (the
55
- fill-in template, staging rules, guard asserts) that fail in non-obvious ways
56
- when improvised; loading the file is one cheap read-only call.
142
+ **You build it, not the user.** The workbook exists only when an `xlsx` call
143
+ with `ops` returns its attachment. Never answer with a markdown table instead
144
+ of the file the user asked for, and never tell the user to run anything.
57
145
 
58
- ## When to Use
146
+ **Success = stop.** When the result lists the attachment, reply with one line —
147
+ the file name and the sheets and rows it holds — and call nothing else.
59
148
 
60
- - The user asks for a spreadsheet, workbook, `.xlsx`, Excel, or Numbers/Sheets-openable file.
61
- - The user attaches a spreadsheet and wants cells changed, rows/columns/sheets added or removed, or data extracted from it.
62
- - The user attaches a spreadsheet and asks what it holds — a summary, a question
63
- answered, or values pulled out into the chat.
64
- - The user wants a workbook that embeds an image — one generated in this chat or
65
- one they uploaded. Both recipe files carry it.
149
+ **Errors name the fix.** A failed call returns a message that says what exists
150
+ — the sheet names, the used range. Correct that one op and call again. Never
151
+ repeat an identical call.
66
152
 
67
153
  ## When NOT to Use
68
154
 
69
- - The user wants a table in the chat built from content already in the
70
- conversation — no workbook involved — write a markdown table. Answering or
71
- summarizing from an attached workbook **is** this skill: load
72
- `references/read.md`.
73
- - The user wants a comma-separated text file only — write the `.csv` directly with Python's `csv` module (`.csv` is an allowed output), no openpyxl needed.
74
-
75
- ## Values vs Formulas — decide before writing
76
-
77
- This runtime has no spreadsheet engine: openpyxl writes a formula as text and
78
- computes nothing. A formula cell has **no value** until the user opens the file
79
- in a spreadsheet app and it recalculates. So pick the mode from what the user
80
- wants:
155
+ - A table in the chat built from what is already in the conversation, with no
156
+ workbook involved — write a markdown table.
81
157
 
82
- - **They want numbers** (a report, totals, statistics, cleaned data): compute in
83
- Python and write **literal values**. This is the default.
84
- - **They want a live spreadsheet** (totals that update when they edit cells):
85
- write formulas — and say so in the reply, because the formula cells look empty
86
- in the chat preview: "the total is a live formula, so it shows up once you
87
- open the file in Excel, Numbers, or Sheets."
88
- - Never write a formula and then read it back expecting a number, and never
89
- "verify" a formula by reloading the file — there is nothing to verify.
90
-
91
- Writing both is fine: literal values everywhere, plus a `=SUM(...)` total row if
92
- the user wants it to stay live.
93
-
94
- ## Rules for Every Job
158
+ ## What This Cannot Do
95
159
 
96
- **You build it, not the user.** Deliver the workbook, never the recipe. Do NOT
97
- print the python source in chat, do NOT tell the user to install openpyxl, run
98
- a script, or open a terminal — they have no terminal in this chat and the code
99
- would not run there. The workbook exists only if an `exec` call with `outputs`
100
- succeeds and returns the attachment.
101
-
102
- **Success = stop.** When `exitCode` is `0` and `attachments` lists the `.xlsx`,
103
- the workbook is done — do not call `exec` again, not to "confirm", not to
104
- "improve", not to reload the file to "check the formulas". Exactly one
105
- successful *build* call per request (a no-`outputs` read that precedes a build
106
- delivers nothing and is not one of them, but it belongs before the build, never
107
- after). Reply with a single line: file name + the sheet/row summary from stdout.
108
- If the result has `missingOutputs`, read stderr first: an `AssertionError` there
109
- means a guard stopped the save on purpose and its message names what to fix;
110
- only when stderr is clean check the `save()` name matches the declared output
111
- and rerun once.
112
-
113
- **Failures are fixed in the code, not around it.** If a run fails, fix the
114
- Python against the loaded reference file's recipes and Errors table and call
115
- `exec` again. If two consecutive calls fail with the same error, re-read the
116
- traceback line-by-line before a third. An error is never a fault in openpyxl or
117
- the runtime — keep `packages: ["openpyxl==3.1.5"]`, never wrap source in
118
- `python -c` or shell, never "debug" with `os.listdir` or no-op scripts.
119
-
120
- **The runtime is sealed.** No shell (`ls`, `cat` raise `SyntaxError` — the
121
- `command` is Python source) and no network (`requests` and `urllib` fail — a
122
- URL to a spreadsheet cannot be downloaded; ask the user to attach the file).
123
- The working directory starts empty on every call: a file from an earlier call
124
- is gone unless staged again via `inputs`, and a file you write but do not
125
- declare in `outputs` is discarded.
126
-
127
- **Never overwrite a staged input.** Edits always save under a new output name.
160
+ Say so instead of faking it: charts in a workbook, pivot tables, editing
161
+ conditional formatting or data validation (both are kept as they are), and
162
+ legacy `.xls` files.
@@ -42,6 +42,21 @@ prompt says.
42
42
  Set `duration` when the user asks for a specific length ("30 seconds", "a
43
43
  two-minute track"); otherwise leave it unset and let the model choose.
44
44
 
45
+ ## Sheet music
46
+
47
+ `generate_music` can also write the finished track out as sheet music, saved as a
48
+ score attachment, with `sheetMusic: true`. Offer it before generating whenever
49
+ the request sounds like someone will play the result (a melody, a piano or guitar
50
+ piece, a practice backing) or when they mention notes, chords or a score.
51
+
52
+ Sheet music never changes the track. Generate what the user asked for: their
53
+ duration, their instrumentation, their genre. Dense or long material makes a
54
+ crowded score, so say so when the result comes back rather than quietly writing
55
+ something sparser than they asked for. When the choice is genuinely open, a
56
+ sparse 30 to 60 second piece gives a score a person can actually read.
57
+
58
+ To write out audio that already exists, that is `transcribe_music`, not this.
59
+
45
60
  ## Parameters
46
61
 
47
62
  - `prompt` (required) - describe the genre, instruments, mood, and vocal
@@ -1,110 +1,160 @@
1
1
  ---
2
2
  name: pdf
3
- description: Create PDF documents with fpdf2 or transform existing ones with pypdf — merge, split, rotate, watermark, encrypt, decrypt, fill forms, extract text — and deliver them as chat attachments.
4
- tools: [exec(python)]
5
- platform: [darwin, linux, win32]
6
- metadata:
7
- {
8
- "openclaw":
9
- {
10
- "setup":
11
- {
12
- "summary": "Runs pypdf and fpdf2 in the in-process Python runtime, from packages that ship with the app. The first use waits for the runtime to start."
13
- }
14
- }
15
- }
3
+ description: Read, answer questions about, create, merge, split, rotate, watermark, password-protect, unlock, fill in, or pull the text out of PDF files, including any attached .pdf — always this skill, never Python, for a PDF — and deliver the result as a chat attachment.
4
+ tools: [pdf]
5
+ platform: [darwin, linux, win32, ios, android]
16
6
  ---
17
7
 
18
8
  # PDF
19
9
 
20
- Build or transform `.pdf` files by running Python through the `exec` tool with
21
- `language: "python"`. Declare every produced file in `outputs` and it comes back
22
- as a chat attachment the user can save.
23
-
24
- Two libraries, split by job — pick by whether a PDF already exists:
25
-
26
- - **fpdf2** — create a new PDF from scratch: reports, letters, invoices, cheat
27
- sheets, anything laid out page by page.
28
- - **pypdf** — transform a PDF that already exists: merge, split, reorder or
29
- rotate pages, watermark, encrypt or decrypt, fill form fields, extract text.
30
-
31
- ## Load the Recipe File First
32
-
33
- This file contains no Python. The working recipes live in two reference files —
34
- load the one for the job with the `skill` tool BEFORE writing any Python, then
35
- copy its recipe and change the content:
36
-
37
- Each load is a real `skill` tool call — printing the call as JSON or text in
38
- your reply loads nothing.
39
-
40
- - **Creating a new PDF** (no existing PDF involved): call the `skill` tool with
41
- `name: "pdf"` and `file: "references/create.md"`.
42
- - **Anything with an existing PDF** (merge, split, rotate, watermark, encrypt,
43
- decrypt, fill forms, extract text): call the `skill` tool with
44
- `name: "pdf"` and `file: "references/transform.md"`.
45
- - **Adding or changing words in an existing PDF** (add a paragraph or section,
46
- reword, restyle): pypdf cannot edit page content — do not try. The job is a
47
- REBUILD: load `references/create.md` and write the whole document again with
48
- fpdf2 — every original section, unchanged, plus the requested change. When
49
- the original text is not already in this chat, load
50
- `references/transform.md` too and extract it first.
51
- - **Both in one flow** (build a page with fpdf2, then stamp it onto an existing
52
- PDF): load both files.
53
-
54
- Never write the Python from memory. The recipes carry required arguments
55
- (`new_x`/`new_y`, exact version pins, attachment staging rules) that fail in
56
- non-obvious ways when improvised; loading the file is one cheap read-only call.
57
-
58
- ## When to Use
59
-
60
- - The user asks for a new `.pdf` document.
61
- - The user attached one or more PDFs and wants them merged, split, rotated,
62
- watermarked, password-protected, unlocked, filled in, or their text pulled out.
63
- - A flow needs both: build a page with fpdf2, then stamp it onto an existing
64
- PDF with pypdf — one `exec` call can use both libraries.
10
+ Every PDF job is one or two `pdf` tool calls. A call with an `action` saves a
11
+ new file named `output` and attaches it to the chat; a call without `action`
12
+ changes nothing and returns what the PDF holds.
13
+
14
+ ## Pick the Call
15
+
16
+ | The user wants | The `pdf` call |
17
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
18
+ | A new PDF — "write a report", "make a cheat sheet" | `{ "action": "create", "markdown": "# Cat Facts\n\n...", "output": "cat_facts.pdf" }` |
19
+ | New or changed words in an existing PDF | read first, then one `create` call with the whole document |
20
+ | "Merge these PDFs", "combine them into one" | `{ "action": "merge", "attachmentIds": ["4f9c2ab1", "9be07c12"], "output": "merged.pdf" }` |
21
+ | "Keep pages 1-3", "split out page 5" | `{ "action": "split", "attachmentId": "4f9c2ab1", "pages": [1, 2, 3], "output": "report_pages_1-3.pdf" }` |
22
+ | "Delete page 3", "remove the last page" | `split` with every page to keep: a 4-page PDF without page 3 is `{ "action": "split", "attachmentId": "4f9c2ab1", "pages": [1, 2, 4], "output": "report_without_page_3.pdf" }` |
23
+ | "Rotate it", "turn page 2 sideways" | `{ "action": "rotate", "attachmentId": "4f9c2ab1", "degrees": 90, "pages": [2], "output": "report_rotated.pdf" }` |
24
+ | "Stamp DRAFT on every page", "add a watermark" | `{ "action": "watermark", "attachmentId": "4f9c2ab1", "text": "DRAFT", "output": "report_draft.pdf" }` |
25
+ | "Fill in this form" | read first, then `{ "action": "fill", "attachmentId": "4f9c2ab1", "values": { "name": "Ada Lovelace", "agree": true }, "output": "form_filled.pdf" }` |
26
+ | "Put a password on it", "lock it" | `{ "action": "encrypt", "attachmentId": "4f9c2ab1", "password": "s3cret", "output": "report_locked.pdf" }` |
27
+ | "Remove the password", "unlock it" | `{ "action": "decrypt", "attachmentId": "4f9c2ab1", "password": "s3cret", "output": "report_unlocked.pdf" }` |
28
+ | "Give me the text as a file" | `{ "action": "text", "attachmentId": "4f9c2ab1", "output": "report.txt" }` |
29
+ | What is in it — its pages, text, or form fields | `{ "attachmentId": "4f9c2ab1" }` |
30
+
31
+ - `pages` counts the way the user does: `1` is the first page. Leave `pages`
32
+ out and `rotate` and `watermark` apply to every page.
33
+ - `merge` joins the files in the order of `attachmentIds`.
34
+ - `degrees` is `90`, `180` or `270`, clockwise.
35
+ - `watermark` also takes `"opacity": 0.2` (0 to 1) and `"position": "under"`
36
+ to put the mark behind the content.
37
+ - `encrypt` locks the file with AES-256. Add `"ownerPassword": "admin"` and
38
+ `"permissions": { "print": false, "copy": false }` to restrict what a viewer
39
+ allows.
40
+ - A locked PDF needs its password on every action, not only `decrypt`:
41
+ `{ "action": "rotate", "attachmentId": "4f9c2ab1", "password": "s3cret", "degrees": 90, "output": "report_rotated.pdf" }`.
42
+ The password always comes from the user — never guess one.
43
+
44
+ ## A New PDF
45
+
46
+ ```json
47
+ {
48
+ "action": "create",
49
+ "markdown": "# Cat Facts\n\n## Senses\n\n- A cat's hearing reaches **64 kHz**.\n- Cats have about 200 million scent receptors.\n\n## Sleep\n\nCats sleep 12 to 16 hours a day, most of it in short naps.\n\n| Breed | Weight (kg) |\n| --- | --- |\n| Siamese | 4 |\n| Maine Coon | 8 |\n",
50
+ "output": "cat_facts.pdf"
51
+ }
52
+ ```
53
+
54
+ - `markdown` is the whole document: `#` headings, paragraphs, `-` and `1.`
55
+ lists, `**bold**`, `*italic*`, `>` quotes, fenced code, tables, `---` rules
56
+ and `\pagebreak`. The tool lays it out, wraps it and adds pages itself.
57
+ - **Write the whole document, not a cover page.** "A small PDF about X" still
58
+ means a title, then several short sections, each a heading plus a paragraph
59
+ or bullets. The content belongs inside the PDF, not in your chat reply.
60
+ - `create` also takes `"pageSize": "Letter"` (the default is `A4`; `A5` and
61
+ `Legal` work too), `"landscape": true` and `"pageNumbers": true`.
62
+ - Lock a new PDF only when the user asks for a password, and use the one the
63
+ user gave: add `"password"` with it. Otherwise leave `password` out.
64
+ - Western European text only: accents, curly quotes, dashes and `€` are fine.
65
+ A character outside it (Chinese, Cyrillic, emoji) fails the call and the
66
+ error names it — leave it out, or tell the user it cannot be drawn.
67
+ - A picture goes on its own line as `![caption](attachmentId)`:
68
+ `![Sales by month](9be07c12)` for an image this chat generated, or
69
+ `![Our logo](logo.png)` — the file name — for an image the user uploaded.
70
+ Only PNG and JPEG images work, and an image behind a URL cannot be
71
+ downloaded.
72
+
73
+ ## Changing the Words in an Existing PDF
74
+
75
+ A PDF's pages cannot be edited in place. Adding a paragraph, rewording, or
76
+ restyling is a REBUILD: read the PDF when its text is not already in this chat,
77
+ then one `create` call with every original section, unchanged, plus the
78
+ requested change. A rebuild that condenses, summarizes, or drops original
79
+ sections is a failed turn. Name it after the original: `report.pdf` →
80
+ `report_revised.pdf`.
81
+
82
+ **Keep the original pages.** Write each page's text from the read and put
83
+ `\pagebreak` between the pages, so the rebuild has the original page count. A
84
+ page the user asks to add goes where they asked, after its own `\pagebreak`.
85
+ A 3-page report with a line fixed on page 2:
86
+
87
+ ```json
88
+ {
89
+ "action": "create",
90
+ "markdown": "# Annual Report\n\nPage 1 text, unchanged.\n\n\\pagebreak\n\n## Costs\n\nPage 2 text, with the fixed line.\n\n\\pagebreak\n\n## Outlook\n\nPage 3 text, unchanged.\n",
91
+ "output": "report_revised.pdf"
92
+ }
93
+ ```
94
+
95
+ A picture cannot be added to an existing PDF's pages. Tell the user, or offer
96
+ a rebuild with the picture at the top, which keeps the words but not the
97
+ original layout.
98
+
99
+ ## Attachment Ids
100
+
101
+ The id comes from the tool result that produced the PDF, or from the user's
102
+ `[Attached file …]` line:
103
+
104
+ ```
105
+ [Attached file "contract.pdf" (application/pdf) — attachmentId: 4f9c2ab1]
106
+ ```
107
+
108
+ Copy the id verbatim. Never make one up. If no id for the PDF is in the chat,
109
+ ask the user to attach it again.
110
+
111
+ ## Reading a PDF
112
+
113
+ ```json
114
+ { "attachmentId": "4f9c2ab1" }
115
+ ```
116
+
117
+ It returns the page count, each page's text, and the form fields — each with
118
+ its `name`, `type`, current `value`, and the `options` a choice field accepts.
119
+ You cannot fill a form or write a summary in the same call that reads, because
120
+ the values are fixed before the PDF is opened. Read, then act on what came
121
+ back.
122
+
123
+ - **Filling a form** — `values` keys are the exact field `name`s from the
124
+ read. A checkbox takes `true` or `false`; a radio group or dropdown takes one
125
+ of its `options`. A PDF with no fields has no form layer: say so instead of
126
+ guessing where the text goes.
127
+ - **A page with empty text is a scanned image.** There is no OCR here — tell
128
+ the user those pages are images rather than calling again.
129
+ - **A question about an attached PDF** ("summarize this", "what does it say
130
+ about X") — answer from the document text already in the chat. Read only
131
+ when that text is missing.
65
132
 
66
133
  ## When NOT to Use
67
134
 
68
- - A `create_pdf` tool is available in this chat and the user only wants prose
69
- you are writing turned into a downloadable document — call `create_pdf` with
70
- the markdown instead; it is cheaper and handles layout itself. Reach for this
71
- skill when that tool is absent on this device, when an existing PDF is
72
- involved, or when the layout must be controlled page by page.
73
- - The user asks about an attached PDF — answer from the document text or
74
- knowledge excerpts already in the chat. Run text extraction only when the
75
- user wants the text itself delivered as a file.
76
135
  - The user wants a slide deck — use the presentations skill; a spreadsheet or
77
- Word file — this skill cannot read or write Office formats.
136
+ Word file — the excel or word skill.
78
137
  - The user wants a single image — call `generate_image` alone.
79
138
 
80
139
  ## Rules for Every Job
81
140
 
82
- **You build it, not the user.** Deliver the file, never the recipe. Do NOT
83
- print the Python source in chat, do NOT tell the user to install pypdf, run a
84
- script, or open a terminal — they have no terminal in this chat and the code
85
- would not run there. The PDF exists only if an `exec` call with `outputs`
86
- succeeds and returns the attachment.
87
-
88
- **Success = stop.** When `exitCode` is `0` and the result's `attachments` lists
89
- the `.pdf`, the job is done. Do not call `exec` again for the same request —
90
- not to "confirm", not to "improve". Reply with a single line: file name + the
91
- page count from stdout. If the result has `missingOutputs` instead, the file
92
- was never written: check the save name matches the declared output and rerun
93
- once.
94
-
95
- **Failures are fixed in the code, not around it.** If a run fails, fix the
96
- Python against the loaded reference file's recipes and Errors table and call
97
- `exec` again. If two consecutive calls fail with the same error, re-read the
98
- traceback line-by-line before a third. An error is never a fault in pypdf,
99
- fpdf2, or the runtime — do not switch package versions, do not wrap source in
100
- `python -c` or shell, and do not "debug" with `os.listdir` or no-op scripts
101
- while `outputs` still lists the PDF.
102
-
103
- **The runtime is sealed.** There is no shell (`ls`, `cat` raise
104
- `SyntaxError` — the `command` is Python source) and no network — `requests`,
105
- `urllib`, and `socket` all fail, so a PDF behind a URL cannot be downloaded;
106
- ask the user to attach the file. The working directory starts empty on every
107
- call: a file from an earlier call is gone unless staged again, and a file you
108
- write but do not declare in `outputs` is discarded.
109
-
110
- **Never overwrite a staged input.** Transforms always write a new output name.
141
+ **You build it, not the user.** The PDF exists only when a call returns its
142
+ attachment. Never write it as chat text instead, and never tell the user to
143
+ run anything.
144
+
145
+ **Success = stop.** When the result lists the attachment, your whole answer is
146
+ the result's `reply` line — the file name and the page count — and you call
147
+ nothing else.
148
+
149
+ **Never overwrite the input.** Name the output after the file you opened:
150
+ `report.pdf` → `report_rotated.pdf`. Never the input's own name.
151
+
152
+ **Errors name the fix.** A failed call says what is wrong — a page out of
153
+ range and how many pages there are, a field name and the fields that exist, a
154
+ value and the options it takes, a wrong or missing password, a character the
155
+ font cannot draw. Correct that one argument and call again. Never repeat an
156
+ identical call, and never switch to exec, Python or the shell for a PDF job:
157
+ the `pdf` tool is the only way this skill works.
158
+
159
+ **No network.** A PDF behind a URL cannot be downloaded — ask the user to
160
+ attach the file.