@xiaotianxt/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/EXCLUDED.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -0
  4. package/SECURITY.md +23 -0
  5. package/SOURCES.md +45 -0
  6. package/bin/skills.mjs +241 -0
  7. package/package.json +38 -0
  8. package/skills/1password/SKILL.md +94 -0
  9. package/skills/1password/agents/openai.yaml +4 -0
  10. package/skills/1password/references/item-management.md +80 -0
  11. package/skills/1password/references/op-cli.md +107 -0
  12. package/skills/apple-calendar-event/SKILL.md +81 -0
  13. package/skills/apple-calendar-event/agents/openai.yaml +4 -0
  14. package/skills/apple-calendar-event/scripts/calendar_audit.py +201 -0
  15. package/skills/apple-calendar-event/scripts/calendar_event.py +164 -0
  16. package/skills/bro-browser/SKILL.md +118 -0
  17. package/skills/bro-browser/agents/openai.yaml +4 -0
  18. package/skills/bro-browser/references/tool-map.md +102 -0
  19. package/skills/bro-browser/references/workflows.md +146 -0
  20. package/skills/bro-browser/scripts/bro-call.mjs +189 -0
  21. package/skills/calendar/SKILL.md +182 -0
  22. package/skills/calendar/agents/openai.yaml +4 -0
  23. package/skills/calendar/references/operations.md +255 -0
  24. package/skills/calendar/scripts/calendar_list_review.py +157 -0
  25. package/skills/calendar/scripts/event_dedupe_preview.py +155 -0
  26. package/skills/canvas/SKILL.md +70 -0
  27. package/skills/canvas/agents/openai.yaml +4 -0
  28. package/skills/canvas/references/canvas-api.md +76 -0
  29. package/skills/course-exam-review-planner/SKILL.md +127 -0
  30. package/skills/cx/SKILL.md +25 -0
  31. package/skills/gh-fix-ci/LICENSE.txt +201 -0
  32. package/skills/gh-fix-ci/SKILL.md +81 -0
  33. package/skills/gh-fix-ci/agents/openai.yaml +6 -0
  34. package/skills/gh-fix-ci/assets/github-small.svg +3 -0
  35. package/skills/gh-fix-ci/assets/github.png +0 -0
  36. package/skills/gh-fix-ci/scripts/inspect_pr_checks.py +509 -0
  37. package/skills/gh-review-workflow/SKILL.md +61 -0
  38. package/skills/gh-review-workflow/agents/openai.yaml +4 -0
  39. package/skills/gh-review-workflow/references/workflow.md +48 -0
  40. package/skills/gh-review-workflow/scripts/fetch_review_state.py +222 -0
  41. package/skills/gh-review-workflow/scripts/resolve_review_threads.py +83 -0
  42. package/skills/github/SKILL.md +74 -0
  43. package/skills/github/agents/openai.yaml +6 -0
  44. package/skills/github/assets/github-small.svg +3 -0
  45. package/skills/github/assets/github.png +0 -0
  46. package/skills/gws-calendar/SKILL.md +126 -0
  47. package/skills/gws-calendar-agenda/SKILL.md +52 -0
  48. package/skills/gws-calendar-insert/SKILL.md +66 -0
  49. package/skills/gws-docs/SKILL.md +48 -0
  50. package/skills/gws-docs-write/SKILL.md +49 -0
  51. package/skills/gws-drive/SKILL.md +137 -0
  52. package/skills/gws-drive-upload/SKILL.md +52 -0
  53. package/skills/gws-gmail/SKILL.md +62 -0
  54. package/skills/gws-gmail-forward/SKILL.md +55 -0
  55. package/skills/gws-gmail-reply/SKILL.md +58 -0
  56. package/skills/gws-gmail-reply-all/SKILL.md +62 -0
  57. package/skills/gws-gmail-send/SKILL.md +57 -0
  58. package/skills/gws-gmail-triage/SKILL.md +50 -0
  59. package/skills/gws-gmail-watch/SKILL.md +58 -0
  60. package/skills/gws-shared/SKILL.md +27 -0
  61. package/skills/helium-browser-mcp/SKILL.md +137 -0
  62. package/skills/helium-browser-mcp/agents/openai.yaml +4 -0
  63. package/skills/helium-browser-mcp/scripts/obmcp.mjs +92 -0
  64. package/skills/helium-browser-mcp/scripts/openbrowsermcp-stdio-proxy.mjs +170 -0
  65. package/skills/learn/SKILL.md +122 -0
  66. package/skills/learn/agents/openai.yaml +7 -0
  67. package/skills/learn/assets/AGENTS.template.md +33 -0
  68. package/skills/learn/assets/errorlog.template.typ +61 -0
  69. package/skills/learn/assets/reading-sequence.template.md +23 -0
  70. package/skills/learn/assets/source-index.template.md +17 -0
  71. package/skills/learn/assets/tasklog.template.typ +57 -0
  72. package/skills/learn/assets/workbook.template.typ +60 -0
  73. package/skills/learn/references/learning-science.md +103 -0
  74. package/skills/learn/scripts/init_learning_workspace.py +70 -0
  75. package/skills/macos-messages/SKILL.md +258 -0
  76. package/skills/memory/SKILL.md +33 -0
  77. package/skills/memory/codex.md +186 -0
  78. package/skills/memory/opencode.md +164 -0
  79. package/skills/mimestreamctl/SKILL.md +170 -0
  80. package/skills/mimestreamctl/agents/openai.yaml +4 -0
  81. package/skills/mimestreamctl/scripts/mimestreamctl +33 -0
  82. package/skills/mon/SKILL.md +51 -0
  83. package/skills/mon/scripts/mon_spend_review.py +458 -0
  84. package/skills/ocr/SKILL.md +136 -0
  85. package/skills/ocr/agents/openai.yaml +4 -0
  86. package/skills/ocr/references/local-ocr-best-practices.md +297 -0
  87. package/skills/ocr/references/mineru-api.md +159 -0
  88. package/skills/ocr/scripts/ocr-router +22 -0
  89. package/skills/ocr/scripts/ocr_router.py +741 -0
  90. package/skills/panopto-mp4-bulk-download/SKILL.md +57 -0
  91. package/skills/panopto-mp4-bulk-download/agents/openai.yaml +4 -0
  92. package/skills/panopto-mp4-bulk-download/references/url-patterns.md +26 -0
  93. package/skills/panopto-mp4-bulk-download/scripts/panopto_bulk_mp4.sh +213 -0
  94. package/skills/rust-systems-style/SKILL.md +109 -0
  95. package/skills/rust-systems-style/agents/openai.yaml +4 -0
  96. package/skills/rust-systems-style/references/rust-review-checklist.md +77 -0
  97. package/skills/rust-systems-style/references/style-sources.md +68 -0
  98. package/skills/ship-ai-native-cli/SKILL.md +76 -0
  99. package/skills/ship-ai-native-cli/agents/openai.yaml +4 -0
  100. package/skills/ship-ai-native-cli/references/case-notes.md +83 -0
  101. package/skills/ship-ai-native-cli/references/product-method.md +82 -0
  102. package/skills/ship-ai-native-cli/references/release-checklist.md +147 -0
  103. package/skills/ship-ai-native-cli/references/rust-cli-shape.md +111 -0
  104. package/skills/telegram-mtproto-session/SKILL.md +125 -0
  105. package/skills/telegram-mtproto-session/agents/openai.yaml +4 -0
  106. package/skills/telegram-mtproto-session/scripts/telegram_session.py +687 -0
  107. package/skills/tg/SKILL.md +173 -0
  108. package/skills/things3-manager/SKILL.md +116 -0
  109. package/skills/things3-manager/scripts/things +42 -0
  110. package/skills/things3-manager/scripts/things_cli.py +514 -0
  111. package/skills/web-artifacts-builder/LICENSE.txt +202 -0
  112. package/skills/web-artifacts-builder/SKILL.md +74 -0
  113. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
  114. package/skills/web-artifacts-builder/scripts/init-artifact.sh +379 -0
  115. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  116. package/skills/yeet/LICENSE.txt +201 -0
  117. package/skills/yeet/SKILL.md +71 -0
  118. package/skills/yeet/agents/openai.yaml +6 -0
  119. package/skills/yeet/assets/yeet-small.svg +3 -0
  120. package/skills/yeet/assets/yeet.png +0 -0
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import urllib.parse
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ APP_BUNDLE_ID = os.environ.get("THINGS_APP_BUNDLE_ID", "com.culturedcode.ThingsMac")
15
+ SKILL_DATA_DIR = Path(os.environ.get("SKILL_DATA_DIR", Path.home() / ".codex" / "skills-data" / "things3-manager"))
16
+ ENV_FILE = SKILL_DATA_DIR / ".env"
17
+
18
+
19
+ def encode_url(command: str, params: dict[str, Any] | None = None) -> str:
20
+ base = f"things:///{command}"
21
+ if not params:
22
+ return base
23
+
24
+ encoded: list[str] = []
25
+ for key, value in params.items():
26
+ if value is None:
27
+ continue
28
+ if isinstance(value, bool):
29
+ value = "true" if value else "false"
30
+ elif isinstance(value, list):
31
+ value = ",".join(str(item) for item in value)
32
+ encoded.append(f"{key}={urllib.parse.quote(str(value), safe='')}")
33
+ return base + "?" + "&".join(encoded)
34
+
35
+
36
+ def execute(url: str, dry_run: bool) -> None:
37
+ if dry_run:
38
+ print(url)
39
+ return
40
+
41
+ subprocess.run(["open", "-b", APP_BUNDLE_ID, url], check=True)
42
+ print(url)
43
+
44
+
45
+ def split_csv(values: list[str] | None) -> list[str] | None:
46
+ if not values:
47
+ return None
48
+ items: list[str] = []
49
+ for value in values:
50
+ for part in value.split(","):
51
+ part = part.strip()
52
+ if part:
53
+ items.append(part)
54
+ return items or None
55
+
56
+
57
+ def read_json_payload(args: argparse.Namespace) -> str:
58
+ if args.data:
59
+ return args.data
60
+ if args.data_file:
61
+ if args.data_file == "-":
62
+ return sys.stdin.read()
63
+ return Path(args.data_file).read_text(encoding="utf-8")
64
+ raise SystemExit("json requires --data or --data-file")
65
+
66
+
67
+ def json_requires_token(payload: Any) -> bool:
68
+ if isinstance(payload, dict):
69
+ operation = payload.get("operation")
70
+ if operation == "update":
71
+ return True
72
+ attributes = payload.get("attributes")
73
+ if json_requires_token(attributes):
74
+ return True
75
+ items = payload.get("items")
76
+ if json_requires_token(items):
77
+ return True
78
+ return False
79
+ if isinstance(payload, list):
80
+ return any(json_requires_token(item) for item in payload)
81
+ return False
82
+
83
+
84
+ def validate_json_payload(payload: Any) -> None:
85
+ if not isinstance(payload, list):
86
+ raise SystemExit(
87
+ "Things json requires a top-level JSON array of to-do/project objects. "
88
+ "Use [{\"type\":\"project\",\"attributes\":{...}}], not {\"items\":[...]}.",
89
+ )
90
+ for index, item in enumerate(payload):
91
+ if not isinstance(item, dict):
92
+ raise SystemExit(f"Things json item {index} must be an object.")
93
+ item_type = item.get("type")
94
+ if item_type not in {"to-do", "project"}:
95
+ raise SystemExit(f"Things json item {index} has unsupported top-level type: {item_type!r}.")
96
+ attributes = item.get("attributes")
97
+ if not isinstance(attributes, dict):
98
+ raise SystemExit(f"Things json item {index} must include an attributes object.")
99
+
100
+
101
+ def env_token(cli_token: str | None = None, required: bool = False) -> str | None:
102
+ token = cli_token or os.environ.get("THINGS_AUTH_TOKEN") or None
103
+ if required and not token:
104
+ raise SystemExit("Missing Things auth token. Set THINGS_AUTH_TOKEN, use set-token, or pass --auth-token.")
105
+ return token
106
+
107
+
108
+ def quote_env_value(value: str) -> str:
109
+ return value.replace("\\", "\\\\").replace('"', '\\"')
110
+
111
+
112
+ def write_env_key(key: str, value: str) -> None:
113
+ SKILL_DATA_DIR.mkdir(parents=True, exist_ok=True)
114
+ lines: list[str] = []
115
+ if ENV_FILE.exists():
116
+ lines = ENV_FILE.read_text(encoding="utf-8").splitlines()
117
+
118
+ replacement = f'{key}="{quote_env_value(value)}"'
119
+ updated = False
120
+ out_lines: list[str] = []
121
+ for line in lines:
122
+ if line.startswith(f"{key}="):
123
+ out_lines.append(replacement)
124
+ updated = True
125
+ else:
126
+ out_lines.append(line)
127
+ if not updated:
128
+ out_lines.append(replacement)
129
+ ENV_FILE.write_text("\n".join(out_lines).rstrip() + "\n", encoding="utf-8")
130
+
131
+
132
+ def common_write_flags(parser: argparse.ArgumentParser, include_list: bool = True, include_heading: bool = False) -> None:
133
+ parser.add_argument("--notes")
134
+ parser.add_argument("--when")
135
+ parser.add_argument("--deadline")
136
+ parser.add_argument("--tag", action="append")
137
+ parser.add_argument("--completed", action="store_true")
138
+ parser.add_argument("--canceled", action="store_true")
139
+ parser.add_argument("--reveal", action="store_true")
140
+ parser.add_argument("--dry-run", action="store_true")
141
+ if include_list:
142
+ parser.add_argument("--list-title")
143
+ parser.add_argument("--list-id")
144
+ if include_heading:
145
+ parser.add_argument("--heading")
146
+ parser.add_argument("--heading-id")
147
+
148
+
149
+ def cmd_add_todo(args: argparse.Namespace) -> None:
150
+ if not args.title and not args.titles:
151
+ raise SystemExit("add-todo requires --title or --titles")
152
+ params = {
153
+ "title": args.title,
154
+ "titles": "\n".join(args.titles) if args.titles else None,
155
+ "notes": args.notes,
156
+ "when": args.when,
157
+ "deadline": args.deadline,
158
+ "tags": split_csv(args.tag),
159
+ "checklist-items": "\n".join(args.checklist) if args.checklist else None,
160
+ "list": args.list_title,
161
+ "list-id": args.list_id,
162
+ "heading": args.heading,
163
+ "heading-id": args.heading_id,
164
+ "completed": True if args.completed else None,
165
+ "canceled": True if args.canceled else None,
166
+ "show-quick-entry": True if args.show_quick_entry else None,
167
+ "reveal": True if args.reveal else None,
168
+ }
169
+ execute(encode_url("add", params), args.dry_run)
170
+
171
+
172
+ def cmd_add_project(args: argparse.Namespace) -> None:
173
+ params = {
174
+ "title": args.title,
175
+ "notes": args.notes,
176
+ "when": args.when,
177
+ "deadline": args.deadline,
178
+ "tags": split_csv(args.tag),
179
+ "area": args.area_title,
180
+ "area-id": args.area_id,
181
+ "to-dos": "\n".join(args.todo) if args.todo else None,
182
+ "completed": True if args.completed else None,
183
+ "canceled": True if args.canceled else None,
184
+ "reveal": True if args.reveal else None,
185
+ }
186
+ execute(encode_url("add-project", params), args.dry_run)
187
+
188
+
189
+ def cmd_update_todo(args: argparse.Namespace) -> None:
190
+ params = {
191
+ "id": args.id,
192
+ "auth-token": env_token(args.auth_token, required=True),
193
+ "title": args.title,
194
+ "notes": args.notes,
195
+ "prepend-notes": args.prepend_notes,
196
+ "append-notes": args.append_notes,
197
+ "when": args.when,
198
+ "deadline": args.deadline,
199
+ "tags": split_csv(args.tag),
200
+ "add-tags": split_csv(args.add_tag),
201
+ "checklist-items": "\n".join(args.checklist) if args.checklist else None,
202
+ "prepend-checklist-items": "\n".join(args.prepend_checklist) if args.prepend_checklist else None,
203
+ "append-checklist-items": "\n".join(args.append_checklist) if args.append_checklist else None,
204
+ "list": args.list_title,
205
+ "list-id": args.list_id,
206
+ "heading": args.heading,
207
+ "heading-id": args.heading_id,
208
+ "completed": args.completed if args.completed else None,
209
+ "canceled": args.canceled if args.canceled else None,
210
+ "reveal": True if args.reveal else None,
211
+ "duplicate": True if args.duplicate else None,
212
+ }
213
+ execute(encode_url("update", params), args.dry_run)
214
+
215
+
216
+ def cmd_update_project(args: argparse.Namespace) -> None:
217
+ params = {
218
+ "id": args.id,
219
+ "auth-token": env_token(args.auth_token, required=True),
220
+ "title": args.title,
221
+ "notes": args.notes,
222
+ "prepend-notes": args.prepend_notes,
223
+ "append-notes": args.append_notes,
224
+ "when": args.when,
225
+ "deadline": args.deadline,
226
+ "tags": split_csv(args.tag),
227
+ "add-tags": split_csv(args.add_tag),
228
+ "area": args.area_title,
229
+ "area-id": args.area_id,
230
+ "completed": args.completed if args.completed else None,
231
+ "canceled": args.canceled if args.canceled else None,
232
+ "reveal": True if args.reveal else None,
233
+ "duplicate": True if args.duplicate else None,
234
+ }
235
+ execute(encode_url("update-project", params), args.dry_run)
236
+
237
+
238
+ def cmd_show(args: argparse.Namespace) -> None:
239
+ if not args.id and not args.query:
240
+ raise SystemExit("show requires --id or --query")
241
+ params = {
242
+ "id": args.id,
243
+ "query": args.query,
244
+ "filter": split_csv(args.filter_tag),
245
+ }
246
+ execute(encode_url("show", params), args.dry_run)
247
+
248
+
249
+ def cmd_search(args: argparse.Namespace) -> None:
250
+ params = {"query": args.query}
251
+ execute(encode_url("search", params), args.dry_run)
252
+
253
+
254
+ def cmd_version(args: argparse.Namespace) -> None:
255
+ execute(encode_url("version"), args.dry_run)
256
+
257
+
258
+ def cmd_json(args: argparse.Namespace) -> None:
259
+ data = read_json_payload(args)
260
+ parsed = json.loads(data)
261
+ validate_json_payload(parsed)
262
+ token_required = json_requires_token(parsed)
263
+ params = {
264
+ "data": json.dumps(parsed, separators=(",", ":")),
265
+ "reveal": True if args.reveal else None,
266
+ "auth-token": env_token(args.auth_token, required=token_required),
267
+ }
268
+ execute(encode_url("json", params), args.dry_run)
269
+
270
+
271
+ def cmd_set_token(args: argparse.Namespace) -> None:
272
+ write_env_key("THINGS_AUTH_TOKEN", args.token)
273
+ print(str(ENV_FILE))
274
+
275
+
276
+ def cmd_print_config(_: argparse.Namespace) -> None:
277
+ print(f"SKILL_DATA_DIR={SKILL_DATA_DIR}")
278
+ print(f"ENV_FILE={ENV_FILE}")
279
+ print(f"THINGS_APP_BUNDLE_ID={APP_BUNDLE_ID}")
280
+ token = os.environ.get("THINGS_AUTH_TOKEN", "")
281
+ if token:
282
+ print(f"THINGS_AUTH_TOKEN_SET=yes length={len(token)}")
283
+ else:
284
+ print("THINGS_AUTH_TOKEN_SET=no")
285
+
286
+
287
+ def run_osascript(script: str, argv: list[str]) -> str:
288
+ result = subprocess.run(
289
+ ["osascript", "-", *argv],
290
+ check=True,
291
+ input=script,
292
+ text=True,
293
+ capture_output=True,
294
+ )
295
+ return result.stdout.strip()
296
+
297
+
298
+ def format_table(rows: list[dict[str, str]], columns: list[tuple[str, str]]) -> str:
299
+ widths: dict[str, int] = {}
300
+ for key, label in columns:
301
+ widths[key] = len(label)
302
+ for row in rows:
303
+ for key, _ in columns:
304
+ widths[key] = max(widths[key], len(row.get(key, "")))
305
+
306
+ lines: list[str] = []
307
+ header = " ".join(label.ljust(widths[key]) for key, label in columns)
308
+ divider = " ".join("-" * widths[key] for key, _ in columns)
309
+ lines.append(header)
310
+ lines.append(divider)
311
+ for row in rows:
312
+ lines.append(" ".join(row.get(key, "").ljust(widths[key]) for key, _ in columns))
313
+ return "\n".join(lines)
314
+
315
+
316
+ def cmd_find_open_todos(args: argparse.Namespace) -> None:
317
+ script = r"""
318
+ on pad2(n)
319
+ if n < 10 then
320
+ return "0" & (n as text)
321
+ end if
322
+ return n as text
323
+ end pad2
324
+
325
+ on iso_date(d)
326
+ if d is missing value then
327
+ return ""
328
+ end if
329
+ set y to year of d as integer
330
+ set m to month of d as integer
331
+ set day_num to day of d as integer
332
+ set hh to hours of d as integer
333
+ set mm to minutes of d as integer
334
+ set ss to seconds of d as integer
335
+ return (y as text) & "-" & my pad2(m) & "-" & my pad2(day_num) & "T" & my pad2(hh) & ":" & my pad2(mm) & ":" & my pad2(ss)
336
+ end iso_date
337
+
338
+ on safe_project_name(t)
339
+ try
340
+ set parent_project to project of t
341
+ if parent_project is missing value then
342
+ return ""
343
+ end if
344
+ return name of parent_project
345
+ on error
346
+ return ""
347
+ end try
348
+ end safe_project_name
349
+
350
+ on run argv
351
+ set project_name to item 1 of argv
352
+ set title_filter to item 2 of argv
353
+ set field_sep to character id 31
354
+ tell application "Things3"
355
+ if project_name is not "" then
356
+ try
357
+ set xs to to dos of project project_name
358
+ on error
359
+ set xs to {}
360
+ end try
361
+ else
362
+ set xs to to dos
363
+ end if
364
+
365
+ set rows to {}
366
+ repeat with t in xs
367
+ set title_text to name of t
368
+ if title_filter is "" or title_text contains title_filter then
369
+ set end of rows to (id of t) & field_sep & title_text & field_sep & ((status of t) as string) & field_sep & my iso_date(activation date of t) & field_sep & my iso_date(due date of t) & field_sep & my safe_project_name(t)
370
+ end if
371
+ end repeat
372
+ end tell
373
+
374
+ set AppleScript's text item delimiters to linefeed
375
+ return rows as text
376
+ end run
377
+ """
378
+ output = run_osascript(script, [args.project or "", args.title_contains or ""])
379
+ rows: list[dict[str, str]] = []
380
+ if output:
381
+ for line in output.splitlines():
382
+ item_id, title, status, activation_date, due_date, project = (line.split("\x1f") + ["", "", "", "", "", ""])[:6]
383
+ project_name = project or args.project or ""
384
+ rows.append(
385
+ {
386
+ "id": item_id,
387
+ "title": title,
388
+ "status": status,
389
+ "activation_date": activation_date,
390
+ "due_date": due_date,
391
+ "project": project_name,
392
+ }
393
+ )
394
+
395
+ rows.sort(key=lambda row: (row["activation_date"] or "9999-99-99T99:99:99", row["due_date"] or "9999-99-99T99:99:99", row["title"]))
396
+
397
+ if args.json:
398
+ print(json.dumps(rows, ensure_ascii=False, indent=2))
399
+ return
400
+
401
+ if not rows:
402
+ print("No matching open to-dos.")
403
+ return
404
+
405
+ print(
406
+ format_table(
407
+ rows,
408
+ [
409
+ ("id", "ID"),
410
+ ("title", "Title"),
411
+ ("status", "Status"),
412
+ ("activation_date", "Activation"),
413
+ ("due_date", "Due"),
414
+ ("project", "Project"),
415
+ ],
416
+ )
417
+ )
418
+
419
+
420
+ def build_parser() -> argparse.ArgumentParser:
421
+ parser = argparse.ArgumentParser(description="Things 3 URL-scheme CLI")
422
+ subparsers = parser.add_subparsers(dest="command", required=True)
423
+
424
+ add_todo = subparsers.add_parser("add-todo", help="Create a Things to-do")
425
+ add_todo.add_argument("--title")
426
+ add_todo.add_argument("--titles", action="append")
427
+ add_todo.add_argument("--checklist", action="append")
428
+ add_todo.add_argument("--show-quick-entry", action="store_true")
429
+ common_write_flags(add_todo, include_list=True, include_heading=True)
430
+ add_todo.set_defaults(func=cmd_add_todo)
431
+
432
+ add_project = subparsers.add_parser("add-project", help="Create a Things project")
433
+ add_project.add_argument("--title", required=True)
434
+ add_project.add_argument("--todo", action="append")
435
+ add_project.add_argument("--area-title")
436
+ add_project.add_argument("--area-id")
437
+ common_write_flags(add_project, include_list=False, include_heading=False)
438
+ add_project.set_defaults(func=cmd_add_project)
439
+
440
+ update_todo = subparsers.add_parser("update-todo", help="Update a Things to-do")
441
+ update_todo.add_argument("--id", required=True)
442
+ update_todo.add_argument("--auth-token")
443
+ update_todo.add_argument("--title")
444
+ update_todo.add_argument("--prepend-notes")
445
+ update_todo.add_argument("--append-notes")
446
+ update_todo.add_argument("--add-tag", action="append")
447
+ update_todo.add_argument("--checklist", action="append")
448
+ update_todo.add_argument("--prepend-checklist", action="append")
449
+ update_todo.add_argument("--append-checklist", action="append")
450
+ update_todo.add_argument("--duplicate", action="store_true")
451
+ common_write_flags(update_todo, include_list=True, include_heading=True)
452
+ update_todo.set_defaults(func=cmd_update_todo)
453
+
454
+ update_project = subparsers.add_parser("update-project", help="Update a Things project")
455
+ update_project.add_argument("--id", required=True)
456
+ update_project.add_argument("--auth-token")
457
+ update_project.add_argument("--title")
458
+ update_project.add_argument("--prepend-notes")
459
+ update_project.add_argument("--append-notes")
460
+ update_project.add_argument("--add-tag", action="append")
461
+ update_project.add_argument("--area-title")
462
+ update_project.add_argument("--area-id")
463
+ update_project.add_argument("--duplicate", action="store_true")
464
+ common_write_flags(update_project, include_list=False, include_heading=False)
465
+ update_project.set_defaults(func=cmd_update_project)
466
+
467
+ show = subparsers.add_parser("show", help="Open a built-in list, project, tag, or item")
468
+ show.add_argument("--id")
469
+ show.add_argument("--query")
470
+ show.add_argument("--filter-tag", action="append")
471
+ show.add_argument("--dry-run", action="store_true")
472
+ show.set_defaults(func=cmd_show)
473
+
474
+ search = subparsers.add_parser("search", help="Open Things search")
475
+ search.add_argument("query", nargs="?", default="")
476
+ search.add_argument("--dry-run", action="store_true")
477
+ search.set_defaults(func=cmd_search)
478
+
479
+ find_open_todos = subparsers.add_parser("find-open-todos", help="List matching open to-dos with ids and dates")
480
+ find_open_todos.add_argument("--project")
481
+ find_open_todos.add_argument("--title-contains")
482
+ find_open_todos.add_argument("--json", action="store_true")
483
+ find_open_todos.set_defaults(func=cmd_find_open_todos)
484
+
485
+ version = subparsers.add_parser("version", help="Open the Things version command")
486
+ version.add_argument("--dry-run", action="store_true")
487
+ version.set_defaults(func=cmd_version)
488
+
489
+ json_cmd = subparsers.add_parser("json", help="Send a JSON payload to Things")
490
+ json_cmd.add_argument("--data")
491
+ json_cmd.add_argument("--data-file")
492
+ json_cmd.add_argument("--auth-token")
493
+ json_cmd.add_argument("--reveal", action="store_true")
494
+ json_cmd.add_argument("--dry-run", action="store_true")
495
+ json_cmd.set_defaults(func=cmd_json)
496
+
497
+ set_token = subparsers.add_parser("set-token", help="Persist THINGS_AUTH_TOKEN into the skill env file")
498
+ set_token.add_argument("--token", required=True)
499
+ set_token.set_defaults(func=cmd_set_token)
500
+
501
+ print_config = subparsers.add_parser("print-config", help="Show env-backed config paths")
502
+ print_config.set_defaults(func=cmd_print_config)
503
+
504
+ return parser
505
+
506
+
507
+ def main() -> None:
508
+ parser = build_parser()
509
+ args = parser.parse_args()
510
+ args.func(args)
511
+
512
+
513
+ if __name__ == "__main__":
514
+ main()