@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.
- package/LICENSE.md +198 -0
- package/README.md +49 -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,353 @@
|
|
|
1
|
+
# Editing an Attached Workbook (openpyxl)
|
|
2
|
+
|
|
3
|
+
Edit a workbook that is already in this chat by running Python through the
|
|
4
|
+
`exec` tool: change cells, add or insert rows, add columns or sheets, delete
|
|
5
|
+
rows, columns, or sheets. Stage the workbook as an input, modify it, and save
|
|
6
|
+
under a **new** output name such as `revised.xlsx` — never overwrite the staged
|
|
7
|
+
input.
|
|
8
|
+
|
|
9
|
+
Macro-enabled files (`.xlsm`) can be staged as inputs and read, but this
|
|
10
|
+
runtime cannot deliver `.xlsm` back — macros never survive. Save the edit as
|
|
11
|
+
`.xlsx` and tell the user the macros were not preserved.
|
|
12
|
+
|
|
13
|
+
## Staging the Workbook
|
|
14
|
+
|
|
15
|
+
**A workbook you built earlier in this chat is edited exactly like any other
|
|
16
|
+
attachment — through its id.** The `exec` result that produced it carried
|
|
17
|
+
`attachments: [{ attachmentId, fileName, byteLength }]`; scroll back, copy that
|
|
18
|
+
`attachmentId` character for character, and stage it. The working directory is
|
|
19
|
+
wiped between calls, so a file you saved last turn is not on disk — without a
|
|
20
|
+
staged input `load_workbook("report.xlsx")` raises
|
|
21
|
+
`FileNotFoundError: [Errno 44] No such file or directory`.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"language": "python",
|
|
26
|
+
"packages": ["openpyxl==3.1.5"],
|
|
27
|
+
"inputs": [{ "attachmentId": "<the id from the earlier exec result>", "path": "existing.xlsx" }],
|
|
28
|
+
"outputs": ["revised.xlsx"],
|
|
29
|
+
"command": "..."
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**A workbook the user uploaded is staged the same way — by its id.** The
|
|
34
|
+
`[Attached file …]` line on their message names it:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
[Attached file "budget.xlsx" (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) — attachmentId: 4f9c2ab1]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Copy that id verbatim into `attachmentId`, exactly as for a workbook a tool
|
|
41
|
+
produced. This holds for **`.csv` and `.xlsm` uploads too**, not just `.xlsx`: an
|
|
42
|
+
id-less entry resolves to an uploaded *image*, so any `inputs` entry whose `path`
|
|
43
|
+
names a data or document file is rejected outright. Only an uploaded **image** is
|
|
44
|
+
staged with `path` alone and no `attachmentId` key.
|
|
45
|
+
|
|
46
|
+
Staged files land in the working directory under the bare `path` names —
|
|
47
|
+
reference `load_workbook("existing.xlsx")` by that name only.
|
|
48
|
+
`attachment … not found in this chat` means you invented an id or the file is not
|
|
49
|
+
attached. Re-copy the exact id from the `exec` result or the `[Attached file …]`
|
|
50
|
+
line that names the workbook; if no id appears anywhere in the chat, ask the
|
|
51
|
+
user to attach the file again.
|
|
52
|
+
|
|
53
|
+
`wb.save("revised.xlsx")` must match the declared output name. Keep the
|
|
54
|
+
`command` source multi-line with real newlines — never collapse it with `;`.
|
|
55
|
+
|
|
56
|
+
## Adding an Image to an Existing Workbook
|
|
57
|
+
|
|
58
|
+
Stage two files: the workbook by its `attachmentId`, and the picture. Add `"pillow"`
|
|
59
|
+
to `packages` — **deliberately unpinned**, since the runtime owns its version and a
|
|
60
|
+
pin sends openpyxl to PyPI with it.
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"language": "python",
|
|
65
|
+
"packages": ["openpyxl==3.1.5", "pillow"],
|
|
66
|
+
"inputs": [
|
|
67
|
+
{ "attachmentId": "<id of the workbook>", "path": "existing.xlsx" },
|
|
68
|
+
{ "attachmentId": "<id from the generate_image result>", "path": "photo.png" }
|
|
69
|
+
],
|
|
70
|
+
"outputs": ["revised.xlsx"],
|
|
71
|
+
"command": "..."
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`path` is a name you choose; it has nothing to do with the attachment id, and a
|
|
76
|
+
`fileName` seen in a tool result is not a file on disk. A `generate_image` result
|
|
77
|
+
anywhere in the conversation is the image the request points at — stage that id
|
|
78
|
+
rather than asking the user to attach it again.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from openpyxl.drawing.image import Image as XLImage
|
|
82
|
+
|
|
83
|
+
img = XLImage("photo.png") # the path from inputs, nothing else
|
|
84
|
+
img.width, img.height = 320, 320 # pixels
|
|
85
|
+
ws.add_image(img, "A1") # a worksheet method; A1 is the top-left corner
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
One `add_image` per workbook — inside a loop over sheets it embeds a copy per sheet.
|
|
89
|
+
Never wrap the import or the call in a `try`/`except` that saves anyway.
|
|
90
|
+
|
|
91
|
+
## The Golden Rule of Edits
|
|
92
|
+
|
|
93
|
+
**The staged sheet already has its header and all its data.** Editing never
|
|
94
|
+
re-creates them: no `HEADERS`, no `ROWS`, no copy of the create template. Append
|
|
95
|
+
only what is genuinely new, and change only the cells you were asked to change.
|
|
96
|
+
Re-appending the header and the rows writes the whole table a second time and the
|
|
97
|
+
user opens a file where every row appears twice — the create template belongs to
|
|
98
|
+
the create path (`references/create.md`) and nowhere else.
|
|
99
|
+
|
|
100
|
+
Adding one row is the whole program:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from openpyxl import load_workbook
|
|
104
|
+
|
|
105
|
+
wb = load_workbook("existing.xlsx")
|
|
106
|
+
ws = wb.active # wb["Sheet Name"] to pick another
|
|
107
|
+
|
|
108
|
+
before = ws.max_row # the sheet is already this long
|
|
109
|
+
ws.append([2026, 82500, 135000, 107500])
|
|
110
|
+
|
|
111
|
+
wb.save("revised.xlsx") # NEW name, matching the declared output
|
|
112
|
+
print(f"{before} rows in, {ws.max_row} rows out")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
That print is the check: one added row means the count goes up by exactly one. If
|
|
116
|
+
it roughly doubles, the run re-appended the existing data — fix it and rerun
|
|
117
|
+
rather than delivering a workbook with the table in it twice.
|
|
118
|
+
|
|
119
|
+
## Putting the Row Where It Belongs
|
|
120
|
+
|
|
121
|
+
Appending is right when the table has no order, and wrong when it has one: a sheet
|
|
122
|
+
running 2016…2026 with 2015 stuck on the end reads as broken. When the new row
|
|
123
|
+
belongs inside an existing order, insert it at that position instead.
|
|
124
|
+
|
|
125
|
+
You already know the position — 2015 sorts above 2016, and the data starts at row 2
|
|
126
|
+
— so write the index as a number. Do not scan, sort, or compare anything to work it
|
|
127
|
+
out:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from openpyxl import load_workbook
|
|
131
|
+
|
|
132
|
+
wb = load_workbook("existing.xlsx")
|
|
133
|
+
ws = wb.active
|
|
134
|
+
|
|
135
|
+
ROW = [2015, 278, 930, 45.5]
|
|
136
|
+
AT = 2 # 2015 goes above 2016 — row 1 is the header
|
|
137
|
+
|
|
138
|
+
assert not any(m.max_row >= AT for m in ws.merged_cells.ranges) and not any(
|
|
139
|
+
isinstance(c.value, str) and c.value.startswith("=") for r in ws.iter_rows() for c in r
|
|
140
|
+
), "a merged range at or below AT, or a formula — append instead, their ranges do not move"
|
|
141
|
+
|
|
142
|
+
before = ws.max_row
|
|
143
|
+
ws.insert_rows(AT)
|
|
144
|
+
for column, value in enumerate(ROW, start=1):
|
|
145
|
+
cell = ws.cell(row=AT, column=column, value=value)
|
|
146
|
+
cell.number_format = ws.cell(row=AT + 1, column=column).number_format
|
|
147
|
+
|
|
148
|
+
wb.save("revised.xlsx")
|
|
149
|
+
print(f"{before} rows in, {ws.max_row} rows out")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`AT` is never `1` — that would push the header down into the data.
|
|
153
|
+
|
|
154
|
+
The two lines that look optional are the ones that matter. An inserted cell starts
|
|
155
|
+
with no number format, so without the copy the new row shows a bare `278` in a
|
|
156
|
+
column of `278.00`s; taking the format from `AT + 1` uses the row that used to sit
|
|
157
|
+
there. And `insert_rows` moves cells but neither formula ranges nor merged ranges,
|
|
158
|
+
so a `=SUM(B2:B11)` total would go on summing the old span and quietly leave the
|
|
159
|
+
new row out, while a `Total` merged across `A8:B8` would stay pinned to row 8 as
|
|
160
|
+
its row slid to 9 — the assert stops both before anything is saved.
|
|
161
|
+
|
|
162
|
+
The two halves are scoped differently on purpose. A merged range is disturbed only
|
|
163
|
+
if it sits at or below `AT`, which is why the check is `m.max_row >= AT` rather than
|
|
164
|
+
"any merged cell": a title merged across `A1:C1` is untouched by an insert further
|
|
165
|
+
down, and failing on it would push you to append out of order for no reason. A
|
|
166
|
+
formula gives no such signal — one in `D1` can reference `B2:B11` — so any formula
|
|
167
|
+
at all is enough to stop the insert.
|
|
168
|
+
|
|
169
|
+
When the assert fires, do not delete it. Append the row at the end with the
|
|
170
|
+
previous template and tell the user the table kept its file order so their totals
|
|
171
|
+
stay correct.
|
|
172
|
+
|
|
173
|
+
## Other Edits — Touch Only What Changes
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from openpyxl import load_workbook
|
|
177
|
+
from openpyxl.styles import Font
|
|
178
|
+
|
|
179
|
+
wb = load_workbook("existing.xlsx")
|
|
180
|
+
ws = wb["Q1 Sales"] # or wb.active; wb.sheetnames lists them
|
|
181
|
+
|
|
182
|
+
ws.cell(row=1, column=5, value="Margin %").font = Font(bold=True)
|
|
183
|
+
for row, margin in [(2, 0.31), (3, 0.42), (4, 0.18)]:
|
|
184
|
+
cell = ws.cell(row=row, column=5, value=margin)
|
|
185
|
+
cell.number_format = '0.0%'
|
|
186
|
+
|
|
187
|
+
ws["B2"] = 150 # update a cell in place
|
|
188
|
+
|
|
189
|
+
notes = wb.create_sheet("Notes")
|
|
190
|
+
notes["A1"] = "Updated unit counts for North"
|
|
191
|
+
|
|
192
|
+
wb.save("revised.xlsx") # NEW name, matching the declared output
|
|
193
|
+
print(f"{len(wb.sheetnames)} sheets: {wb.sheetnames}")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`wb["Sheet Name"]` raises `KeyError` when the name does not exist — when unsure,
|
|
197
|
+
print `wb.sheetnames` in the same run that edits, pick from it, and never guess.
|
|
198
|
+
|
|
199
|
+
## Deleting Rows, Columns and Sheets
|
|
200
|
+
|
|
201
|
+
openpyxl deletes for real, so none of this needs XML work. `ws.delete_rows(index)`
|
|
202
|
+
and `ws.delete_cols(index)` take a **1-based** index and an optional count, so
|
|
203
|
+
`ws.delete_rows(5, 3)` drops rows 5, 6 and 7 together; a sheet goes with
|
|
204
|
+
`del wb["Notes"]`. Row 1 is the header — `delete_rows(1)` throws it away, and data
|
|
205
|
+
rows start at 2, exactly as for an insert.
|
|
206
|
+
|
|
207
|
+
**Delete from the bottom up.** Each delete shifts everything below it, so a loop
|
|
208
|
+
over ascending indices removes the wrong rows after the first: dropping rows 3 and
|
|
209
|
+
5 top-down deletes row 3, then deletes what used to be row 6. Collect the row
|
|
210
|
+
numbers first and walk them in reverse — the same rule applies right-to-left for
|
|
211
|
+
`delete_cols`:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from openpyxl import load_workbook
|
|
215
|
+
|
|
216
|
+
wb = load_workbook("existing.xlsx")
|
|
217
|
+
ws = wb.active
|
|
218
|
+
|
|
219
|
+
DROP = (2021, 2023) # the column-A values whose rows go
|
|
220
|
+
|
|
221
|
+
before = ws.max_row
|
|
222
|
+
targets = [r for r in range(2, ws.max_row + 1) if ws.cell(row=r, column=1).value in DROP]
|
|
223
|
+
assert targets, f"no row matched {DROP} — check the values are numbers, not strings"
|
|
224
|
+
|
|
225
|
+
assert not any(m.max_row >= min(targets) for m in ws.merged_cells.ranges) and not any(
|
|
226
|
+
isinstance(c.value, str) and c.value.startswith("=") for r in ws.iter_rows() for c in r
|
|
227
|
+
), "a merged range at or below the first deleted row, or a formula — their ranges do not move"
|
|
228
|
+
|
|
229
|
+
for row in reversed(targets): # bottom-up; ascending order deletes the wrong rows
|
|
230
|
+
ws.delete_rows(row)
|
|
231
|
+
|
|
232
|
+
wb.save("revised.xlsx") # NEW name, matching the declared output
|
|
233
|
+
print(f"{before} rows in, {ws.max_row} rows out")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The `assert targets` line is what stops a no-op being delivered, and it belongs
|
|
237
|
+
before the loop rather than after it. Without it, values that match nothing leave
|
|
238
|
+
the sheet untouched and the workbook still saves at `exitCode 0` with an
|
|
239
|
+
attachment indistinguishable from a real delete. With it the run raises, nothing
|
|
240
|
+
is written, and the result carries `missingOutputs` instead. The usual cause is a
|
|
241
|
+
type mismatch — the string `"2021"` is not the number `2021` — or a wrong column
|
|
242
|
+
index. **An assert that fires is a failed turn to diagnose, not a workbook to
|
|
243
|
+
deliver**: fix the match and rerun, and never delete the assert to get a file out.
|
|
244
|
+
The printed `rows in / rows out` line is then the reply line, not the check, and it
|
|
245
|
+
still costs no extra call — both live in the run that does the deleting.
|
|
246
|
+
|
|
247
|
+
The second assert is the `insert_rows` hazard in reverse, and it covers the same two
|
|
248
|
+
things with the same scoping. `delete_rows` moves cells but leaves formula text
|
|
249
|
+
alone, so a `=SUM(B2:B11)` total goes on summing eleven rows of a table that now
|
|
250
|
+
holds nine, pulling in blanks or the wrong cells. It leaves merged ranges alone too:
|
|
251
|
+
a `Total` merged at `A8:B8` keeps covering row 8 after a row above it is deleted, and
|
|
252
|
+
a title merged across `A1:C1` still claims three columns after a `delete_cols`. Both
|
|
253
|
+
are silent — no error, and the damage only shows when the user opens the file.
|
|
254
|
+
|
|
255
|
+
Merges are again checked from the first deleted row down (`m.max_row >= min(targets)`),
|
|
256
|
+
so a banner above every deletion does not block the edit, while any formula anywhere
|
|
257
|
+
does. When it fires, do not delete it: say which rows you would have removed and ask
|
|
258
|
+
whether to drop the merges and formulas too, or tell the user the deletion has to
|
|
259
|
+
happen in Excel, where the ranges follow. Note the column case is not covered by that
|
|
260
|
+
row check — if you are calling `delete_cols` on a sheet with horizontal merges, treat
|
|
261
|
+
any merged range as a stop.
|
|
262
|
+
|
|
263
|
+
## Reading Cell Values During an Edit
|
|
264
|
+
|
|
265
|
+
`load_workbook` has two modes, and neither gives both formulas and values:
|
|
266
|
+
|
|
267
|
+
- `load_workbook("f.xlsx")` — formula cells hold the formula **string**
|
|
268
|
+
(`"=SUM(D2:D4)"`).
|
|
269
|
+
- `load_workbook("f.xlsx", data_only=True)` — formula cells hold the value the
|
|
270
|
+
last spreadsheet app **cached** when it saved. A file that openpyxl itself wrote
|
|
271
|
+
has no cache, so these cells read `None`.
|
|
272
|
+
|
|
273
|
+
Plain data cells read the same either way. When a formula cell reads `None` under
|
|
274
|
+
`data_only=True`, the file was never recalculated by a spreadsheet app — compute
|
|
275
|
+
the number in Python from the data cells instead of hunting for it.
|
|
276
|
+
|
|
277
|
+
**Never `save()` a workbook opened with `data_only=True`.** That mode loads values
|
|
278
|
+
in place of formulas, so saving writes the values back and every formula the user
|
|
279
|
+
had is gone — silently, at `exitCode 0`, with an attachment that looks fine. A
|
|
280
|
+
workbook you intend to save is always opened plainly:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from openpyxl import load_workbook
|
|
284
|
+
|
|
285
|
+
values = load_workbook("existing.xlsx", data_only=True) # read numbers here
|
|
286
|
+
wb = load_workbook("existing.xlsx") # edit and save this one
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Read from `values`, write to `wb`, and save `wb`. One open, one job.
|
|
290
|
+
|
|
291
|
+
That is reading in service of an edit. Reading for the *user* — a summary or an
|
|
292
|
+
answer delivered as chat text — is its own flow with its own call shape: load
|
|
293
|
+
`references/read.md`.
|
|
294
|
+
|
|
295
|
+
## Errors
|
|
296
|
+
|
|
297
|
+
- Never print the workbook's bytes or base64 — stdout is capped and the file
|
|
298
|
+
travels through `outputs`. A build call prints only a short summary line.
|
|
299
|
+
Never pass an absolute path to `save()`.
|
|
300
|
+
- `attachment … not found in this chat` — `inputs` listed an id that is not in
|
|
301
|
+
this chat (often a copied placeholder). Only stage real ids from prior tool
|
|
302
|
+
results or `[Attached file …]` lines.
|
|
303
|
+
- `an id-less input stages an uploaded image, and this path names a document` —
|
|
304
|
+
an `.xlsx` was staged with no `attachmentId`. Spreadsheets are always staged
|
|
305
|
+
by id.
|
|
306
|
+
- `no uploaded image in this chat — attach an image or pass an attachmentId` —
|
|
307
|
+
an id-less input was sent when the user uploaded no image at all.
|
|
308
|
+
- `FileNotFoundError: [Errno 44] No such file or directory` on a workbook you
|
|
309
|
+
saved in an earlier call means it was never staged: the working directory is
|
|
310
|
+
fresh every call. Add the file to `inputs` with its `attachmentId`.
|
|
311
|
+
- A delivered workbook whose formulas have turned into blanks means it was opened
|
|
312
|
+
with `data_only=True` and then saved. Open a second, plain workbook to edit.
|
|
313
|
+
- A delivered workbook whose table appears twice means the edit re-appended the
|
|
314
|
+
header and rows onto the staged sheet. An edit adds only what is new.
|
|
315
|
+
- `AssertionError: a merged range at or below AT, or a formula — append instead …`
|
|
316
|
+
means a merged range sits at or below the insertion row, or the sheet has a formula
|
|
317
|
+
whose range `insert_rows` would not move. Append the row at the end instead and say
|
|
318
|
+
why in the reply.
|
|
319
|
+
- `AssertionError: a merged range at or below the first deleted row, or a formula …`
|
|
320
|
+
is the same hazard on a delete, and has no safe fallback: say which rows you would
|
|
321
|
+
remove and ask the user how to handle the merges and formulas.
|
|
322
|
+
- `AssertionError: no row matched …` means the delete found nothing, usually because
|
|
323
|
+
the compared values are strings on one side and numbers on the other. Diagnose the
|
|
324
|
+
match and rerun; do not remove the assert, because the workbook it would deliver
|
|
325
|
+
is the staged one unchanged.
|
|
326
|
+
- Rows that disappeared from the wrong places mean the delete loop ran over
|
|
327
|
+
ascending indices. Collect the targets first and delete in reverse.
|
|
328
|
+
- A row that renders unlike the rest of its column — `278` among `278.00`s — was
|
|
329
|
+
inserted without copying `number_format` from the row below it.
|
|
330
|
+
- `'MergedCell' object attribute 'value' is read-only` means the write hit a merged
|
|
331
|
+
non-anchor cell — write the range's top-left cell instead.
|
|
332
|
+
- `".xlsm" is not an allowed output type` means the run tried to deliver a
|
|
333
|
+
macro-enabled file — save as `.xlsx` and tell the user macros were not preserved.
|
|
334
|
+
- `KeyError` on `wb["Sheet Name"]` — the sheet name does not exist; print
|
|
335
|
+
`wb.sheetnames` in the run that edits and pick from it.
|
|
336
|
+
- `ModuleNotFoundError: No module named 'openpyxl'` — add
|
|
337
|
+
`["openpyxl==3.1.5"]` to `packages` and rerun. Never try to install it.
|
|
338
|
+
- `ImportError: You must install Pillow to fetch image objects` means an image was
|
|
339
|
+
embedded without `"pillow"` in `packages` — openpyxl does not install it.
|
|
340
|
+
- `FileNotFoundError` on a 32-character hex name means an attachment id was opened as
|
|
341
|
+
a path. The id belongs in `attachmentId`; open the `path` you chose.
|
|
342
|
+
- On an `AttributeError` from openpyxl the API name is wrong, and on a `TypeError`
|
|
343
|
+
about missing positional arguments a required argument was left out — fix either
|
|
344
|
+
against this file's examples. Do not retry the same call, and do not switch to a
|
|
345
|
+
shell.
|
|
346
|
+
|
|
347
|
+
## Finish
|
|
348
|
+
|
|
349
|
+
When `exitCode` is `0` and `attachments` lists the `.xlsx`, stop tool use and
|
|
350
|
+
answer with one line: file name + the `rows in / rows out` (or sheets) summary
|
|
351
|
+
from stdout. Exactly one successful `exec` per request. If the result has
|
|
352
|
+
`missingOutputs`, read stderr first — an `AssertionError` there means a guard
|
|
353
|
+
stopped the save on purpose and its message names what to fix.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Reading a Workbook to Answer in Chat (openpyxl)
|
|
2
|
+
|
|
3
|
+
When the user asks what an attached workbook *holds* — a summary, a question
|
|
4
|
+
answered, specific values pulled out — the deliverable is your reply in the
|
|
5
|
+
chat, not a file. This is a **read request**: exactly one `exec` call, staging
|
|
6
|
+
the workbook in `inputs` and declaring **no `outputs`**, whose whole job is to
|
|
7
|
+
print the sheets so you can read them in the result.
|
|
8
|
+
|
|
9
|
+
## The exec call
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"language": "python",
|
|
14
|
+
"packages": ["openpyxl==3.1.5"],
|
|
15
|
+
"inputs": [{ "attachmentId": "<id from the [Attached file …] line>", "path": "existing.xlsx" }],
|
|
16
|
+
"maxOutputChars": 24000,
|
|
17
|
+
"command": "..."
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The id rules: copy it verbatim from the `[Attached file …]` line on the user's
|
|
22
|
+
message or from the earlier `exec` result that produced the file, never invent
|
|
23
|
+
one, never stage a workbook id-less (an id-less entry resolves to an uploaded
|
|
24
|
+
*image*). If no id appears anywhere in the chat, ask the user to attach the
|
|
25
|
+
file again. `maxOutputChars` raises the stdout cap so a full workbook comes
|
|
26
|
+
back in one result; keep the sample's 24000. Declare no `outputs` — a read
|
|
27
|
+
builds nothing.
|
|
28
|
+
|
|
29
|
+
## The Read Program
|
|
30
|
+
|
|
31
|
+
Prints every sheet under a `[Sheet]` marker, then its rows — one printed line
|
|
32
|
+
per row:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from openpyxl import load_workbook
|
|
36
|
+
|
|
37
|
+
wb = load_workbook("existing.xlsx", data_only=True)
|
|
38
|
+
for name in wb.sheetnames:
|
|
39
|
+
ws = wb[name]
|
|
40
|
+
print(f"[Sheet] {name} ({ws.max_row} rows)")
|
|
41
|
+
for row in ws.iter_rows(values_only=True):
|
|
42
|
+
print(" | ".join("" if v is None else str(v).replace("\n", " ") for v in row))
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `replace` is load-bearing: a multi-line cell (Alt+Enter in Excel) embeds
|
|
46
|
+
`"\n"` in its value, and an embedded newline would split one row across two
|
|
47
|
+
printed lines. Flattened, every printed line is exactly one sheet row.
|
|
48
|
+
|
|
49
|
+
`data_only=True` is the right mode here: a formula cell prints the value the
|
|
50
|
+
last spreadsheet app cached, or an empty field when the file was never
|
|
51
|
+
recalculated (`None` prints as nothing). An empty field under a `Total` header
|
|
52
|
+
is that, not missing data — say so, and when the answer needs the number, work
|
|
53
|
+
it out from the data rows that did print.
|
|
54
|
+
|
|
55
|
+
**You cannot summarize in the call that reads.** The words in `command` are
|
|
56
|
+
fixed before the program runs, so one call cannot inform itself: any summary
|
|
57
|
+
written into it was written blind — recalled or invented, not read. Python only
|
|
58
|
+
*transports* the values; the summarizing happens in your reply, after the
|
|
59
|
+
result comes back.
|
|
60
|
+
|
|
61
|
+
## A Successful Read Ends Tool Use
|
|
62
|
+
|
|
63
|
+
When the result prints the sheets, reply with the summary or the answer as chat
|
|
64
|
+
text — when the user asked for the table in the chat, that reply is a markdown
|
|
65
|
+
table built from the rows you read, never from memory. **Scale the reply to the
|
|
66
|
+
workbook**: a summary is much shorter than what it summarizes — a small sheet
|
|
67
|
+
earns a few sentences, and only a many-sheet workbook earns sections. Restating
|
|
68
|
+
every row is not a summary. Do **not**:
|
|
69
|
+
|
|
70
|
+
- call `exec` again to "re-check", "read more", or read the same workbook a
|
|
71
|
+
second time;
|
|
72
|
+
- build a summary `.xlsx` the user never asked for — an unrequested file is a
|
|
73
|
+
failed turn, not a bonus.
|
|
74
|
+
|
|
75
|
+
If stdout ends with `… [truncated]`, the workbook is longer than the cap:
|
|
76
|
+
answer from what came back and say the answer covers the sheets up to that
|
|
77
|
+
point. Do not rerun the read — it prints the same beginning again.
|
|
78
|
+
|
|
79
|
+
If the user asks for the summary **as a file**, that is a read followed by a
|
|
80
|
+
build: the read call above first, then one build call that writes the new
|
|
81
|
+
workbook from the values you actually read (load `references/create.md` for the
|
|
82
|
+
build). The read still declares no `outputs`.
|
|
83
|
+
|
|
84
|
+
## Errors
|
|
85
|
+
|
|
86
|
+
- `attachment … not found in this chat` — the id was invented or the file is
|
|
87
|
+
not attached. Re-copy the exact id; if none exists, ask the user to attach
|
|
88
|
+
the file again instead of retrying.
|
|
89
|
+
- `an id-less input stages an uploaded image, and this path names a document` —
|
|
90
|
+
the workbook was staged with no `attachmentId`. Spreadsheets (`.xlsx`,
|
|
91
|
+
`.csv`, `.xlsm`) are always staged by id.
|
|
92
|
+
- `FileNotFoundError: [Errno 44] No such file or directory` — the file was
|
|
93
|
+
never staged; the working directory is fresh every call. Add it to `inputs`
|
|
94
|
+
with its `attachmentId`.
|
|
95
|
+
- `ModuleNotFoundError: No module named 'openpyxl'` — add
|
|
96
|
+
`["openpyxl==3.1.5"]` to `packages` and rerun. Never try to install it.
|
|
97
|
+
- A formula cell that prints nothing is not a bug — the file was never
|
|
98
|
+
recalculated by a spreadsheet app. Compute the number from the data rows
|
|
99
|
+
instead of rerunning.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: github
|
|
3
|
+
description: Search, read, and write GitHub repos, issues, and pull requests via the REST API.
|
|
4
|
+
tools: [http_request]
|
|
5
|
+
platform: [darwin, linux, win32, ios, android]
|
|
6
|
+
credentials: [github_access_token]
|
|
7
|
+
allow_list: [https://api.github.com/]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# GitHub
|
|
11
|
+
|
|
12
|
+
Use `http_request` against `https://api.github.com` on every call, with `headers: {"Accept": "application/vnd.github+json"}`. The GitHub PAT credential is attached automatically to every `api.github.com` request — **never include an `auth` block**. Never fetch `github.com` web pages — they return HTML, not data; translate a pasted link to its API path instead (e.g. `github.com/{owner}/{repo}/pull/{n}` → `/repos/{owner}/{repo}/pulls/{n}`).
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"url": "https://api.github.com/search/issues?q=is:pr+is:open+repo:owner/repo&per_page=5",
|
|
17
|
+
"method": "GET",
|
|
18
|
+
"headers": { "Accept": "application/vnd.github+json" }
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Reads
|
|
23
|
+
|
|
24
|
+
- Search repos → `GET /search/repositories?q=...`
|
|
25
|
+
- Search issues/PRs → `GET /search/issues?q=...` (always qualify with `is:pr` or `is:issue` — it returns both)
|
|
26
|
+
- List issues → `GET /repos/{owner}/{repo}/issues?state=open` (items with a `pull_request` key are PRs, not issues)
|
|
27
|
+
- List PRs → `GET /repos/{owner}/{repo}/pulls?state=open`
|
|
28
|
+
- Read a file → `GET /repos/{owner}/{repo}/contents/{path}` (`content` is base64-encoded)
|
|
29
|
+
- List a user's repos → `GET /user/repos?sort=updated`
|
|
30
|
+
|
|
31
|
+
## Writes
|
|
32
|
+
|
|
33
|
+
- Create an issue → `POST /repos/{owner}/{repo}/issues` with `body: {"title": "...", "body": "..."}`
|
|
34
|
+
- Comment on an issue or PR → `POST /repos/{owner}/{repo}/issues/{n}/comments` with `body: {"body": "..."}` (PRs are issues for commenting — use the PR number on the issues endpoint)
|
|
35
|
+
- Close or edit an issue → `PATCH /repos/{owner}/{repo}/issues/{n}` with `body: {"state": "closed"}`
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- Paginate with `per_page` (default small — 5, rarely above 30) and `page`; never fetch more than the request needs.
|
|
40
|
+
- Ask for specific fields where the endpoint supports it, and summarize in plain language rather than echoing raw JSON — responses get truncated past 8KB.
|
|
41
|
+
- **401** — token missing or revoked: tell the user to connect GitHub, don't retry. **404** on a resource the user linked directly usually means it's private, not nonexistent — search can't see private repos either. **403/429** mentioning rate limits — say so and stop.
|
|
42
|
+
- 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,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gmail
|
|
3
|
+
description: Read, search, send, and manage Gmail messages and labels via the Gmail REST API.
|
|
4
|
+
aliases: [inbox, email+send, email+draft, email+reply, email+forward]
|
|
5
|
+
tools: [http_request, gmail_send, gmail_draft]
|
|
6
|
+
platform: [darwin, linux, win32]
|
|
7
|
+
credentials: [gmail_access_token]
|
|
8
|
+
allow_list: [https://gmail.googleapis.com/gmail/v1/users/me/]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Gmail
|
|
12
|
+
|
|
13
|
+
Use `gmail_send` to send, `gmail_draft` to draft, and `http_request` for everything else (list, search, get, labels, trash). The Gmail credential is attached automatically to every `gmail.googleapis.com` request — **never include an `auth` block**.
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
Gmail must be connected. Each Google skill is connected separately, with its
|
|
18
|
+
own app and its own approval — connecting one grants nothing to the others. If
|
|
19
|
+
credentials are missing, tell the user to connect Gmail from Settings, or to
|
|
20
|
+
set the `gmail_access_token` credential.
|
|
21
|
+
|
|
22
|
+
## Base URL
|
|
23
|
+
|
|
24
|
+
`https://gmail.googleapis.com/gmail/v1/users/me`
|
|
25
|
+
|
|
26
|
+
The host is `gmail.googleapis.com` — not `www.googleapis.com`. Send is at `/messages/send`, never `/send`.
|
|
27
|
+
|
|
28
|
+
## Common Operations
|
|
29
|
+
|
|
30
|
+
### List or search messages
|
|
31
|
+
|
|
32
|
+
`messages.list` returns `{id, threadId}` pairs only — no subjects, no snippets. To summarize you need a follow-up `messages.get` per id. Use `q` for Gmail search syntax (`from:`, `subject:`, `is:unread`, `newer_than:7d`, `has:attachment`, `label:work`). Keep `maxResults` ≤ 10.
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"url": "https://gmail.googleapis.com/gmail/v1/users/me/messages",
|
|
37
|
+
"method": "GET",
|
|
38
|
+
"query": { "q": "is:unread newer_than:7d", "labelIds": "INBOX", "maxResults": 10 }
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Get a message (metadata)
|
|
43
|
+
|
|
44
|
+
For lists and summaries always use `format=metadata` — it skips the body and is far cheaper than `full`.
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}",
|
|
49
|
+
"method": "GET",
|
|
50
|
+
"query": {
|
|
51
|
+
"format": "metadata",
|
|
52
|
+
"metadataHeaders": "Subject,From,To,Date,Message-ID,References"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Get a message (full body)
|
|
58
|
+
|
|
59
|
+
Only when the user needs the content. The body is base64url-encoded in `payload.parts[].body.data` (or `payload.body.data`); decode it before presenting. Prefer the `text/plain` part over `text/html`.
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}",
|
|
64
|
+
"method": "GET",
|
|
65
|
+
"query": { "format": "full" }
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Send a message
|
|
70
|
+
|
|
71
|
+
Use `gmail_send` with semantic args: `to` (array), `subject`, and `text` (or `html`). Optional: `cc`, `bcc`, `replyTo`. The tool builds the RFC 2822 message and base64url-encodes it — never construct `raw` yourself.
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{ "to": ["<RECIPIENT_EMAIL>"], "subject": "Subject line", "text": "Message body" }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If you supply both `text` and `html`, only `text` is sent — pick one.
|
|
78
|
+
|
|
79
|
+
Confirm recipient, subject, and body with the user before sending. Report success only when the response contains a message `id`.
|
|
80
|
+
|
|
81
|
+
### Draft a message
|
|
82
|
+
|
|
83
|
+
Use `gmail_draft` with the same envelope as `gmail_send`.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{ "to": ["<RECIPIENT_EMAIL>"], "subject": "Subject line", "text": "Draft body" }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Reply to a message (preserves threading)
|
|
90
|
+
|
|
91
|
+
A reply is `gmail_send` with the original's `threadId`, `inReplyTo`, and `references`. Without these Gmail starts a new thread.
|
|
92
|
+
|
|
93
|
+
1. Get the original with `format=metadata` and headers `Message-ID,References,Subject,From,Reply-To`; capture its `threadId`.
|
|
94
|
+
2. Send with `to` <- original From (or Reply-To), `subject` <- `Re: ` + original (don't double-prefix), `inReplyTo` <- original Message-ID, `references` <- original References then that Message-ID. Keep angle brackets.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"to": ["<RECIPIENT_EMAIL>"],
|
|
99
|
+
"subject": "Re: Original subject",
|
|
100
|
+
"text": "Reply body",
|
|
101
|
+
"threadId": "{originalThreadId}",
|
|
102
|
+
"inReplyTo": "<msg-id@mail.gmail.com>",
|
|
103
|
+
"references": "<msg-id@mail.gmail.com>"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Modify labels (mark read, archive, star)
|
|
108
|
+
|
|
109
|
+
System labels: `INBOX`, `UNREAD`, `STARRED`, `IMPORTANT`, `SPAM`, `TRASH`. Mark read = remove `UNREAD`; archive = remove `INBOX`; star = add `STARRED`.
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}/modify",
|
|
114
|
+
"method": "POST",
|
|
115
|
+
"body": { "removeLabelIds": ["UNREAD", "INBOX"] }
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Trash a message
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}/trash",
|
|
124
|
+
"method": "POST"
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Output Policy
|
|
129
|
+
|
|
130
|
+
- Lists: up to 5 entries with subject, sender, and a human-readable date. Fetch full bodies only when asked.
|
|
131
|
+
- Decode base64 message bodies before presenting; never include raw base64 blobs.
|
|
132
|
+
- Modify/trash: state the user-facing effect ("marked 3 messages as read"), not the label diff.
|
|
133
|
+
- Confirm destructive or outgoing actions (send, reply, trash) with the user first.
|
|
134
|
+
|
|
135
|
+
## Common Mistakes
|
|
136
|
+
|
|
137
|
+
- Using `www.googleapis.com` for Gmail, or building send/draft/reply through `http_request` instead of `gmail_send`/`gmail_draft`.
|
|
138
|
+
- Treating `messages.list` results as if they had subjects — they need a follow-up `messages.get`.
|
|
139
|
+
- Replying without `threadId` + `inReplyTo`/`references` — Gmail starts a new thread.
|
|
140
|
+
- Sending a placeholder (`<RECIPIENT_EMAIL>`, `recipient@example.com`) — the runtime refuses these.
|
|
141
|
+
- Inventing an address. The runtime refuses any recipient absent from the conversation and from earlier tool results; search Gmail or ask the user.
|
|
142
|
+
- Including an `auth` block by hand — credentials attach automatically; a mistyped key breaks the request.
|