@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.
- package/LICENSE.md +198 -0
- package/README.md +40 -0
- package/index.d.ts +4 -0
- package/index.js +9 -0
- package/package.json +87 -1
- package/skills/apple-notes/SKILL.md +92 -0
- package/skills/apple-notes/append-note.applescript +9 -0
- package/skills/apple-notes/cli.schema.json +32 -0
- package/skills/apple-notes/create-note.applescript +15 -0
- package/skills/apple-notes/delete-note.applescript +10 -0
- package/skills/apple-notes/edit-note.applescript +10 -0
- package/skills/apple-notes/read-note.applescript +28 -0
- package/skills/apple-notes/references/read.md +65 -0
- package/skills/apple-notes/references/write.md +105 -0
- package/skills/apple-notes/search-notes.applescript +21 -0
- package/skills/apple-reminders/SKILL.md +129 -0
- package/skills/apple-reminders/cli.schema.json +201 -0
- package/skills/apple-reminders/references/edit.md +69 -0
- package/skills/apple-reminders/references/view.md +58 -0
- package/skills/asana/SKILL.md +59 -0
- package/skills/diagrams/SKILL.md +107 -0
- package/skills/diagrams/references/class.md +29 -0
- package/skills/diagrams/references/er.md +27 -0
- package/skills/diagrams/references/flowchart.md +33 -0
- package/skills/diagrams/references/gantt.md +38 -0
- package/skills/diagrams/references/mindmap.md +35 -0
- package/skills/diagrams/references/pie.md +27 -0
- package/skills/diagrams/references/sequence.md +32 -0
- package/skills/diagrams/references/state.md +30 -0
- package/skills/diagrams/references/timeline.md +28 -0
- package/skills/excel/SKILL.md +120 -0
- package/skills/excel/references/create.md +374 -0
- package/skills/excel/references/edit.md +353 -0
- package/skills/excel/references/read.md +99 -0
- package/skills/github/SKILL.md +42 -0
- package/skills/gmail/SKILL.md +142 -0
- package/skills/gmail/operations.json +71 -0
- package/skills/google-calendar/SKILL.md +139 -0
- package/skills/google-calendar/operations.json +62 -0
- package/skills/google-docs/SKILL.md +74 -0
- package/skills/google-docs/operations.json +61 -0
- package/skills/google-docs/references/create.md +97 -0
- package/skills/google-docs/references/edit.md +146 -0
- package/skills/google-docs/references/read.md +49 -0
- package/skills/google-drive/SKILL.md +118 -0
- package/skills/google-drive/operations.json +40 -0
- package/skills/google-sheets/SKILL.md +71 -0
- package/skills/google-sheets/operations.json +85 -0
- package/skills/google-sheets/references/create.md +54 -0
- package/skills/google-sheets/references/edit.md +124 -0
- package/skills/google-sheets/references/read.md +74 -0
- package/skills/image-generation/SKILL.md +48 -0
- package/skills/music-generation/SKILL.md +76 -0
- package/skills/notion/SKILL.md +61 -0
- package/skills/notion/operations.json +53 -0
- package/skills/notion/references/comments.md +65 -0
- package/skills/notion/references/databases.md +68 -0
- package/skills/notion/references/pages.md +119 -0
- package/skills/notion/references/tasks.md +28 -0
- package/skills/obsidian/SKILL.md +122 -0
- package/skills/obsidian/cli.schema.json +392 -0
- package/skills/obsidian/references/read.md +79 -0
- package/skills/obsidian/references/write.md +67 -0
- package/skills/pdf/SKILL.md +110 -0
- package/skills/pdf/references/create.md +169 -0
- package/skills/pdf/references/transform.md +270 -0
- package/skills/pdf/scripts/decrypt.py +26 -0
- package/skills/pdf/scripts/encrypt.py +25 -0
- package/skills/pdf/scripts/extract_text.py +25 -0
- package/skills/pdf/scripts/merge.py +21 -0
- package/skills/pdf/scripts/rotate.py +27 -0
- package/skills/presentations/SKILL.md +118 -0
- package/skills/presentations/references/create.md +399 -0
- package/skills/presentations/references/edit.md +314 -0
- package/skills/presentations/references/read.md +127 -0
- package/skills/spotify/SKILL.md +86 -0
- package/skills/weather/SKILL.md +33 -0
- package/skills/word/SKILL.md +141 -0
- package/skills/word/references/create.md +368 -0
- package/skills/word/references/edit.md +704 -0
- package/skills/word/references/read.md +141 -0
- package/skills/word/references/replace.md +86 -0
- package/skills/word/scripts/list_paragraphs.py +19 -0
- 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.
|