@qvac/skills 0.0.0 → 0.1.1

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.
Files changed (84) hide show
  1. package/LICENSE.md +198 -0
  2. package/README.md +40 -0
  3. package/index.d.ts +4 -0
  4. package/index.js +9 -0
  5. package/package.json +87 -1
  6. package/skills/apple-notes/SKILL.md +92 -0
  7. package/skills/apple-notes/append-note.applescript +9 -0
  8. package/skills/apple-notes/cli.schema.json +32 -0
  9. package/skills/apple-notes/create-note.applescript +15 -0
  10. package/skills/apple-notes/delete-note.applescript +10 -0
  11. package/skills/apple-notes/edit-note.applescript +10 -0
  12. package/skills/apple-notes/read-note.applescript +28 -0
  13. package/skills/apple-notes/references/read.md +65 -0
  14. package/skills/apple-notes/references/write.md +105 -0
  15. package/skills/apple-notes/search-notes.applescript +21 -0
  16. package/skills/apple-reminders/SKILL.md +129 -0
  17. package/skills/apple-reminders/cli.schema.json +201 -0
  18. package/skills/apple-reminders/references/edit.md +69 -0
  19. package/skills/apple-reminders/references/view.md +58 -0
  20. package/skills/asana/SKILL.md +59 -0
  21. package/skills/diagrams/SKILL.md +107 -0
  22. package/skills/diagrams/references/class.md +29 -0
  23. package/skills/diagrams/references/er.md +27 -0
  24. package/skills/diagrams/references/flowchart.md +33 -0
  25. package/skills/diagrams/references/gantt.md +38 -0
  26. package/skills/diagrams/references/mindmap.md +35 -0
  27. package/skills/diagrams/references/pie.md +27 -0
  28. package/skills/diagrams/references/sequence.md +32 -0
  29. package/skills/diagrams/references/state.md +30 -0
  30. package/skills/diagrams/references/timeline.md +28 -0
  31. package/skills/excel/SKILL.md +120 -0
  32. package/skills/excel/references/create.md +374 -0
  33. package/skills/excel/references/edit.md +353 -0
  34. package/skills/excel/references/read.md +99 -0
  35. package/skills/github/SKILL.md +42 -0
  36. package/skills/gmail/SKILL.md +142 -0
  37. package/skills/gmail/operations.json +71 -0
  38. package/skills/google-calendar/SKILL.md +139 -0
  39. package/skills/google-calendar/operations.json +62 -0
  40. package/skills/google-docs/SKILL.md +74 -0
  41. package/skills/google-docs/operations.json +61 -0
  42. package/skills/google-docs/references/create.md +97 -0
  43. package/skills/google-docs/references/edit.md +146 -0
  44. package/skills/google-docs/references/read.md +49 -0
  45. package/skills/google-drive/SKILL.md +118 -0
  46. package/skills/google-drive/operations.json +40 -0
  47. package/skills/google-sheets/SKILL.md +71 -0
  48. package/skills/google-sheets/operations.json +85 -0
  49. package/skills/google-sheets/references/create.md +54 -0
  50. package/skills/google-sheets/references/edit.md +124 -0
  51. package/skills/google-sheets/references/read.md +74 -0
  52. package/skills/image-generation/SKILL.md +48 -0
  53. package/skills/music-generation/SKILL.md +76 -0
  54. package/skills/notion/SKILL.md +61 -0
  55. package/skills/notion/operations.json +53 -0
  56. package/skills/notion/references/comments.md +65 -0
  57. package/skills/notion/references/databases.md +68 -0
  58. package/skills/notion/references/pages.md +119 -0
  59. package/skills/notion/references/tasks.md +28 -0
  60. package/skills/obsidian/SKILL.md +122 -0
  61. package/skills/obsidian/cli.schema.json +392 -0
  62. package/skills/obsidian/references/read.md +79 -0
  63. package/skills/obsidian/references/write.md +67 -0
  64. package/skills/pdf/SKILL.md +110 -0
  65. package/skills/pdf/references/create.md +169 -0
  66. package/skills/pdf/references/transform.md +270 -0
  67. package/skills/pdf/scripts/decrypt.py +26 -0
  68. package/skills/pdf/scripts/encrypt.py +25 -0
  69. package/skills/pdf/scripts/extract_text.py +25 -0
  70. package/skills/pdf/scripts/merge.py +21 -0
  71. package/skills/pdf/scripts/rotate.py +27 -0
  72. package/skills/presentations/SKILL.md +118 -0
  73. package/skills/presentations/references/create.md +399 -0
  74. package/skills/presentations/references/edit.md +314 -0
  75. package/skills/presentations/references/read.md +127 -0
  76. package/skills/spotify/SKILL.md +86 -0
  77. package/skills/weather/SKILL.md +33 -0
  78. package/skills/word/SKILL.md +141 -0
  79. package/skills/word/references/create.md +368 -0
  80. package/skills/word/references/edit.md +704 -0
  81. package/skills/word/references/read.md +141 -0
  82. package/skills/word/references/replace.md +86 -0
  83. package/skills/word/scripts/list_paragraphs.py +19 -0
  84. package/skills/word/scripts/replace_paragraphs.py +58 -0
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: presentations
3
+ description: Create, edit, or read PowerPoint (.pptx) decks with python-pptx — deliver decks as chat attachments, or read an attached one to summarize it or answer questions in the chat. Can embed images the user attached to the chat as well as images generated in it.
4
+ tools: [exec(python)]
5
+ platform: [darwin, linux, win32]
6
+ metadata:
7
+ {
8
+ "openclaw":
9
+ {
10
+ "setup":
11
+ {
12
+ "summary": "Runs python-pptx 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
+ }
16
+ ---
17
+
18
+ # Presentations
19
+
20
+ Build, edit, or read `.pptx` decks by running python-pptx through the `exec`
21
+ tool with `language: "python"`. Declare a produced deck in `outputs` and it
22
+ comes back as a chat attachment the user can save — exactly like a
23
+ `generate_image` result, the `exec` result carries
24
+ `attachments: [{ attachmentId, fileName, byteLength }]`. To answer *from* a
25
+ deck instead of building one, run a read call alone — no `outputs` — and reply
26
+ in the chat.
27
+
28
+ ## Load the Recipe File First
29
+
30
+ This file contains no Python. The working recipes live in three reference
31
+ files — load the one for the job with the `skill` tool BEFORE writing any
32
+ Python, then copy its recipe and change the content:
33
+
34
+ Each load is a real `skill` tool call — printing the call as JSON or text in
35
+ your reply loads nothing.
36
+
37
+ - **Building a new deck** (no existing deck involved; includes embedding
38
+ images generated or uploaded in this chat): call the `skill` tool with
39
+ `name: "presentations"` and `file: "references/create.md"`.
40
+ - **Editing a deck already in this chat** (retitle, recolor, add or revise
41
+ slides on a deck the user attached or a prior call built): call the `skill`
42
+ tool with `name: "presentations"` and `file: "references/edit.md"`.
43
+ - **Answering from an attached deck** (a summary, a question answered,
44
+ content pulled into the chat — no file produced): call the `skill` tool
45
+ with `name: "presentations"` and `file: "references/read.md"`.
46
+ - **A summary delivered as a new file** is a read followed by a build: load
47
+ both `references/read.md` and `references/create.md`.
48
+
49
+ Never write the Python from memory. The recipes carry required rules (typed
50
+ `Inches`/`Pt` lengths, layout reuse, the read-before-edit flow, exact version
51
+ pins, attachment staging) that fail in non-obvious ways when improvised;
52
+ loading the file is one cheap read-only call.
53
+
54
+ ## When to Use
55
+
56
+ - The user asks for a presentation, deck, slides, `.pptx`, PowerPoint, or Keynote-openable file.
57
+ - The user attaches a `.pptx` and asks what it says — a summary, a question
58
+ answered, or content pulled out into the chat.
59
+ - The user wants a slide deck that embeds images generated in this chat.
60
+ - The user attaches an image — a photo, a logo, a screenshot — and wants it on a
61
+ slide. You can see the image, and you can also stage the file it came from:
62
+ load `references/create.md` (new deck) or `references/edit.md` (existing
63
+ deck). Generating a lookalike instead is a failed turn.
64
+
65
+ ## When NOT to Use
66
+
67
+ - The user wants markdown or a document in the chat and no deck is involved —
68
+ just write it. Summarizing or answering from an attached `.pptx` **is** this
69
+ skill: load `references/read.md`.
70
+ - The user wants a single image — call `generate_image` alone.
71
+
72
+ ## Rules for Every Job
73
+
74
+ **You build it, not the user.** Deliver the deck, never the recipe. Do NOT
75
+ print the python source in chat, do NOT tell the user to install python-pptx,
76
+ run a script, or open a terminal — they have no terminal in this chat and the
77
+ code would not run there. The deck exists only if an `exec` call with
78
+ `outputs` succeeds and returns the attachment. Falling back to "here is the
79
+ script, run it yourself" is a failed turn, and so is writing slides as
80
+ markdown/chat text instead of the `.pptx` the user asked for — answering a
81
+ *read* request as chat text is the read flow's finish, not this failure.
82
+
83
+ **Success = stop.** When `exitCode` is `0` and `attachments` lists the
84
+ `.pptx`, the deck is done. Do not call `exec` again for the same request —
85
+ not to "confirm", not to "improve", not a second identical build; a second
86
+ deck attach is rejected. Exactly one successful **build** `exec` per deck
87
+ request — a no-`outputs` read delivers nothing and is not one of them: it
88
+ belongs before the build on an edit, never after it, and on a read request it
89
+ stands alone. Reply with a single line: file name + slide count from stdout.
90
+ If the result has `missingOutputs` instead, the file was never written: check
91
+ the `save()` name matches the declared output and rerun once.
92
+
93
+ **Failures are fixed in the code, not around it.** If a run fails, fix the
94
+ Python against the loaded reference file's recipes and errors table and call
95
+ `exec` again. If two consecutive calls fail with the same error, the fix from
96
+ the first attempt did not land — re-read the traceback line-by-line before a
97
+ third call; retrying the identical `command`, or a version with only cosmetic
98
+ changes, is a loop, not a fix. An error in your code is never a fault in
99
+ python-pptx or in the runtime: do not switch package pins or hunt a "more
100
+ compatible" version — keep `packages: ["python-pptx==1.0.2"]` — do not wrap
101
+ source in `python -c "…"`, `python3`, `pip`, or shell, and do not "debug" with
102
+ `os.listdir`, `print`, or a non-deck script while `outputs` still lists the
103
+ deck. Never search the web about an error; the answer is always in the result
104
+ you already have.
105
+
106
+ **The runtime is sealed.** There is no shell — `ls`, `cat` and `file` raise
107
+ `SyntaxError`, because the `command` is Python source — and no filesystem to
108
+ check outside the `exec` result. There is no network: `requests`, `urllib`,
109
+ and `socket` all fail, and `http_request` returns truncated text, never image
110
+ bytes — an image behind a URL cannot be downloaded; ask the user to attach it
111
+ or offer `generate_image`. The working directory starts empty on every call:
112
+ a file from an earlier call is gone unless staged again by its attachment id,
113
+ and a file you write but do not declare in `outputs` is discarded.
114
+
115
+ **Never overwrite a staged input.** An edit always saves under a new,
116
+ version-bumped name — `deck.pptx` → `deck-v2.pptx` — and a corrected rebuild
117
+ after a broken delivery goes out under the next version, never the name
118
+ already attached this turn.
@@ -0,0 +1,399 @@
1
+ # Building a New Deck (python-pptx)
2
+
3
+ Create a new `.pptx` from scratch by running Python through the `exec` tool.
4
+ A new deck needs **no** `inputs` — do not invent attachment ids. **Exactly
5
+ one** `exec` call per user request when that call succeeds.
6
+
7
+ **A deck that already exists in this chat is never rebuilt here.** "Add a
8
+ slide", "change a title", "revise the deck" — any request that starts from an
9
+ existing `.pptx` is an EDIT: load `references/edit.md` and stage the deck by
10
+ its `attachmentId`. Building a fresh deck for an edit request throws away
11
+ every slide the user already has.
12
+
13
+ ## The exec call
14
+
15
+ ```json
16
+ {
17
+ "language": "python",
18
+ "packages": ["python-pptx==1.0.2"],
19
+ "outputs": ["deck.pptx"],
20
+ "command": "..."
21
+ }
22
+ ```
23
+
24
+ - `language` (required) — always `"python"`.
25
+ - `packages` (required) — `["python-pptx==1.0.2"]` on every call. Pin the
26
+ version; an unpinned install resolves a potentially different library
27
+ version. This exact version ships with the app and installs with no network;
28
+ any other version has to be downloaded, which fails on a device that is
29
+ offline.
30
+ - `outputs` — `["deck.pptx"]`. `save("deck.pptx")` must match the declared
31
+ output name exactly. A file you write but do not declare here is discarded.
32
+ - `command` — the multi-line Python source, with real newline characters.
33
+ Never collapse it to one line joined by `;` — a `for`/`if`/`with` after a
34
+ semicolon is a `SyntaxError`. `command` is the program: its first line is
35
+ the first line of Python that runs. There is no shell and no interpreter to
36
+ invoke, and no installer — packages are declared in `packages`.
37
+ - No `inputs` key at all for a new deck.
38
+ - `maxOutputChars` (stdout cap, default 8192, max 65536) is never needed on a
39
+ build call — it prints one line.
40
+
41
+ ## Embedding images
42
+
43
+ Two kinds of image input, told apart by where the image came from:
44
+
45
+ **Tool-produced files** (`generate_image` output, a prior deck return): stage
46
+ them with the exact `attachmentId` from the tool result — never placeholders
47
+ like `att_deck`, `att_image`, or any id you made up.
48
+
49
+ **Images the user uploaded** ("use this image", a photo attached to their
50
+ message): there is no id to copy — an uploaded image shows none. Stage them
51
+ with `path` only and **no `attachmentId` key**; the first id-less entry is the
52
+ first image of the user's latest message, the second is its second image, and
53
+ so on — never more id-less entries than that message has images. When it has
54
+ none, a single id-less entry resolves to the chat's most recent image instead.
55
+
56
+ Seeing the image in your context is not the same as staging it: the deck is
57
+ built by Python, which reads the working directory and never your context, so
58
+ an uploaded image reaches a slide only through an id-less `inputs` entry. Do
59
+ not call `generate_image` to recreate what the user attached, and do not tell
60
+ them the image cannot be used — the id-less entry is how it is used.
61
+
62
+ **Files the user uploaded that are not images** (a `.pptx` to revise, any
63
+ document): these *do* show an id, on the `[Attached file "…" — attachmentId:
64
+ …]` line of the message that carried them. Copy it verbatim into
65
+ `attachmentId`, exactly as for a tool-produced file. The id-less form never
66
+ reaches them. (Revising an existing deck is its own flow — load
67
+ `references/edit.md`.)
68
+
69
+ ```json
70
+ {
71
+ "language": "python",
72
+ "packages": ["python-pptx==1.0.2"],
73
+ "inputs": [
74
+ { "attachmentId": "<id from generate_image or prior deck>", "path": "slide1.png" },
75
+ { "path": "uploaded.png" }
76
+ ],
77
+ "outputs": ["deck.pptx"],
78
+ "command": "..."
79
+ }
80
+ ```
81
+
82
+ Staged files land in the working directory under the bare `path` names —
83
+ reference `slide.shapes.add_picture("slide1.png", …)` by that name only.
84
+ Paths must be unique bare filenames. The working directory is fresh on every
85
+ call, so a file written by an earlier call is gone unless it is staged again
86
+ as a chat attachment; an attachment from an earlier turn can be used when its
87
+ attachment id is available in the conversation — from a tool result or an
88
+ `[Attached file …]` line — otherwise ask the user to attach the file again.
89
+
90
+ `attachment … not found in this chat` means you invented an id or the file is
91
+ not attached. If the file you meant is an image the user uploaded, drop the
92
+ `attachmentId` key; if it came from a tool result or an `[Attached file …]`
93
+ line, re-copy the exact id; for a new deck drop `inputs` entirely; otherwise
94
+ ask the user to re-attach.
95
+
96
+ If an image was staged in `inputs`, embed it in **that** single build with
97
+ `slide.shapes.add_picture` — never deliver a deck and then rebuild to add the
98
+ image. Soft-failing (`try`/`except` around the picture) and saving without it
99
+ is a failed turn, not a success.
100
+
101
+ ## Image URLs do not work — never download
102
+
103
+ Your Python code has **no network access**: `requests`, `urllib`, and `socket`
104
+ all fail with a network error, and `http_request` returns truncated text, never
105
+ image bytes. When the user gives an image URL, do not try to fetch it from Python
106
+ and do not retry through other tools — that is a dead end. Say the link cannot be
107
+ downloaded and ask the user to attach the image itself, or offer `generate_image`
108
+ for a similar visual. Then build the deck with the staged attachment as above.
109
+
110
+ Say it in the reply, every time. A deck that quietly ships without the image the
111
+ user linked is a failed turn: they asked for that image, and silence reads as
112
+ though it is on the slide. Name the URL you could not fetch and what you need
113
+ instead.
114
+
115
+ ## Writing the Deck
116
+
117
+ Start from this. It is a complete, working deck — a title slide and a bullet slide,
118
+ 16:9, every paragraph sized, saved under the declared output name. Copy it and change
119
+ the content; do not assemble a deck from memory.
120
+
121
+ **Keep the source multi-line.** A `for`/`if`/`with` after a semicolon is a
122
+ `SyntaxError` — paste the block with real newlines, not `stmt; for x in y: …`.
123
+
124
+ **Every length is a typed length — `Inches(...)`, `Pt(...)` or `Emu(...)`, never
125
+ a bare number.** This holds for every position and size anywhere in the deck:
126
+ both pairs of `add_textbox(left, top, width, height)`, the `left`/`top` and the
127
+ `width`/`height` of `add_picture(...)`, `prs.slide_width` and `prs.slide_height`,
128
+ table column widths and row heights, and every margin or offset.
129
+
130
+ python-pptx reads a bare number as **EMU**, and there are 914400 EMU to the inch.
131
+ So `add_textbox(0, 0, 12, 0)` is not "12 wide" — it is a box 0.000013in wide and
132
+ 0in tall, pinned to the top-left corner. Nothing raises: `exitCode` is `0`, there
133
+ is no traceback and no warning, and the deck is delivered looking broken. Write
134
+ `add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))` instead.
135
+
136
+ Recognise the symptom, because it is the only signal you get: **text crammed
137
+ into the top-left corner, or a box that has no size**, means a raw number reached
138
+ an argument that required a typed length. Do not tune the numbers — wrap them.
139
+
140
+ ```python
141
+ from pptx.util import Inches
142
+
143
+ box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1.5))
144
+ # NOT add_textbox(1, 1, 8, 2) — that is 8 EMU wide, an invisible box
145
+ ```
146
+
147
+ **Keep every underscore in API names.** `text_frame`, `add_slide`, `slide_layouts`,
148
+ `slide_width`, `word_wrap`, `add_paragraph`, `add_picture`, `add_textbox`, `PP_ALIGN`
149
+ — stripping them to `textframe` / `addslide` / `addpicture` fails. Copy identifiers
150
+ exactly as written below:
151
+
152
+ ```python
153
+ from pptx import Presentation
154
+ from pptx.util import Inches, Pt
155
+ from pptx.enum.text import PP_ALIGN
156
+
157
+ prs = Presentation()
158
+ prs.slide_width = Inches(13.333) # 16:9 is not the default
159
+ prs.slide_height = Inches(7.5)
160
+
161
+ # Title slide — layout 0 owns a title and a subtitle
162
+ slide = prs.slides.add_slide(prs.slide_layouts[0])
163
+ slide.shapes.title.text = "Why the Sky Is Blue"
164
+ slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(44)
165
+ subtitle = slide.placeholders[1].text_frame
166
+ subtitle.text = "Rayleigh scattering, in four points"
167
+ subtitle.paragraphs[0].font.size = Pt(24)
168
+
169
+ # Content slide — layout 1 owns a title and a body
170
+ slide = prs.slides.add_slide(prs.slide_layouts[1])
171
+ slide.shapes.title.text = "What Happens"
172
+ slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36)
173
+ tf = slide.placeholders[1].text_frame
174
+ tf.word_wrap = True
175
+ for index, point in enumerate([
176
+ "Sunlight arrives carrying every visible wavelength",
177
+ "Air molecules scatter short wavelengths hardest",
178
+ "Blue scatters far more than red",
179
+ "So the daytime sky reads blue in every direction",
180
+ ]):
181
+ p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()
182
+ p.text = point
183
+ p.font.size = Pt(20)
184
+
185
+ prs.save("deck.pptx") # must match the declared output exactly
186
+ print(f"{len(prs.slides)} slides")
187
+ ```
188
+
189
+ Layouts `0` and `1` own the placeholders that example writes to.
190
+
191
+ **Choosing a layout has one rule, and it depends on where the deck came from:**
192
+
193
+ - **Adding to a deck the user gave you** — reuse the layout its own slides
194
+ already use: read a comparable existing slide and pass its `.slide_layout` to
195
+ `add_slide`. That is the only way the new slide inherits the deck's theme.
196
+ That flow is `references/edit.md` — load it.
197
+ - **Building a new deck** — index the bundled template, which commonly uses `0`
198
+ (title), `1` (title + content), `5` (title only), and `6` (blank). Those
199
+ indices belong to *that* template and mean nothing on an uploaded deck.
200
+
201
+ A layout only owns the placeholders it declares, and python-pptx returns `None` for
202
+ the rest — on the blank layout `shapes.title` is `None`, so `shapes.title.text = …`
203
+ raises `AttributeError: 'NoneType' object has no attribute 'text'`, and
204
+ `placeholders[1]` raises `KeyError`. So each slide is one of exactly two kinds, never
205
+ a mix:
206
+
207
+ | slide kind | layout | how you write text |
208
+ | --- | --- | --- |
209
+ | title / title + body | the deck's own layout, or `0`, `1`, `5` in a new deck | `shapes.title`, `placeholders[1]` |
210
+ | hand-designed | `6` (blank) | `shapes.add_textbox(...)` for **every** box, title included |
211
+
212
+ On layout `6` there is no title to reach for — the title is a text box you add.
213
+
214
+ The blank layout is **not** a co-equal way to write a content slide. It is for a
215
+ slide you are genuinely designing by hand — a full-bleed image, a diagram, a
216
+ custom split — and it inherits no font, size, colour or position from the
217
+ template. Using it plus `add_textbox` to hold ordinary title-and-bullets content
218
+ on a deck the user uploaded produces a slide that visibly does not belong: wrong
219
+ typeface, wrong sizes, wrong margins, and no bullets. Placeholders exist so you
220
+ do not have to reproduce a theme you cannot see.
221
+
222
+ Bullets — set `tf.text` for the first bullet, then `add_paragraph()` for the rest.
223
+ Using `add_paragraph()` for the first one leaves a blank leading line.
224
+
225
+ A new text frame holds exactly **one** paragraph, and `tf.paragraphs` is a tuple, so
226
+ `tf.paragraphs[1]` raises `IndexError: tuple index out of range` until you have added
227
+ it. Grow the frame with `p = tf.add_paragraph()`, which returns the new paragraph, and
228
+ write through that. A paragraph owns `.text`, `.font` and `.alignment` and nothing
229
+ else — it has no `.paragraphs` and no `.add_paragraph()`, so never reassign your frame
230
+ variable to a paragraph.
231
+
232
+ The template's body placeholder inherits 28pt, so size every paragraph you add —
233
+ including sub-levels — or it renders far larger than intended:
234
+
235
+ ```python
236
+ from pptx.util import Pt
237
+
238
+ slide = prs.slides.add_slide(prs.slide_layouts[1])
239
+ slide.shapes.title.text = "Agenda"
240
+ tf = slide.placeholders[1].text_frame
241
+ tf.word_wrap = True
242
+ for index, point in enumerate(["first point", "second point"]):
243
+ p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()
244
+ p.text = point
245
+ p.font.size = Pt(20) # an unsized paragraph inherits 28pt
246
+ ```
247
+
248
+ Every deck returned through `outputs` is fitted before it reaches the user, so text
249
+ that would overflow its box is shrunk to fit automatically. That is a safety net, not
250
+ a licence to overfill: shrinking below about 16pt is unreadable from a room. Budget
251
+ each slide at no more than five bullets of about 100 characters. A sixth bullet is a
252
+ second slide titled `... (cont.)`, never a smaller font — and prose belongs in the
253
+ chat reply, not on a slide.
254
+
255
+ Free text — the only way to add text outside a placeholder is
256
+ `shapes.add_textbox(left, top, width, height)` — all four are required, and all
257
+ four are typed lengths, never bare numbers — then write into its `.text_frame`.
258
+ There is no `add_text_frame`, no `add_text`, and no `add_paragraph` on `shapes`:
259
+
260
+ ```python
261
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
262
+ box = slide.shapes.add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))
263
+ tf = box.text_frame
264
+ tf.word_wrap = True
265
+ tf.text = "Why the sky is blue"
266
+ tf.paragraphs[0].font.size = Pt(40) # from pptx.util import Pt
267
+ ```
268
+
269
+ Text always lives on the `.text_frame`, never on the shape: `box.paragraphs`,
270
+ `box.add_paragraph()`, `box.word_wrap`, and `box.font` all raise AttributeError. Go
271
+ through `tf = box.text_frame` first — `tf.text`, `tf.paragraphs[0]`,
272
+ `tf.add_paragraph()`, `tf.word_wrap`. `shape.text` is the one shortcut that reads
273
+ through to the frame; there is no matching `shape.font`.
274
+
275
+ Formatting lives one level lower still — on a paragraph or a run, never on a shape or
276
+ a frame. A title is sized through its paragraph:
277
+
278
+ ```python
279
+ title = slide.shapes.title
280
+ title.text = "Why the Sky is Blue"
281
+ title.text_frame.paragraphs[0].font.size = Pt(44) # not title.font.size
282
+ title.text_frame.paragraphs[0].font.bold = True
283
+ title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
284
+ ```
285
+
286
+ Slides are added with `prs.slides.add_slide(layout)` — `prs.add_slide` does not
287
+ exist. Alignment comes from an enum import, not an attribute path:
288
+
289
+ ```python
290
+ from pptx.enum.text import PP_ALIGN
291
+
292
+ tf.paragraphs[0].alignment = PP_ALIGN.CENTER
293
+ ```
294
+
295
+ Bullet characters are not needed — a placeholder body renders bullets itself. Give
296
+ each bullet its own paragraph; never pack several `\n`-joined bullets into one.
297
+ Never type the marker into the text: `"1. "`, `"2. "`, `"- "` and `"• "` prefixes
298
+ render *next to* the bullet the placeholder already draws, in the wrong font.
299
+ Bullets belong in a body placeholder for exactly this reason — a bare
300
+ `add_textbox` draws none, and typing them by hand to compensate is the wrong fix.
301
+ Put the content in a placeholder instead.
302
+
303
+ Font color — the type is `RGBColor` with **RGB in all caps**. Not `RgbColor`,
304
+ `rgbColor`, or `rgb_color`:
305
+
306
+ ```python
307
+ from pptx.dml.color import RGBColor
308
+
309
+ title.text_frame.paragraphs[0].font.color.rgb = RGBColor(0x1A, 0x73, 0xE8)
310
+ ```
311
+
312
+ Backgrounds and fills — `background` hangs off the slide itself, never off
313
+ `slide.shapes` or a shape, and the fill is set in two steps: `solid()` first,
314
+ then the color:
315
+
316
+ ```python
317
+ from pptx.dml.color import RGBColor
318
+
319
+ fill = slide.background.fill # slide.shapes has no background
320
+ fill.solid()
321
+ fill.fore_color.rgb = RGBColor(0x0B, 0x1F, 0x3A)
322
+
323
+ box.fill.solid() # a shape is tinted through its own .fill
324
+ box.fill.fore_color.rgb = RGBColor(0xF2, 0xF2, 0xF2)
325
+ ```
326
+
327
+ A paragraph has no `.fill` at all — coloring text goes through the font,
328
+ `paragraph.font.color.rgb = RGBColor(...)`, as above. `fill` exists on a shape
329
+ and on `slide.background`, nowhere else you will need.
330
+
331
+ Images — go through the shapes collection: `slide.shapes.add_picture(...)`. A
332
+ `Slide` has no picture or text-box methods of its own — `slide.add_picture`,
333
+ `slide.addpicture`, `slide.add_textbox`, and `slide.addtextbox` all raise
334
+ `AttributeError: 'Slide' object has no attribute '…'`. Fix: put `.shapes` between
335
+ `slide` and the method. Pass only one of `width`/`height`; passing both distorts
336
+ the picture. Generate image-slide visuals at 1024×512 so they fill the content box;
337
+ a 512×512 square letterboxes with wide empty bands either side.
338
+
339
+ **Do not soft-fail images or imports.** Never wrap `add_picture` or color imports in
340
+ `try`/`except` that prints a warning and continues. A missing file or
341
+ `cannot import name 'RgbColor'` must raise so you fix it and rerun — a deck that
342
+ saves without the requested image is a failed turn, not a success.
343
+
344
+ ```python
345
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
346
+ # correct: slide.shapes.add_picture — never slide.add_picture / slide.addpicture
347
+ slide.shapes.add_picture("slide1.png", Inches(0.75), Inches(1.0), width=Inches(11.83))
348
+ prs.save("deck.pptx")
349
+ print(f"{len(prs.slides)} slides")
350
+ ```
351
+
352
+ ## Errors
353
+
354
+ - Never print the deck's bytes or base64 — stdout is capped (8 KB by default)
355
+ and the file travels through `outputs`. A build call prints only the slide
356
+ count line (e.g. `7 slides`). No "Presentation created successfully", no
357
+ try/except warnings on stdout.
358
+ - `cannot import name 'RgbColor' from 'pptx.dml.color'` means the name is wrong —
359
+ use `RGBColor` (all-caps RGB). Do not catch the ImportError and save anyway.
360
+ - Never pass an absolute path to `save()`.
361
+ - `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was
362
+ collapsed — restore multi-line newlines from the sample and rerun. Underscores
363
+ in names (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`
364
+ or change the package pin.
365
+ - `outputs declare a .pptx but command does not build one` means the source never
366
+ calls `Presentation(...).save(...)` — paste the skill sample (edited for content),
367
+ not a diagnostic `os.listdir` or shell wrapper.
368
+ - `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or
369
+ wrong — add `["python-pptx==1.0.2"]` and rerun. Never try to install it.
370
+ - `attachment … not found in this chat` means `inputs` listed an id that is not in
371
+ this chat (often a copied placeholder like `att_deck`). For a new deck, omit
372
+ `inputs` entirely and rerun. Only stage real ids from prior tool results.
373
+ - Text crammed into the top-left corner, or a shape with no visible size, means a
374
+ bare number reached an argument that required a typed length — python-pptx reads
375
+ it as EMU (914400 to the inch), so `add_textbox(0, 0, 12, 0)` is an invisible box
376
+ in the corner. Nothing raises and `exitCode` is `0`, so this only ever shows up in
377
+ the delivered deck. Wrap every position and size in `Inches(...)` / `Pt(...)` /
378
+ `Emu(...)` and rerun — do not tune the raw numbers.
379
+ - On an `AttributeError` from python-pptx the API name is wrong, and on a `TypeError`
380
+ about missing positional arguments a required argument was left out — fix either
381
+ against this file's examples. Do not retry the same call, and do not switch to a shell.
382
+ - `AttributeError: 'Slide' object has no attribute 'add_picture'` (or `addpicture`,
383
+ `add_textbox`, `addtextbox`) means the call skipped `.shapes` — use
384
+ `slide.shapes.add_picture(...)` / `slide.shapes.add_textbox(...)`, never
385
+ `slide.add_*`.
386
+ - `AttributeError: 'SlideShapes' object has no attribute 'background'` means the
387
+ background was reached through the shapes collection — it lives on the slide:
388
+ `slide.background.fill.solid()` then `fill.fore_color.rgb = RGBColor(...)`.
389
+ - `AttributeError: '_Paragraph' object has no attribute 'fill'` means a fill was
390
+ asked of text — paragraphs have none. Color text with
391
+ `paragraph.font.color.rgb = RGBColor(...)`; `.fill` belongs to a shape or to
392
+ `slide.background`.
393
+
394
+ ## Finish
395
+
396
+ When `exitCode` is `0` and `attachments` lists the `.pptx`, stop tool use and
397
+ answer with one line: file name + the slide count from stdout. Exactly one
398
+ successful build `exec` per request — re-running the same build is spam, not
399
+ quality.