@qvac/skills 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE.md +198 -0
  2. package/README.md +40 -0
  3. package/index.d.ts +4 -0
  4. package/index.js +9 -0
  5. package/package.json +87 -1
  6. package/skills/apple-notes/SKILL.md +92 -0
  7. package/skills/apple-notes/append-note.applescript +9 -0
  8. package/skills/apple-notes/cli.schema.json +32 -0
  9. package/skills/apple-notes/create-note.applescript +15 -0
  10. package/skills/apple-notes/delete-note.applescript +10 -0
  11. package/skills/apple-notes/edit-note.applescript +10 -0
  12. package/skills/apple-notes/read-note.applescript +28 -0
  13. package/skills/apple-notes/references/read.md +65 -0
  14. package/skills/apple-notes/references/write.md +105 -0
  15. package/skills/apple-notes/search-notes.applescript +21 -0
  16. package/skills/apple-reminders/SKILL.md +129 -0
  17. package/skills/apple-reminders/cli.schema.json +201 -0
  18. package/skills/apple-reminders/references/edit.md +69 -0
  19. package/skills/apple-reminders/references/view.md +58 -0
  20. package/skills/asana/SKILL.md +59 -0
  21. package/skills/diagrams/SKILL.md +107 -0
  22. package/skills/diagrams/references/class.md +29 -0
  23. package/skills/diagrams/references/er.md +27 -0
  24. package/skills/diagrams/references/flowchart.md +33 -0
  25. package/skills/diagrams/references/gantt.md +38 -0
  26. package/skills/diagrams/references/mindmap.md +35 -0
  27. package/skills/diagrams/references/pie.md +27 -0
  28. package/skills/diagrams/references/sequence.md +32 -0
  29. package/skills/diagrams/references/state.md +30 -0
  30. package/skills/diagrams/references/timeline.md +28 -0
  31. package/skills/excel/SKILL.md +120 -0
  32. package/skills/excel/references/create.md +374 -0
  33. package/skills/excel/references/edit.md +353 -0
  34. package/skills/excel/references/read.md +99 -0
  35. package/skills/github/SKILL.md +42 -0
  36. package/skills/gmail/SKILL.md +142 -0
  37. package/skills/gmail/operations.json +71 -0
  38. package/skills/google-calendar/SKILL.md +139 -0
  39. package/skills/google-calendar/operations.json +62 -0
  40. package/skills/google-docs/SKILL.md +74 -0
  41. package/skills/google-docs/operations.json +61 -0
  42. package/skills/google-docs/references/create.md +97 -0
  43. package/skills/google-docs/references/edit.md +146 -0
  44. package/skills/google-docs/references/read.md +49 -0
  45. package/skills/google-drive/SKILL.md +118 -0
  46. package/skills/google-drive/operations.json +40 -0
  47. package/skills/google-sheets/SKILL.md +71 -0
  48. package/skills/google-sheets/operations.json +85 -0
  49. package/skills/google-sheets/references/create.md +54 -0
  50. package/skills/google-sheets/references/edit.md +124 -0
  51. package/skills/google-sheets/references/read.md +74 -0
  52. package/skills/image-generation/SKILL.md +48 -0
  53. package/skills/music-generation/SKILL.md +76 -0
  54. package/skills/notion/SKILL.md +61 -0
  55. package/skills/notion/operations.json +53 -0
  56. package/skills/notion/references/comments.md +65 -0
  57. package/skills/notion/references/databases.md +68 -0
  58. package/skills/notion/references/pages.md +119 -0
  59. package/skills/notion/references/tasks.md +28 -0
  60. package/skills/obsidian/SKILL.md +122 -0
  61. package/skills/obsidian/cli.schema.json +392 -0
  62. package/skills/obsidian/references/read.md +79 -0
  63. package/skills/obsidian/references/write.md +67 -0
  64. package/skills/pdf/SKILL.md +110 -0
  65. package/skills/pdf/references/create.md +169 -0
  66. package/skills/pdf/references/transform.md +270 -0
  67. package/skills/pdf/scripts/decrypt.py +26 -0
  68. package/skills/pdf/scripts/encrypt.py +25 -0
  69. package/skills/pdf/scripts/extract_text.py +25 -0
  70. package/skills/pdf/scripts/merge.py +21 -0
  71. package/skills/pdf/scripts/rotate.py +27 -0
  72. package/skills/presentations/SKILL.md +118 -0
  73. package/skills/presentations/references/create.md +399 -0
  74. package/skills/presentations/references/edit.md +314 -0
  75. package/skills/presentations/references/read.md +127 -0
  76. package/skills/spotify/SKILL.md +86 -0
  77. package/skills/weather/SKILL.md +33 -0
  78. package/skills/word/SKILL.md +141 -0
  79. package/skills/word/references/create.md +368 -0
  80. package/skills/word/references/edit.md +704 -0
  81. package/skills/word/references/read.md +141 -0
  82. package/skills/word/references/replace.md +86 -0
  83. package/skills/word/scripts/list_paragraphs.py +19 -0
  84. package/skills/word/scripts/replace_paragraphs.py +58 -0
@@ -0,0 +1,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.