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.
- package/README.md +666 -0
- package/SOUL.md +104 -0
- package/config/hooks.json +14 -0
- package/config/mcp.json +145 -0
- package/package.json +86 -0
- package/skills/.gitkeep +0 -0
- package/skills/apple-notes.md +193 -0
- package/skills/apple-reminders.md +189 -0
- package/skills/camsnap.md +162 -0
- package/skills/coding.md +14 -0
- package/skills/documents.md +13 -0
- package/skills/email.md +13 -0
- package/skills/gif-search.md +196 -0
- package/skills/healthcheck.md +225 -0
- package/skills/image-gen.md +147 -0
- package/skills/model-usage.md +182 -0
- package/skills/obsidian.md +207 -0
- package/skills/pdf.md +211 -0
- package/skills/research.md +13 -0
- package/skills/skill-creator.md +142 -0
- package/skills/spotify.md +149 -0
- package/skills/summarize.md +230 -0
- package/skills/things.md +199 -0
- package/skills/tmux.md +204 -0
- package/skills/trello.md +183 -0
- package/skills/video-frames.md +202 -0
- package/skills/weather.md +127 -0
- package/src/a2a/A2AClient.js +136 -0
- package/src/a2a/A2AServer.js +316 -0
- package/src/a2a/AgentCard.js +79 -0
- package/src/agents/SubAgentManager.js +369 -0
- package/src/agents/Supervisor.js +192 -0
- package/src/channels/BaseChannel.js +104 -0
- package/src/channels/DiscordChannel.js +288 -0
- package/src/channels/EmailChannel.js +172 -0
- package/src/channels/GoogleChatChannel.js +316 -0
- package/src/channels/HttpChannel.js +26 -0
- package/src/channels/LineChannel.js +168 -0
- package/src/channels/SignalChannel.js +186 -0
- package/src/channels/SlackChannel.js +329 -0
- package/src/channels/TeamsChannel.js +272 -0
- package/src/channels/TelegramChannel.js +347 -0
- package/src/channels/WhatsAppChannel.js +219 -0
- package/src/channels/index.js +198 -0
- package/src/cli.js +1267 -0
- package/src/config/agentProfiles.js +120 -0
- package/src/config/channels.js +32 -0
- package/src/config/default.js +206 -0
- package/src/config/models.js +123 -0
- package/src/config/permissions.js +167 -0
- package/src/core/AgentLoop.js +446 -0
- package/src/core/Compaction.js +143 -0
- package/src/core/CostTracker.js +116 -0
- package/src/core/EventBus.js +46 -0
- package/src/core/Task.js +67 -0
- package/src/core/TaskQueue.js +206 -0
- package/src/core/TaskRunner.js +226 -0
- package/src/daemon/DaemonManager.js +301 -0
- package/src/hooks/HookRunner.js +230 -0
- package/src/index.js +482 -0
- package/src/mcp/MCPAgentRunner.js +112 -0
- package/src/mcp/MCPClient.js +186 -0
- package/src/mcp/MCPManager.js +412 -0
- package/src/models/ModelRouter.js +180 -0
- package/src/safety/AuditLog.js +135 -0
- package/src/safety/CircuitBreaker.js +126 -0
- package/src/safety/FilesystemGuard.js +169 -0
- package/src/safety/GitRollback.js +139 -0
- package/src/safety/HumanApproval.js +156 -0
- package/src/safety/InputSanitizer.js +72 -0
- package/src/safety/PermissionGuard.js +83 -0
- package/src/safety/Sandbox.js +70 -0
- package/src/safety/SecretScanner.js +100 -0
- package/src/safety/SecretVault.js +250 -0
- package/src/scheduler/Heartbeat.js +115 -0
- package/src/scheduler/Scheduler.js +228 -0
- package/src/services/models/outputSchema.js +15 -0
- package/src/services/openai.js +25 -0
- package/src/services/sessions.js +65 -0
- package/src/setup/theme.js +110 -0
- package/src/setup/wizard.js +788 -0
- package/src/skills/SkillLoader.js +168 -0
- package/src/storage/TaskStore.js +69 -0
- package/src/systemPrompt.js +526 -0
- package/src/tenants/TenantContext.js +19 -0
- package/src/tenants/TenantManager.js +379 -0
- package/src/tools/ToolRegistry.js +141 -0
- package/src/tools/applyPatch.js +144 -0
- package/src/tools/browserAutomation.js +223 -0
- package/src/tools/createDocument.js +265 -0
- package/src/tools/cronTool.js +105 -0
- package/src/tools/editFile.js +139 -0
- package/src/tools/executeCommand.js +123 -0
- package/src/tools/glob.js +67 -0
- package/src/tools/grep.js +121 -0
- package/src/tools/imageAnalysis.js +120 -0
- package/src/tools/index.js +173 -0
- package/src/tools/listDirectory.js +47 -0
- package/src/tools/manageAgents.js +47 -0
- package/src/tools/manageMCP.js +159 -0
- package/src/tools/memory.js +478 -0
- package/src/tools/messageChannel.js +45 -0
- package/src/tools/projectTracker.js +259 -0
- package/src/tools/readFile.js +52 -0
- package/src/tools/screenCapture.js +112 -0
- package/src/tools/searchContent.js +76 -0
- package/src/tools/searchFiles.js +75 -0
- package/src/tools/sendEmail.js +118 -0
- package/src/tools/sendFile.js +63 -0
- package/src/tools/textToSpeech.js +161 -0
- package/src/tools/transcribeAudio.js +82 -0
- package/src/tools/useMCP.js +29 -0
- package/src/tools/webFetch.js +150 -0
- package/src/tools/webSearch.js +134 -0
- 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
|
+
```
|
package/skills/things.md
ADDED
|
@@ -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 |
|