@qvac/skills 0.0.0 → 0.1.0

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 +49 -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,314 @@
1
+ # Editing an Existing Deck (python-pptx)
2
+
3
+ Editing means opening the deck that already exists and changing only what the
4
+ user asked for.
5
+
6
+ **The first line of an edit is always `Presentation("<staged path>")`.** A bare
7
+ `Presentation()` is only ever for a brand-new deck — it opens the bundled blank
8
+ template, not the user's file, so retyping the slides regenerates their text and
9
+ throws away the original content and design. A rebuilt deck is a failed turn.
10
+ Stage the deck as an input by its real `attachmentId` and open **that staged
11
+ file**. If no attachment id for the deck is available, ask the user to attach it
12
+ again.
13
+
14
+ ## Staging the deck
15
+
16
+ The id comes from wherever the deck entered the chat: the `exec` result that
17
+ delivered it, or — for a deck the **user uploaded** — the `[Attached file …]`
18
+ line on their message, which names every non-image upload:
19
+
20
+ ```
21
+ [Attached file "quarterly.pptx" (application/vnd.openxmlformats-officedocument.presentationml.presentation) — attachmentId: 4f9c2ab1]
22
+ ```
23
+
24
+ Copy that id verbatim — never placeholders like `att_deck` or any id you made
25
+ up. A `.pptx` is never staged id-less: the id-less form resolves to an uploaded
26
+ *image*, so it cannot reach a deck. An attachment from an earlier turn can be
27
+ used when its attachment id is available in the conversation — from a tool
28
+ result or an `[Attached file …]` line. Otherwise, ask the user to attach the
29
+ file again.
30
+
31
+ ## Read first, then edit
32
+
33
+ **An edit that writes new prose is two `exec` calls, in this order:** a **read**
34
+ that stages the deck, prints what is on the slides and declares **no `outputs`**;
35
+ then the **edit** that stages the same deck, makes the change and declares the
36
+ output.
37
+
38
+ The read has to be its own call, because one call cannot inform itself. The words
39
+ you put on a new slide are in the source you submit — fixed before the program
40
+ runs — so a `print` in that same program reports the deck back to you only after
41
+ the slide was already written and saved. A single call can still *compute*
42
+ against the deck (`prs.slides[1].slide_layout`, `len(prs.slides)`), because that
43
+ is code the runtime evaluates against the real file. What it cannot do is let you
44
+ **write** from what the deck says.
45
+
46
+ A mechanical edit needs no read: a font size, a colour, a slide whose text the
47
+ user already gave you. Read first when the new text has to agree with the deck —
48
+ a conclusion, a summary, a "what changed" slide — and go straight to the edit
49
+ when it does not.
50
+
51
+ Still forbidden, and unchanged: an `exec` opened *after* the deck is delivered to
52
+ check what you sent. The result you already hold is the whole account of that
53
+ run — that is the loop `Success = stop` closes.
54
+
55
+ The read call declares **no `outputs`** — it builds nothing, it only reports:
56
+
57
+ ```json
58
+ {
59
+ "language": "python",
60
+ "packages": ["python-pptx==1.0.2"],
61
+ "inputs": [{ "attachmentId": "<id of the deck in this chat>", "path": "deck.pptx" }],
62
+ "maxOutputChars": 24000,
63
+ "command": "..."
64
+ }
65
+ ```
66
+
67
+ `maxOutputChars` raises the stdout cap so the whole deck comes back in one
68
+ result — without it stdout is capped at 8 KB. Keep the sample's 24000 (the cap's
69
+ maximum is 65536). A build call prints one line and never needs it.
70
+
71
+ The edit call is the one that builds, and there is **exactly one** of those:
72
+
73
+ ```json
74
+ {
75
+ "language": "python",
76
+ "packages": ["python-pptx==1.0.2"],
77
+ "inputs": [{ "attachmentId": "<id of the deck in this chat>", "path": "deck.pptx" }],
78
+ "outputs": ["deck-v2.pptx"],
79
+ "command": "..."
80
+ }
81
+ ```
82
+
83
+ - `packages` — pin exactly `python-pptx==1.0.2` on every call; an unpinned
84
+ install resolves a potentially different library version. This exact version
85
+ ships with the app and installs with no network; any other version has to be
86
+ downloaded, which fails on a device that is offline.
87
+ - `inputs` — the staged deck. Paths must be unique bare filenames; staged files
88
+ land in the working directory under those names — reference
89
+ `Presentation("deck.pptx")` by that name only.
90
+ - `outputs` — the file to deliver. A file you write but do not declare here is
91
+ discarded. Omit on a read call — a read builds nothing.
92
+ - `command` — the multi-line Python source, with real newline characters. Never
93
+ collapse it to one line joined by `;` — a `for`/`if`/`with` after a semicolon
94
+ is a `SyntaxError`.
95
+
96
+ Both stage the same deck by the same `attachmentId`: the working directory is
97
+ fresh on every call, so the read leaves nothing behind for the edit to reuse.
98
+
99
+ Name the output after the deck you opened: keep its stem and bump a version —
100
+ `deck.pptx` → `deck-v2.pptx`, and an edit of that one → `deck-v3.pptx`. The
101
+ shared stem reads as one document's history in the chat, and the new name leaves
102
+ the version you opened still openable. Never overwrite the staged input.
103
+
104
+ ## The read call
105
+
106
+ The read call's whole program is the loop — layout name and every line, so the
107
+ edit that follows can be written against real content:
108
+
109
+ ```python
110
+ from pptx import Presentation
111
+
112
+ prs = Presentation("deck.pptx") # the staged input — never Presentation()
113
+ for index, slide in enumerate(prs.slides):
114
+ lines = [s.text_frame.text.replace("\n", " ") for s in slide.shapes if s.has_text_frame]
115
+ print(f"{index}: [{slide.slide_layout.name}] {' | '.join(lines)}")
116
+ ```
117
+
118
+ The `replace` is load-bearing: a multi-paragraph body embeds `"\n"` between its
119
+ bullets, and an embedded newline would split one slide across several printed
120
+ lines. Flattened, every printed line is exactly one slide, starting with its
121
+ index and layout name.
122
+
123
+ It saves nothing and declares no `outputs`. Read its result before writing the
124
+ edit: the layout names decide which layout the new slide copies, and the lines
125
+ decide what it can truthfully say.
126
+
127
+ **Read the deck before you write into it.** New content has to agree with what
128
+ is already on the slides, and you cannot write a conclusion, a summary, or a
129
+ "what changed" slide from the titles alone — the titles are headings, and the
130
+ substance is in the bodies underneath them. Loop every slide and print every
131
+ shape with `shape.has_text_frame` in the **read** call, then write the edit
132
+ against what came back. A slide written from titles only reads as though it
133
+ belongs to a different deck: it restates the headings, invents specifics the
134
+ deck never claimed, and contradicts the bullets it is supposed to close.
135
+
136
+ Read through `slide.shapes` **only**. `slide.placeholders` is not a second place
137
+ to look — every placeholder is already in `slide.shapes`, the same shape reached
138
+ by a narrower door, so looping both prints the whole deck twice and doubles what
139
+ you have to read back. Nor can you dedupe your way out of it: python-pptx builds
140
+ a fresh wrapper on each access, so the title reached through `shapes` and the
141
+ title reached through `placeholders` are `==`-distinct objects over one XML
142
+ element — `in`, `is` and `set()` all fail to spot the repeat. One loop over
143
+ `slide.shapes`, guarded by `shape.has_text_frame`, is the whole read.
144
+
145
+ Print `slide.slide_layout.name`, never the layout object — `print(slide.slide_layout)`
146
+ gives `<pptx.slide.SlideLayout object at 0x…>`, which tells you nothing and leaves
147
+ the layout choice to guesswork. The name is the template's own label, like
148
+ `Title Slide`, `Title and Content`, or `Section Header`.
149
+
150
+ That same read locates the slide to change: match on the text you printed, and
151
+ edit through the shape you matched. python-pptx has no API to delete or reorder
152
+ slides — say so instead of hacking at the XML.
153
+
154
+ ## The edit call
155
+
156
+ The edit call then opens the same staged file, changes it in place, and saves
157
+ under the versioned name — never over the staged input:
158
+
159
+ ```python
160
+ from pptx import Presentation
161
+ from pptx.util import Pt
162
+ from pptx.dml.color import RGBColor
163
+
164
+ prs = Presentation("deck.pptx") # the staged input — never Presentation()
165
+ before = len(prs.slides)
166
+
167
+ # Retitle the first slide in place — every other shape keeps its text
168
+ title = prs.slides[0].shapes.title
169
+ title.text = "Why the Sky Is Blue — Revised"
170
+ title.text_frame.paragraphs[0].font.size = Pt(44)
171
+
172
+ # Recolor existing text through its paragraph font
173
+ first_body = prs.slides[1].placeholders[1].text_frame
174
+ first_body.paragraphs[0].font.color.rgb = RGBColor(0x1A, 0x73, 0xE8)
175
+
176
+ # One new slide, on the layout a comparable BODY slide uses — never the cover's
177
+ model = prs.slides[1] # a content slide; slide 0 is usually the cover
178
+ slide = prs.slides.add_slide(model.slide_layout)
179
+ slide.shapes.title.text = "What Changed"
180
+ tf = slide.placeholders[1].text_frame
181
+ tf.word_wrap = True
182
+ tf.text = "One new closing slide, nothing else touched"
183
+ tf.paragraphs[0].font.size = Pt(20)
184
+
185
+ prs.save("deck-v2.pptx") # the declared output — not deck.pptx
186
+ print(f"{before} -> {len(prs.slides)} slides")
187
+ ```
188
+
189
+ **Reuse the deck's own layout — never the blank one.** `prs.slide_layouts[…]`
190
+ indexes the *template's* layout list, and on an uploaded deck those indices mean
191
+ whatever that template says; a slide's own `.slide_layout` is the layout it is
192
+ already built on, so passing that to `add_slide` gives the new slide the same
193
+ placeholders, fonts, colours and positions as its neighbours. Choose the index
194
+ by reading the deck — pick the existing slide that most resembles the one you
195
+ are adding, a content slide for a content slide — and fill the placeholders it
196
+ hands you. The index above is that choice, not a constant: a one-slide deck has
197
+ only `prs.slides[0]`, and `prs.slides[1]` raises `IndexError`.
198
+
199
+ **Slide 0 is almost always the cover**, on a `Title Slide` layout that owns a big
200
+ centred title and a subtitle and nothing else. Copying *that* layout for a
201
+ conclusion produces a second cover page in the middle of the deck — placeholders
202
+ that fit one line, no bullet body, and title styling that shouts. Take the layout
203
+ from a slide that carries real content — typically `Title and Content` — and reach
204
+ for the cover's layout only when you are genuinely adding another cover. Reaching
205
+ for `slide_layouts[6]` (blank) and hand-placing text boxes on a themed deck
206
+ inherits none of the theme and **guarantees a visual mismatch** with the slides
207
+ beside it.
208
+
209
+ **Every length is a typed length — `Inches(...)`, `Pt(...)` or `Emu(...)`, never
210
+ a bare number** — for every `add_textbox(left, top, width, height)` argument,
211
+ every `add_picture(...)` position and size, and every margin or offset.
212
+ python-pptx reads a bare number as EMU (914400 to the inch), nothing raises, and
213
+ the deck is delivered with text crammed into the top-left corner or a box that
214
+ has no size. Do not tune the numbers — wrap them:
215
+
216
+ ```python
217
+ from pptx.util import Inches
218
+
219
+ box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1.5))
220
+ # NOT add_textbox(1, 1, 8, 2) — that is 8 EMU wide, an invisible box
221
+ ```
222
+
223
+ Size every paragraph you add (`p.font.size = Pt(20)`, as in the sample) — the
224
+ template's body placeholder inherits 28pt, so an unsized paragraph renders far
225
+ larger than intended. Font color goes through `RGBColor` with **RGB in all
226
+ caps** (never `RgbColor`), on a paragraph's font — a paragraph has no `.fill`.
227
+ For the full slide-authoring API — placeholders vs. text boxes, bullets,
228
+ formatting, backgrounds, pictures — load `references/create.md`.
229
+
230
+ If an image was staged in `inputs`, embed it in **that** single build with
231
+ `slide.shapes.add_picture` — never deliver a deck and then rebuild to add the
232
+ image. Soft-failing (`try`/`except` around the picture) and saving without it
233
+ is a failed turn, not a success.
234
+
235
+ ## Verify the slide count
236
+
237
+ **Verify an added slide by the slide count.** Print `before` and `after` as the
238
+ sample does, then read the number back: the delta has to be exactly what the user
239
+ asked for — one added slide is `2 -> 3 slides`, and `2 -> 4 slides` means the
240
+ slide got appended twice. A delta that does not match the request is a **failed
241
+ turn to diagnose, not a result to report**: find the second `add_slide` (or the
242
+ one that never ran) and rerun. Note the count can only ever grow, since there is
243
+ no API to delete a slide. This check is about slides you add — an edit that only
244
+ changes text on existing slides leaves the count flat, and that is correct.
245
+
246
+ That check lives inside the build, so it takes no extra call: the counts come
247
+ from one `print` in the same `exec` that does the edit. A delivered deck is still
248
+ never reopened to "verify" it.
249
+
250
+ **This is the one exception to `Success = stop`, and it is not a second call.**
251
+ When the edit added slides, `exitCode: 0` plus an attachment cannot tell the
252
+ edit you were asked for apart from one that fired twice or not at all — every
253
+ one of those produces both. Read the before/after count printed by that same
254
+ run before you reply. The fix is a corrected build, never an `exec` opened to
255
+ inspect what was already delivered.
256
+
257
+ That corrected build goes out under the **next** version: `deck-v3.pptx` after a
258
+ broken `deck-v2.pptx`. The name you already delivered is refused on a second
259
+ attach — `"deck-v2.pptx" was already attached this turn` — so reusing it turns a
260
+ recoverable turn into a dead one. The broken version stays in the chat either
261
+ way, so name the good file in your reply.
262
+
263
+ ## Errors
264
+
265
+ - `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or
266
+ wrong — add `["python-pptx==1.0.2"]` and rerun. Never try to install it.
267
+ - `attachment … not found in this chat` means `inputs` listed an id that is not
268
+ in this chat (often a copied placeholder like `att_deck`). Re-copy the exact
269
+ id from the tool result or the `[Attached file …]` line that names the deck;
270
+ if neither exists, ask the user to attach it again instead of retrying.
271
+ - Every line showing up twice in the read output means the loop walked
272
+ `slide.shapes` *and* `slide.placeholders`. Placeholders are already shapes, so
273
+ the second pass re-reads what the first one found. Drop it — one loop over
274
+ `slide.shapes` is the whole read.
275
+ - A read result ending in `… [truncated]` means the deck outgrew the cap:
276
+ answer from what came back and say the answer covers the deck up to that
277
+ point. Do not rerun the read — it prints the same beginning again.
278
+ - A slide count whose delta does not match the request (`2 -> 4 slides` when one
279
+ slide was asked for) means `add_slide` ran twice, even at `exitCode 0`. Read the
280
+ printed before/after count before replying — see Verify the slide count.
281
+ - A new slide that does not match the deck around it — different font, size or
282
+ colour, bullets missing — was added on the blank layout instead of the deck's
283
+ own. Read a comparable existing slide, pass its `.slide_layout` to `add_slide`,
284
+ and fill its placeholders.
285
+ - Text crammed into the top-left corner, or a shape with no visible size, means a
286
+ bare number reached an argument that required a typed length — wrap every
287
+ position and size in `Inches(...)` / `Pt(...)` / `Emu(...)` and rerun.
288
+ - `cannot import name 'RgbColor' from 'pptx.dml.color'` means the name is wrong —
289
+ use `RGBColor` (all-caps RGB). Do not catch the ImportError and save anyway.
290
+ - Never pass an absolute path to `save()`.
291
+ - `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was
292
+ collapsed — restore multi-line newlines from the sample and rerun. Underscores
293
+ in names (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`
294
+ or change the package pin.
295
+ - `outputs declare a .pptx but command does not build one` means the source never
296
+ calls `Presentation(...).save(...)` — paste the skill sample (edited for content),
297
+ not a diagnostic `os.listdir` or shell wrapper.
298
+ - On an `AttributeError` from python-pptx the API name is wrong, and on a `TypeError`
299
+ about missing positional arguments a required argument was left out — fix either
300
+ against this file's examples. Do not retry the same call, and do not switch to a shell.
301
+ - `AttributeError: 'Slide' object has no attribute 'add_picture'` (or `addpicture`,
302
+ `add_textbox`, `addtextbox`) means the call skipped `.shapes` — use
303
+ `slide.shapes.add_picture(...)` / `slide.shapes.add_textbox(...)`, never
304
+ `slide.add_*`.
305
+ - Never print the deck's bytes or base64 — stdout is capped and the file travels
306
+ through `outputs`. A build call prints only the before/after slide count line;
307
+ only a read call prints slide text.
308
+
309
+ ## Finish
310
+
311
+ When `exitCode` is `0`, `attachments` lists the `.pptx`, and the printed
312
+ before/after count matches the request, stop tool use and answer with one line:
313
+ file name + slide count from stdout. Exactly one successful build `exec` per
314
+ request — the no-`outputs` read attaches nothing and is not that call.
@@ -0,0 +1,127 @@
1
+ # Reading a Deck to Answer in Chat (python-pptx)
2
+
3
+ When the user asks what an attached `.pptx` *says* — a summary, a question
4
+ answered, specific content pulled out — the deliverable is your reply in the
5
+ chat, not a file. This is a **read request**: one no-`outputs` read call,
6
+ staged by the deck's real `attachmentId`, is the only `exec` of the turn — no
7
+ build call follows it.
8
+
9
+ **You cannot summarize in the call that reads.** The words in `command` are
10
+ fixed before the program runs, so one call cannot inform itself: any summary
11
+ written into it was written blind — recalled or invented, not read. Python only
12
+ *transports* the slides; the summarizing happens in your reply, after the
13
+ result comes back.
14
+
15
+ ## Staging the deck
16
+
17
+ The id comes from wherever the deck entered the chat: the `exec` result that
18
+ delivered it, or — for a deck the **user uploaded** — the `[Attached file …]`
19
+ line on their message, which names every non-image upload:
20
+
21
+ ```
22
+ [Attached file "quarterly.pptx" (application/vnd.openxmlformats-officedocument.presentationml.presentation) — attachmentId: 4f9c2ab1]
23
+ ```
24
+
25
+ Copy that id verbatim — never placeholders like `att_deck` or any id you made
26
+ up. A `.pptx` is never staged id-less: the id-less form resolves to an uploaded
27
+ *image*, so it cannot reach a deck. If no attachment id for the deck is
28
+ available anywhere in the chat, ask the user to attach it again — the working
29
+ directory is fresh on every call, so a file from an earlier call is gone unless
30
+ staged again by its id.
31
+
32
+ ## The exec call
33
+
34
+ The read call declares **no `outputs`** — it builds nothing, it only reports:
35
+
36
+ ```json
37
+ {
38
+ "language": "python",
39
+ "packages": ["python-pptx==1.0.2"],
40
+ "inputs": [{ "attachmentId": "<id of the deck in this chat>", "path": "deck.pptx" }],
41
+ "maxOutputChars": 24000,
42
+ "command": "..."
43
+ }
44
+ ```
45
+
46
+ - `packages` — pin exactly `python-pptx==1.0.2`; this exact version ships with
47
+ the app and installs with no network; any other version has to be downloaded,
48
+ which fails on a device that is offline.
49
+ - `inputs` — the staged deck, under a unique bare filename; reference
50
+ `Presentation("deck.pptx")` by that name only.
51
+ - `maxOutputChars` — raises the stdout cap so the whole deck comes back in one
52
+ result — without it stdout is capped at 8 KB. Keep the sample's 24000
53
+ (default 8192, max 65536). Only a read call prints slide text.
54
+ - `command` — the multi-line Python source, with real newline characters.
55
+ Never collapse it to one line joined by `;`.
56
+
57
+ ## The recipe
58
+
59
+ The read call's whole program is the loop — layout name and every line:
60
+
61
+ ```python
62
+ from pptx import Presentation
63
+
64
+ prs = Presentation("deck.pptx") # the staged input — never Presentation()
65
+ for index, slide in enumerate(prs.slides):
66
+ lines = [s.text_frame.text.replace("\n", " ") for s in slide.shapes if s.has_text_frame]
67
+ print(f"{index}: [{slide.slide_layout.name}] {' | '.join(lines)}")
68
+ ```
69
+
70
+ The `replace` is load-bearing: a multi-paragraph body embeds `"\n"` between its
71
+ bullets, and an embedded newline would split one slide across several printed
72
+ lines. Flattened, every printed line is exactly one slide, starting with its
73
+ index and layout name.
74
+
75
+ A bare `Presentation()` opens the bundled blank template, not the user's file —
76
+ the first line is always the staged path. Loop every slide and print every
77
+ shape guarded by `shape.has_text_frame`: the titles are headings, and the
78
+ substance is in the bodies underneath them.
79
+
80
+ Read through `slide.shapes` **only**. `slide.placeholders` is not a second place
81
+ to look — every placeholder is already in `slide.shapes`, the same shape reached
82
+ by a narrower door, so looping both prints the whole deck twice and doubles what
83
+ you have to read back. Nor can you dedupe your way out of it: python-pptx builds
84
+ a fresh wrapper on each access, so the title reached through `shapes` and the
85
+ title reached through `placeholders` are `==`-distinct objects over one XML
86
+ element — `in`, `is` and `set()` all fail to spot the repeat. One loop over
87
+ `slide.shapes`, guarded by `shape.has_text_frame`, is the whole read.
88
+
89
+ ## Finish: answer in the chat
90
+
91
+ **A successful read ends tool use.** When the result prints the slides, reply
92
+ with the summary or the answer as chat text. **Scale the reply to the deck**: a
93
+ summary is much shorter than what it summarizes — a handful of slides earns
94
+ three to five sentences, and only a long deck earns sections. Restating every
95
+ slide is not a summary. Do **not**:
96
+
97
+ - call `exec` again to "re-check", "read more", or read the same deck a second
98
+ time;
99
+ - build a summary `.pptx` the user never asked for — an unrequested file is a
100
+ failed turn, not a bonus.
101
+
102
+ If stdout ends with `… [truncated]`, the deck is longer than the cap: answer
103
+ from what came back and say the answer covers the deck up to that point. Do not
104
+ rerun the read — it prints the same beginning again.
105
+
106
+ If the user asks for the summary **as a file**, that is a read followed by a
107
+ build: the read call first, then one build call that writes the new deck from
108
+ the slides you actually read. The build call follows `references/create.md` —
109
+ load it; the read still declares no `outputs`.
110
+
111
+ ## Errors
112
+
113
+ - `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or
114
+ wrong — add `["python-pptx==1.0.2"]` and rerun. Never try to install it.
115
+ - `attachment … not found in this chat` means `inputs` listed an id that is not
116
+ in this chat (often a copied placeholder like `att_deck`). Re-copy the exact
117
+ id from the tool result or the `[Attached file …]` line that names the deck;
118
+ if neither exists, ask the user to attach it again instead of retrying.
119
+ - Every line showing up twice in the read output means the loop walked
120
+ `slide.shapes` *and* `slide.placeholders`. Placeholders are already shapes —
121
+ drop the second loop; one loop over `slide.shapes` is the whole read.
122
+ - A read result ending in `… [truncated]` means the deck outgrew the cap:
123
+ answer from what came back — do not rerun the read.
124
+ - `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was
125
+ collapsed — restore multi-line newlines from the sample and rerun. Underscores
126
+ in names (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`
127
+ or change the package pin.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: spotify
3
+ description: Play, search, and control music on Spotify — songs, artists, albums, playlists, and playback.
4
+ emoji: 🎵
5
+ tools: [http_request]
6
+ platform: [darwin, linux, win32, ios, android]
7
+ credentials: [spotify_access_token]
8
+ allow_list: [https://api.spotify.com/v1/]
9
+ metadata:
10
+ {
11
+ "openclaw":
12
+ {
13
+ "requires":
14
+ {
15
+ "credentials": ["spotify_access_token"],
16
+ "credentialChecks":
17
+ { "spotify_access_token": { "url": "https://api.spotify.com/v1/me" } }
18
+ }
19
+ }
20
+ }
21
+ ---
22
+
23
+ # Spotify
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**. Never invent track/album/artist URIs — search first and copy `uri` from the JSON response.
26
+
27
+ ```json
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
36
+
37
+ - 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.
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).
41
+
42
+ ## Recipe: play a song by name
43
+
44
+ 1. Search:
45
+
46
+ ```json
47
+ {
48
+ "url": "https://api.spotify.com/v1/search",
49
+ "method": "GET",
50
+ "query": { "q": "Radiohead Creep", "type": "track", "limit": 5 }
51
+ }
52
+ ```
53
+
54
+ 2. Copy `tracks.items[0].uri` into the play body:
55
+
56
+ ```json
57
+ {
58
+ "url": "https://api.spotify.com/v1/me/player/play",
59
+ "method": "PUT",
60
+ "body": { "uris": ["spotify:track:70LcF31zb1H0PyJoS1Sx1r"] }
61
+ }
62
+ ```
63
+
64
+ ## Other operations
65
+
66
+ All paths are under `https://api.spotify.com/v1`.
67
+
68
+ | Ask | Method and path |
69
+ | --------------- | ----------------------------------------------------------------------- |
70
+ | What's playing? | `GET /me/player/currently-playing` |
71
+ | Pause | `PUT /me/player/pause` |
72
+ | Resume | `PUT /me/player/play` (no body) |
73
+ | Next track | `POST /me/player/next` |
74
+ | Add to queue | `POST /me/player/queue` with `query`: `{ "uri": "spotify:track:<id>" }` |
75
+ | My playlists | `GET /me/playlists` |
76
+ | Top tracks | `GET /me/top/tracks` with `query`: `{ "time_range": "medium_term" }` |
77
+ | Recently played | `GET /me/player/recently-played` |
78
+ | List devices | `GET /me/player/devices` |
79
+
80
+ ## Notes
81
+
82
+ - Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at 5. 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
+ - 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
+ - Playback commands return **204 No Content** on success (empty body). A **204** from currently-playing means nothing is playing.
85
+ - **403** with `PREMIUM_REQUIRED` → user needs Spotify Premium. Other **403**s are usually app-scope/Development Mode limits — report and stop.
86
+ - Never print the token or the `Authorization` header, and don't claim a write succeeded without a successful response in this turn.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: weather
3
+ description: Get current weather and short forecasts for cities via wttr.in.
4
+ tools: [http_request]
5
+ platform: [darwin, linux, win32, ios, android]
6
+ allow_list: [https://wttr.in/]
7
+ ---
8
+
9
+ # Weather
10
+
11
+ Use `http_request` with wttr.in for current conditions and short forecasts (max 3 days). Pick the smallest format — the tool output is fed back as your next prompt.
12
+
13
+ ## Format guide
14
+
15
+ **Default to `?format=3` for any "what's the weather…?" / "what about …?" / single-location question.** Only escalate to a multi-day form if the user explicitly says "tomorrow", "weekend", "next N days".
16
+
17
+ - Current / casual → `?format=3` (one line, smallest)
18
+ - Today's forecast → `?1T`
19
+ - Tomorrow / weekend (2 days) → `?2T`
20
+ - Full 3-day forecast → `?T`
21
+
22
+ Every wttr.in call MUST end in one of these suffixes. **Never call `https://wttr.in/<location>` with no `?…` suffix** — the bare URL returns a multi-kilobyte response that will overflow the context.
23
+
24
+ ```json
25
+ { "url": "https://wttr.in/London?format=3", "method": "GET" }
26
+ { "url": "https://wttr.in/New+York?2T", "method": "GET" }
27
+ ```
28
+
29
+ ## Notes
30
+
31
+ - No API key. Spaces in city → `+` (e.g. `New+York`). Ask for the location if missing.
32
+ - **wttr.in caps at 3 days.** If the user asks for "next week" or longer, say so and offer `?T` (3-day grid). Options like `?7`, `format=11`, `num_of_days=` don't exist.
33
+ - Summarize in plain language. Don't claim live weather unless the request succeeded.