daemora 1.0.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 (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: spotify
3
+ description: Control Spotify playback, search tracks/albums/playlists, manage queue, get now-playing info, and switch devices via the terminal. Use when the user asks to play music, pause, skip, search for a song, add to queue, or control Spotify. Requires Spotify Premium and spogo or spotify_player CLI.
4
+ triggers: spotify, play music, pause music, skip song, next track, previous track, now playing, add to queue, spotify search, play album, play playlist, music control
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ ✅ Play/pause/skip tracks, search and play specific songs/albums/artists/playlists, get current playing status, manage queue, switch output device
10
+
11
+ ❌ Download music, access Spotify on behalf of other users, anything requiring Spotify free tier (Premium required for playback control)
12
+
13
+ ## Setup (one-time)
14
+
15
+ ```bash
16
+ # Install spogo (preferred — simpler auth)
17
+ brew tap steipete/tap && brew install spogo
18
+ spogo auth import --browser chrome # import from Chrome cookies
19
+ spogo status # verify it works
20
+
21
+ # OR: install spotify_player (alternative)
22
+ brew install spotify_player
23
+ # First run opens Spotify auth in browser
24
+ ```
25
+
26
+ ## Playback Control
27
+
28
+ ```bash
29
+ # Play / Pause
30
+ spogo play
31
+ spogo pause
32
+
33
+ # Skip
34
+ spogo next
35
+ spogo prev
36
+
37
+ # Current track info
38
+ spogo status
39
+ # → 🎵 Now Playing: Daft Punk — Get Lucky (Random Access Memories)
40
+ # ⏱ 2:34 / 4:08 🔊 65% 🔀 Shuffle: off
41
+
42
+ # Volume
43
+ spogo volume 80 # set to 80%
44
+ spogo volume +10 # relative up
45
+ spogo volume -10 # relative down
46
+ ```
47
+
48
+ ## Search & Play
49
+
50
+ ```bash
51
+ # Play a specific track
52
+ spogo search track "Get Lucky Daft Punk"
53
+ # Shows results; then play by index:
54
+ spogo play --track "spotify:track:2TpxZ7JUBn3uw46aR7qd6V"
55
+
56
+ # Play an artist's top tracks
57
+ spogo search artist "Arctic Monkeys"
58
+
59
+ # Play a playlist
60
+ spogo search playlist "lofi hip hop"
61
+
62
+ # Play an album
63
+ spogo search album "Random Access Memories"
64
+ ```
65
+
66
+ ## Queue Management
67
+
68
+ ```bash
69
+ # Add current search result to queue
70
+ spogo queue add "spotify:track:<id>"
71
+
72
+ # View queue (spotify_player)
73
+ spotify_player playback queue
74
+ ```
75
+
76
+ ## Device Management
77
+
78
+ ```bash
79
+ # List available devices (speakers, phones, computers)
80
+ spogo device list
81
+ # → 1. MacBook Pro (active)
82
+ # 2. Kitchen Speaker
83
+ # 3. iPhone
84
+
85
+ # Transfer playback to a device
86
+ spogo device set "Kitchen Speaker"
87
+ spogo device set 2 # by index
88
+ ```
89
+
90
+ ## Spotify Player (fallback commands)
91
+
92
+ ```bash
93
+ # Status
94
+ spotify_player playback status
95
+
96
+ # Play/Pause/Skip
97
+ spotify_player playback play
98
+ spotify_player playback pause
99
+ spotify_player playback next
100
+ spotify_player playback previous
101
+
102
+ # Like current track
103
+ spotify_player like
104
+
105
+ # Search
106
+ spotify_player search "query"
107
+
108
+ # Connect to device
109
+ spotify_player connect
110
+ ```
111
+
112
+ ## Get Rich Now-Playing Info
113
+
114
+ ```python
115
+ #!/usr/bin/env python3
116
+ """Get detailed now-playing info using spogo status output."""
117
+ import subprocess, re
118
+
119
+ result = subprocess.run(["spogo", "status"], capture_output=True, text=True)
120
+ output = result.stdout.strip()
121
+ if "Not playing" in output or result.returncode != 0:
122
+ print("⏸ Nothing is currently playing")
123
+ else:
124
+ print(f"🎵 {output}")
125
+ ```
126
+
127
+ ## Error Handling
128
+
129
+ | Error | Fix |
130
+ |-------|-----|
131
+ | `auth failed` | Re-run `spogo auth import --browser chrome` (cookies expired) |
132
+ | `no active device` | Open Spotify app first, play something manually, then control via CLI |
133
+ | `premium required` | Spotify playback control requires Premium |
134
+ | `command not found` | Install with `brew install spogo` |
135
+ | `rate limited` | Wait 30s; Spotify API has rate limits on rapid commands |
136
+
137
+ ## Response Format
138
+
139
+ When reporting now-playing status, format it clearly:
140
+
141
+ ```
142
+ 🎵 Now Playing
143
+ Track: Get Lucky
144
+ Artist: Daft Punk
145
+ Album: Random Access Memories
146
+ Progress: 2:34 / 4:08
147
+ Volume: 65% 🔊
148
+ Device: MacBook Pro
149
+ ```
@@ -0,0 +1,230 @@
1
+ ---
2
+ name: summarize
3
+ description: Summarize long documents, articles, PDFs, web pages, emails, chat threads, meeting notes, codebases, or any large text content. Use when the user asks to summarize something, make it shorter, give a TL;DR, extract key points, or condense a document. Can handle files, URLs, and pasted content.
4
+ triggers: summarize, summary, tldr, tl;dr, condense, key points, brief, shorten, digest, recap, highlights, executive summary, abstract, overview
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ ✅ Summarizing articles, documents, PDFs, emails, long threads, meeting notes, code reviews, research papers, YouTube transcripts, Slack threads
10
+
11
+ ❌ Summarizing real-time data or live events — fetch the content first, then summarize
12
+
13
+ ## Summarization Levels
14
+
15
+ Choose based on what the user needs:
16
+
17
+ | Level | Output | Use Case |
18
+ |-------|--------|---------|
19
+ | **TL;DR** | 1-3 sentences | Quick status check, skimming |
20
+ | **Key Points** | 5-10 bullets | Decision making, sharing |
21
+ | **Executive Summary** | 2-3 paragraphs | Briefing leadership |
22
+ | **Detailed Summary** | Structured sections | Replacing reading the full doc |
23
+ | **Action Items** | Bullet list of tasks | After meetings, emails |
24
+
25
+ ## Summarize a URL
26
+
27
+ ```python
28
+ # Fetch page content, then summarize
29
+ # (Uses built-in webFetch tool — the model reads the page and summarizes)
30
+
31
+ # Strategy for long pages:
32
+ # 1. webFetch(url) → get the content
33
+ # 2. If content > 4000 words: extract the main article body, skip nav/footer
34
+ # 3. Structure the summary based on what the user asked for
35
+ ```
36
+
37
+ ## Summarize a File
38
+
39
+ ```python
40
+ #!/usr/bin/env python3
41
+ from pathlib import Path
42
+
43
+ def read_for_summary(path: str, max_chars: int = 50000) -> str:
44
+ """Read file content for summarization."""
45
+ p = Path(path)
46
+ suffix = p.suffix.lower()
47
+
48
+ if suffix in ('.txt', '.md', '.rst', '.log'):
49
+ content = p.read_text(encoding='utf-8', errors='ignore')
50
+
51
+ elif suffix == '.pdf':
52
+ # Use pdfplumber if available
53
+ try:
54
+ import pdfplumber
55
+ with pdfplumber.open(path) as pdf:
56
+ content = "\n\n".join(
57
+ f"[Page {i+1}]\n{page.extract_text() or ''}"
58
+ for i, page in enumerate(pdf.pages)
59
+ )
60
+ except ImportError:
61
+ # Fall back to pdftotext (brew install poppler)
62
+ import subprocess
63
+ result = subprocess.run(["pdftotext", path, "-"], capture_output=True, text=True)
64
+ content = result.stdout
65
+
66
+ elif suffix in ('.docx',):
67
+ import subprocess
68
+ result = subprocess.run(["pandoc", path, "-t", "plain"], capture_output=True, text=True)
69
+ content = result.stdout
70
+
71
+ elif suffix in ('.json',):
72
+ import json
73
+ data = json.loads(p.read_text())
74
+ content = json.dumps(data, indent=2)
75
+
76
+ elif suffix in ('.csv',):
77
+ lines = p.read_text().split('\n')
78
+ headers = lines[0] if lines else ""
79
+ content = f"CSV with {len(lines)-1} rows.\nHeaders: {headers}\n\nFirst 10 rows:\n" + '\n'.join(lines[1:11])
80
+
81
+ else:
82
+ content = p.read_text(encoding='utf-8', errors='ignore')
83
+
84
+ # Truncate if too long (keep start + end for context)
85
+ if len(content) > max_chars:
86
+ half = max_chars // 2
87
+ content = (content[:half] +
88
+ f"\n\n[... {len(content) - max_chars} characters truncated ...]\n\n" +
89
+ content[-half:])
90
+ return content
91
+
92
+ # Read the file and pass to the model for summarization
93
+ content = read_for_summary("/path/to/document.pdf")
94
+ print(f"Ready to summarize: {len(content)} chars")
95
+ ```
96
+
97
+ ## Summarize Long Email Thread
98
+
99
+ ```python
100
+ #!/usr/bin/env python3
101
+ def parse_email_thread(raw_email: str) -> list[dict]:
102
+ """Extract individual messages from an email thread."""
103
+ messages = []
104
+ # Split on common email separators
105
+ import re
106
+ parts = re.split(r'\n-{10,}\n|On .+ wrote:\n', raw_email)
107
+ for i, part in enumerate(parts):
108
+ lines = part.strip().split('\n')
109
+ # Try to extract From/Date
110
+ from_line = next((l for l in lines[:5] if l.startswith('From:')), f"Message {i+1}")
111
+ body = '\n'.join(lines[3:]) # skip headers
112
+ messages.append({"from": from_line, "body": body[:500]})
113
+ return messages
114
+
115
+ # Format for summarization
116
+ def format_thread_for_summary(messages: list[dict]) -> str:
117
+ return "\n\n---\n\n".join(
118
+ f"**{m['from']}**\n{m['body']}"
119
+ for m in messages
120
+ )
121
+ ```
122
+
123
+ ## Summary Output Templates
124
+
125
+ Use these structures when presenting summaries:
126
+
127
+ ### TL;DR
128
+ ```
129
+ **TL;DR:** [1-2 sentences capturing the absolute core message]
130
+ ```
131
+
132
+ ### Key Points
133
+ ```
134
+ **Key Points:**
135
+ • [Most important insight]
136
+ • [Second key point]
137
+ • [Third key point]
138
+ • [Fourth key point]
139
+ • [Fifth key point]
140
+
141
+ **Bottom line:** [One sentence conclusion or recommendation]
142
+ ```
143
+
144
+ ### Executive Summary
145
+ ```
146
+ **Executive Summary**
147
+
148
+ **What:** [What this document/meeting/thread is about — 1 sentence]
149
+
150
+ **Key Findings:**
151
+ [2-3 sentences of the most important content]
152
+
153
+ **Decisions / Actions:**
154
+ • [Decision or action item 1]
155
+ • [Decision or action item 2]
156
+
157
+ **Next Steps:**
158
+ • [What happens next]
159
+ ```
160
+
161
+ ### Meeting / Call Recap
162
+ ```
163
+ **Meeting Recap — [Date] — [Topic]**
164
+ **Attendees:** [names]
165
+
166
+ **Discussed:**
167
+ • [Topic 1]
168
+ • [Topic 2]
169
+
170
+ **Decisions Made:**
171
+ • [Decision 1]
172
+
173
+ **Action Items:**
174
+ • [Person] → [Task] by [Date]
175
+ • [Person] → [Task]
176
+ ```
177
+
178
+ ## Summarizing Code / PRs / Diffs
179
+
180
+ When summarizing code or a git diff:
181
+
182
+ ```bash
183
+ # Get a diff to summarize
184
+ git diff main..feature-branch --stat
185
+ git log main..feature-branch --oneline
186
+
187
+ # For a large diff, summarize by file:
188
+ git diff main..feature-branch -- src/ | head -200
189
+ ```
190
+
191
+ Structure code summary as:
192
+ ```
193
+ **Changes Summary**
194
+ - Files changed: N
195
+ - Lines added: +X / removed: -Y
196
+
197
+ **What changed:**
198
+ • [Feature or component]: [what was done]
199
+ • [Another area]: [what was done]
200
+
201
+ **Why (from commit messages):** [inferred purpose]
202
+ **Risk level:** Low / Medium / High — [brief reason]
203
+ ```
204
+
205
+ ## Chunking Strategy for Very Long Content
206
+
207
+ When content exceeds context limits, use this strategy:
208
+
209
+ ```python
210
+ def chunk_for_summary(text: str, chunk_size: int = 8000) -> list[str]:
211
+ """Split text into overlapping chunks for multi-pass summarization."""
212
+ chunks = []
213
+ overlap = 200 # chars overlap between chunks
214
+ start = 0
215
+ while start < len(text):
216
+ end = min(start + chunk_size, len(text))
217
+ # Try to end at a paragraph boundary
218
+ if end < len(text):
219
+ last_para = text.rfind('\n\n', start, end)
220
+ if last_para > start + chunk_size // 2:
221
+ end = last_para
222
+ chunks.append(text[start:end])
223
+ start = end - overlap
224
+ return chunks
225
+
226
+ # Strategy:
227
+ # 1. Summarize each chunk individually
228
+ # 2. Combine chunk summaries
229
+ # 3. Summarize the combined summaries → final summary
230
+ ```
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: things
3
+ description: Manage Things 3 tasks, projects, and areas on macOS. Create todos with due dates, deadlines, checklists, tags, and notes. List inbox, today, upcoming, and anytime tasks. Search and update existing tasks. Use when the user asks to add a task to Things, check their to-do list, create a project, or manage Things 3. macOS only — requires Things 3 app installed.
4
+ triggers: things, things 3, add task, todo, to-do, create task, things inbox, things today, upcoming tasks, things project, things area, things tag
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ ✅ Add todos with dates/tags/projects, read inbox/today/upcoming, search tasks, move to a project, mark complete, create checklists
10
+
11
+ ❌ Complex project management across teams (use Trello) — Things is personal task management; no API, no web access
12
+
13
+ ## Requirements
14
+
15
+ - macOS with Things 3 installed (`brew install --cask things`)
16
+ - For read operations: grant Full Disk Access to Terminal/Daemora in System Settings → Privacy & Security
17
+ - For write operations: Things URL scheme (no extra permissions needed)
18
+
19
+ ## Read Tasks
20
+
21
+ ```bash
22
+ # Find Things 3 database
23
+ THINGS_DB=$(find ~/Library/Group\ Containers -name "*.sqlite3" -path "*Things3*" 2>/dev/null | head -1)
24
+ echo "DB: $THINGS_DB"
25
+
26
+ # List today's tasks
27
+ sqlite3 "$THINGS_DB" "
28
+ SELECT title, dueDate, notes
29
+ FROM TMTask
30
+ WHERE status = 0
31
+ AND (startDate <= strftime('%s','now') OR startDate IS NULL)
32
+ AND trashed = 0
33
+ ORDER BY dueDate ASC
34
+ LIMIT 20;"
35
+ ```
36
+
37
+ ## Python Helper (Read + Write)
38
+
39
+ ```python
40
+ #!/usr/bin/env python3
41
+ import subprocess, glob, sqlite3, os, urllib.parse
42
+ from datetime import datetime, date
43
+ from pathlib import Path
44
+
45
+ # ── Read: direct SQLite access ─────────────────────────────────────────────
46
+
47
+ def find_db() -> str:
48
+ """Locate Things 3 SQLite database."""
49
+ patterns = [
50
+ os.path.expanduser("~/Library/Group Containers/*/Things/Database5/main.sqlite"),
51
+ os.path.expanduser("~/Library/Group Containers/*/ThingsData-*/Things/Database5/main.sqlite"),
52
+ ]
53
+ for p in patterns:
54
+ matches = glob.glob(p)
55
+ if matches:
56
+ return matches[0]
57
+ raise FileNotFoundError("Things 3 database not found. Make sure Things 3 is installed and has been opened at least once.")
58
+
59
+ def get_tasks(area: str = None, tag: str = None, limit: int = 30) -> list[dict]:
60
+ """Get pending tasks from Things 3."""
61
+ db_path = find_db()
62
+ conn = sqlite3.connect(db_path)
63
+ conn.row_factory = sqlite3.Row
64
+ cur = conn.cursor()
65
+
66
+ query = """
67
+ SELECT t.uuid, t.title, t.notes, t.dueDate, t.deadline,
68
+ p.title as project, a.title as area
69
+ FROM TMTask t
70
+ LEFT JOIN TMTask p ON t.project = p.uuid
71
+ LEFT JOIN TMArea a ON t.area = a.uuid
72
+ WHERE t.status = 0 AND t.trashed = 0 AND t.type = 0
73
+ """
74
+ params = []
75
+ if area:
76
+ query += " AND a.title LIKE ?"
77
+ params.append(f"%{area}%")
78
+ if tag:
79
+ query += """ AND t.uuid IN (
80
+ SELECT task FROM TMTaskTag tt
81
+ JOIN TMTag tg ON tt.tag = tg.uuid
82
+ WHERE tg.title LIKE ?)"""
83
+ params.append(f"%{tag}%")
84
+ query += f" ORDER BY t.dueDate ASC NULLS LAST LIMIT {limit}"
85
+
86
+ rows = cur.execute(query, params).fetchall()
87
+ conn.close()
88
+ return [dict(r) for r in rows]
89
+
90
+ def get_today() -> list[dict]:
91
+ """Get tasks scheduled for today or overdue."""
92
+ db_path = find_db()
93
+ today_ts = int(datetime.combine(date.today(), datetime.min.time()).timestamp())
94
+ conn = sqlite3.connect(db_path)
95
+ conn.row_factory = sqlite3.Row
96
+ cur = conn.cursor()
97
+ rows = cur.execute("""
98
+ SELECT t.uuid, t.title, t.notes, t.dueDate, t.deadline
99
+ FROM TMTask t
100
+ WHERE t.status = 0 AND t.trashed = 0 AND t.type = 0
101
+ AND t.startDate IS NOT NULL AND t.startDate <= ?
102
+ ORDER BY t.dueDate ASC NULLS LAST
103
+ """, (today_ts,)).fetchall()
104
+ conn.close()
105
+ return [dict(r) for r in rows]
106
+
107
+ # ── Write: Things URL scheme ────────────────────────────────────────────────
108
+
109
+ def add_task(
110
+ title: str,
111
+ notes: str = "",
112
+ when: str = "", # "today", "tomorrow", "evening", "anytime", "someday", or "YYYY-MM-DD"
113
+ deadline: str = "", # "YYYY-MM-DD"
114
+ tags: list[str] = None,
115
+ list_name: str = "", # project or area name
116
+ checklist: list[str] = None,
117
+ heading: str = "",
118
+ ) -> str:
119
+ """Add a task to Things 3 using the URL scheme."""
120
+ params = {"title": title}
121
+ if notes: params["notes"] = notes
122
+ if when: params["when"] = when
123
+ if deadline: params["deadline"] = deadline
124
+ if tags: params["tags"] = ",".join(tags)
125
+ if list_name: params["list"] = list_name
126
+ if heading: params["heading"] = heading
127
+ if checklist:
128
+ params["checklist-items"] = "\n".join(checklist)
129
+
130
+ encoded = urllib.parse.urlencode(params)
131
+ url = f"things:///add?{encoded}"
132
+ subprocess.run(["open", url])
133
+ return url
134
+
135
+ def add_project(title: str, notes: str = "", area: str = "", when: str = "") -> None:
136
+ """Create a new project in Things 3."""
137
+ params = {"title": title}
138
+ if notes: params["notes"] = notes
139
+ if area: params["area"] = area
140
+ if when: params["when"] = when
141
+ encoded = urllib.parse.urlencode(params)
142
+ subprocess.run(["open", f"things:///add-project?{encoded}"])
143
+ ```
144
+
145
+ ## Usage Examples
146
+
147
+ ```python
148
+ # Add a simple task
149
+ add_task("Buy groceries")
150
+
151
+ # Task with date and tags
152
+ add_task(
153
+ "Quarterly report",
154
+ notes="See shared doc in Drive",
155
+ when="2026-03-15",
156
+ deadline="2026-03-20",
157
+ tags=["work", "report"],
158
+ list_name="Q1 Planning"
159
+ )
160
+
161
+ # Task with checklist
162
+ add_task(
163
+ "Deploy v2.0",
164
+ checklist=["Run test suite", "Update CHANGELOG", "Tag release", "Deploy to prod", "Notify team"],
165
+ list_name="Engineering",
166
+ when="today"
167
+ )
168
+
169
+ # Read today's tasks
170
+ tasks = get_today()
171
+ print(f"📋 Today ({len(tasks)} tasks):")
172
+ for t in tasks:
173
+ print(f" □ {t['title']}")
174
+
175
+ # Read tasks in a project
176
+ tasks = get_tasks(area="Work")
177
+ for t in tasks:
178
+ print(f" □ {t['title']} [{t.get('project', 'no project')}]")
179
+ ```
180
+
181
+ ## `when` Values
182
+
183
+ | Value | Meaning |
184
+ |-------|---------|
185
+ | `today` | Scheduled for today |
186
+ | `tomorrow` | Tomorrow |
187
+ | `evening` | This evening |
188
+ | `anytime` | Anytime (no date) |
189
+ | `someday` | Someday |
190
+ | `YYYY-MM-DD` | Specific date |
191
+
192
+ ## Error Handling
193
+
194
+ | Error | Fix |
195
+ |-------|-----|
196
+ | DB not found | Open Things 3 first; try `find ~/Library -name "*.sqlite3" 2>/dev/null \| grep -i things` |
197
+ | Permission denied on DB | System Settings → Privacy & Security → Full Disk Access → add Terminal |
198
+ | URL scheme not working | Ensure Things 3 is installed (`ls /Applications/Things3.app`) |
199
+ | Task not appearing | Things syncs on open; click "Things" in dock to bring to front |