@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.
- package/EXCLUDED.md +42 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/SECURITY.md +23 -0
- package/SOURCES.md +45 -0
- package/bin/skills.mjs +241 -0
- package/package.json +38 -0
- package/skills/1password/SKILL.md +94 -0
- package/skills/1password/agents/openai.yaml +4 -0
- package/skills/1password/references/item-management.md +80 -0
- package/skills/1password/references/op-cli.md +107 -0
- package/skills/apple-calendar-event/SKILL.md +81 -0
- package/skills/apple-calendar-event/agents/openai.yaml +4 -0
- package/skills/apple-calendar-event/scripts/calendar_audit.py +201 -0
- package/skills/apple-calendar-event/scripts/calendar_event.py +164 -0
- package/skills/bro-browser/SKILL.md +118 -0
- package/skills/bro-browser/agents/openai.yaml +4 -0
- package/skills/bro-browser/references/tool-map.md +102 -0
- package/skills/bro-browser/references/workflows.md +146 -0
- package/skills/bro-browser/scripts/bro-call.mjs +189 -0
- package/skills/calendar/SKILL.md +182 -0
- package/skills/calendar/agents/openai.yaml +4 -0
- package/skills/calendar/references/operations.md +255 -0
- package/skills/calendar/scripts/calendar_list_review.py +157 -0
- package/skills/calendar/scripts/event_dedupe_preview.py +155 -0
- package/skills/canvas/SKILL.md +70 -0
- package/skills/canvas/agents/openai.yaml +4 -0
- package/skills/canvas/references/canvas-api.md +76 -0
- package/skills/course-exam-review-planner/SKILL.md +127 -0
- package/skills/cx/SKILL.md +25 -0
- package/skills/gh-fix-ci/LICENSE.txt +201 -0
- package/skills/gh-fix-ci/SKILL.md +81 -0
- package/skills/gh-fix-ci/agents/openai.yaml +6 -0
- package/skills/gh-fix-ci/assets/github-small.svg +3 -0
- package/skills/gh-fix-ci/assets/github.png +0 -0
- package/skills/gh-fix-ci/scripts/inspect_pr_checks.py +509 -0
- package/skills/gh-review-workflow/SKILL.md +61 -0
- package/skills/gh-review-workflow/agents/openai.yaml +4 -0
- package/skills/gh-review-workflow/references/workflow.md +48 -0
- package/skills/gh-review-workflow/scripts/fetch_review_state.py +222 -0
- package/skills/gh-review-workflow/scripts/resolve_review_threads.py +83 -0
- package/skills/github/SKILL.md +74 -0
- package/skills/github/agents/openai.yaml +6 -0
- package/skills/github/assets/github-small.svg +3 -0
- package/skills/github/assets/github.png +0 -0
- package/skills/gws-calendar/SKILL.md +126 -0
- package/skills/gws-calendar-agenda/SKILL.md +52 -0
- package/skills/gws-calendar-insert/SKILL.md +66 -0
- package/skills/gws-docs/SKILL.md +48 -0
- package/skills/gws-docs-write/SKILL.md +49 -0
- package/skills/gws-drive/SKILL.md +137 -0
- package/skills/gws-drive-upload/SKILL.md +52 -0
- package/skills/gws-gmail/SKILL.md +62 -0
- package/skills/gws-gmail-forward/SKILL.md +55 -0
- package/skills/gws-gmail-reply/SKILL.md +58 -0
- package/skills/gws-gmail-reply-all/SKILL.md +62 -0
- package/skills/gws-gmail-send/SKILL.md +57 -0
- package/skills/gws-gmail-triage/SKILL.md +50 -0
- package/skills/gws-gmail-watch/SKILL.md +58 -0
- package/skills/gws-shared/SKILL.md +27 -0
- package/skills/helium-browser-mcp/SKILL.md +137 -0
- package/skills/helium-browser-mcp/agents/openai.yaml +4 -0
- package/skills/helium-browser-mcp/scripts/obmcp.mjs +92 -0
- package/skills/helium-browser-mcp/scripts/openbrowsermcp-stdio-proxy.mjs +170 -0
- package/skills/learn/SKILL.md +122 -0
- package/skills/learn/agents/openai.yaml +7 -0
- package/skills/learn/assets/AGENTS.template.md +33 -0
- package/skills/learn/assets/errorlog.template.typ +61 -0
- package/skills/learn/assets/reading-sequence.template.md +23 -0
- package/skills/learn/assets/source-index.template.md +17 -0
- package/skills/learn/assets/tasklog.template.typ +57 -0
- package/skills/learn/assets/workbook.template.typ +60 -0
- package/skills/learn/references/learning-science.md +103 -0
- package/skills/learn/scripts/init_learning_workspace.py +70 -0
- package/skills/macos-messages/SKILL.md +258 -0
- package/skills/memory/SKILL.md +33 -0
- package/skills/memory/codex.md +186 -0
- package/skills/memory/opencode.md +164 -0
- package/skills/mimestreamctl/SKILL.md +170 -0
- package/skills/mimestreamctl/agents/openai.yaml +4 -0
- package/skills/mimestreamctl/scripts/mimestreamctl +33 -0
- package/skills/mon/SKILL.md +51 -0
- package/skills/mon/scripts/mon_spend_review.py +458 -0
- package/skills/ocr/SKILL.md +136 -0
- package/skills/ocr/agents/openai.yaml +4 -0
- package/skills/ocr/references/local-ocr-best-practices.md +297 -0
- package/skills/ocr/references/mineru-api.md +159 -0
- package/skills/ocr/scripts/ocr-router +22 -0
- package/skills/ocr/scripts/ocr_router.py +741 -0
- package/skills/panopto-mp4-bulk-download/SKILL.md +57 -0
- package/skills/panopto-mp4-bulk-download/agents/openai.yaml +4 -0
- package/skills/panopto-mp4-bulk-download/references/url-patterns.md +26 -0
- package/skills/panopto-mp4-bulk-download/scripts/panopto_bulk_mp4.sh +213 -0
- package/skills/rust-systems-style/SKILL.md +109 -0
- package/skills/rust-systems-style/agents/openai.yaml +4 -0
- package/skills/rust-systems-style/references/rust-review-checklist.md +77 -0
- package/skills/rust-systems-style/references/style-sources.md +68 -0
- package/skills/ship-ai-native-cli/SKILL.md +76 -0
- package/skills/ship-ai-native-cli/agents/openai.yaml +4 -0
- package/skills/ship-ai-native-cli/references/case-notes.md +83 -0
- package/skills/ship-ai-native-cli/references/product-method.md +82 -0
- package/skills/ship-ai-native-cli/references/release-checklist.md +147 -0
- package/skills/ship-ai-native-cli/references/rust-cli-shape.md +111 -0
- package/skills/telegram-mtproto-session/SKILL.md +125 -0
- package/skills/telegram-mtproto-session/agents/openai.yaml +4 -0
- package/skills/telegram-mtproto-session/scripts/telegram_session.py +687 -0
- package/skills/tg/SKILL.md +173 -0
- package/skills/things3-manager/SKILL.md +116 -0
- package/skills/things3-manager/scripts/things +42 -0
- package/skills/things3-manager/scripts/things_cli.py +514 -0
- package/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/skills/web-artifacts-builder/SKILL.md +74 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +379 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/yeet/LICENSE.txt +201 -0
- package/skills/yeet/SKILL.md +71 -0
- package/skills/yeet/agents/openai.yaml +6 -0
- package/skills/yeet/assets/yeet-small.svg +3 -0
- 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()
|