daemora 1.0.1 → 1.0.3

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 (134) hide show
  1. package/README.md +106 -76
  2. package/SOUL.md +100 -28
  3. package/config/mcp.json +9 -9
  4. package/package.json +15 -8
  5. package/skills/apple-notes.md +0 -52
  6. package/skills/apple-reminders.md +1 -87
  7. package/skills/camsnap.md +20 -144
  8. package/skills/coding.md +7 -7
  9. package/skills/documents.md +6 -6
  10. package/skills/email.md +6 -6
  11. package/skills/gif-search.md +28 -171
  12. package/skills/healthcheck.md +21 -203
  13. package/skills/image-gen.md +24 -123
  14. package/skills/model-usage.md +18 -165
  15. package/skills/obsidian.md +28 -174
  16. package/skills/pdf.md +30 -181
  17. package/skills/research.md +6 -6
  18. package/skills/skill-creator.md +35 -111
  19. package/skills/spotify.md +2 -17
  20. package/skills/summarize.md +36 -193
  21. package/skills/things.md +23 -175
  22. package/skills/tmux.md +1 -91
  23. package/skills/trello.md +32 -157
  24. package/skills/video-frames.md +26 -166
  25. package/skills/weather.md +6 -6
  26. package/src/a2a/A2AClient.js +2 -2
  27. package/src/a2a/A2AServer.js +6 -6
  28. package/src/a2a/AgentCard.js +2 -2
  29. package/src/agents/SubAgentManager.js +61 -19
  30. package/src/agents/Supervisor.js +4 -4
  31. package/src/channels/BaseChannel.js +6 -6
  32. package/src/channels/BlueBubblesChannel.js +112 -0
  33. package/src/channels/DiscordChannel.js +8 -8
  34. package/src/channels/EmailChannel.js +54 -26
  35. package/src/channels/FeishuChannel.js +140 -0
  36. package/src/channels/GoogleChatChannel.js +8 -8
  37. package/src/channels/HttpChannel.js +2 -2
  38. package/src/channels/IRCChannel.js +144 -0
  39. package/src/channels/LineChannel.js +13 -13
  40. package/src/channels/MatrixChannel.js +97 -0
  41. package/src/channels/MattermostChannel.js +119 -0
  42. package/src/channels/NextcloudChannel.js +133 -0
  43. package/src/channels/NostrChannel.js +175 -0
  44. package/src/channels/SignalChannel.js +9 -9
  45. package/src/channels/SlackChannel.js +10 -10
  46. package/src/channels/TeamsChannel.js +10 -10
  47. package/src/channels/TelegramChannel.js +8 -8
  48. package/src/channels/TwitchChannel.js +128 -0
  49. package/src/channels/WhatsAppChannel.js +10 -10
  50. package/src/channels/ZaloChannel.js +119 -0
  51. package/src/channels/iMessageChannel.js +150 -0
  52. package/src/channels/index.js +241 -11
  53. package/src/cli.js +835 -38
  54. package/src/config/agentProfiles.js +19 -19
  55. package/src/config/channels.js +1 -1
  56. package/src/config/default.js +12 -7
  57. package/src/config/models.js +3 -3
  58. package/src/config/permissions.js +2 -2
  59. package/src/core/AgentLoop.js +13 -13
  60. package/src/core/Compaction.js +3 -3
  61. package/src/core/CostTracker.js +2 -2
  62. package/src/core/EventBus.js +15 -15
  63. package/src/core/TaskQueue.js +24 -7
  64. package/src/core/TaskRunner.js +19 -6
  65. package/src/daemon/DaemonManager.js +4 -4
  66. package/src/hooks/HookRunner.js +4 -4
  67. package/src/index.js +6 -2
  68. package/src/mcp/MCPAgentRunner.js +3 -3
  69. package/src/mcp/MCPClient.js +9 -9
  70. package/src/mcp/MCPManager.js +14 -14
  71. package/src/models/ModelRouter.js +2 -2
  72. package/src/safety/AuditLog.js +3 -3
  73. package/src/safety/CircuitBreaker.js +2 -2
  74. package/src/safety/CommandGuard.js +132 -0
  75. package/src/safety/FilesystemGuard.js +23 -3
  76. package/src/safety/GitRollback.js +5 -5
  77. package/src/safety/HumanApproval.js +9 -9
  78. package/src/safety/InputSanitizer.js +81 -8
  79. package/src/safety/PermissionGuard.js +2 -2
  80. package/src/safety/Sandbox.js +1 -1
  81. package/src/safety/SecretScanner.js +90 -28
  82. package/src/safety/SecretVault.js +2 -2
  83. package/src/scheduler/Heartbeat.js +3 -3
  84. package/src/scheduler/Scheduler.js +6 -6
  85. package/src/setup/theme.js +171 -66
  86. package/src/setup/wizard.js +432 -57
  87. package/src/skills/SkillLoader.js +145 -8
  88. package/src/storage/TaskStore.js +39 -15
  89. package/src/systemPrompt.js +45 -43
  90. package/src/tenants/TenantManager.js +79 -22
  91. package/src/tools/ToolRegistry.js +3 -3
  92. package/src/tools/applyPatch.js +2 -2
  93. package/src/tools/browserAutomation.js +4 -4
  94. package/src/tools/calendar.js +155 -0
  95. package/src/tools/clipboard.js +71 -0
  96. package/src/tools/contacts.js +138 -0
  97. package/src/tools/createDocument.js +2 -2
  98. package/src/tools/cronTool.js +14 -14
  99. package/src/tools/database.js +165 -0
  100. package/src/tools/editFile.js +10 -10
  101. package/src/tools/executeCommand.js +11 -3
  102. package/src/tools/generateImage.js +79 -0
  103. package/src/tools/gitTool.js +141 -0
  104. package/src/tools/glob.js +1 -1
  105. package/src/tools/googlePlaces.js +136 -0
  106. package/src/tools/grep.js +2 -2
  107. package/src/tools/iMessageTool.js +86 -0
  108. package/src/tools/imageAnalysis.js +3 -3
  109. package/src/tools/index.js +56 -2
  110. package/src/tools/makeVoiceCall.js +283 -0
  111. package/src/tools/manageAgents.js +2 -2
  112. package/src/tools/manageMCP.js +38 -20
  113. package/src/tools/memory.js +25 -32
  114. package/src/tools/messageChannel.js +1 -1
  115. package/src/tools/notification.js +90 -0
  116. package/src/tools/philipsHue.js +147 -0
  117. package/src/tools/projectTracker.js +8 -8
  118. package/src/tools/readFile.js +1 -1
  119. package/src/tools/readPDF.js +73 -0
  120. package/src/tools/screenCapture.js +6 -6
  121. package/src/tools/searchContent.js +2 -2
  122. package/src/tools/searchFiles.js +1 -1
  123. package/src/tools/sendEmail.js +79 -24
  124. package/src/tools/sendFile.js +4 -4
  125. package/src/tools/sonos.js +137 -0
  126. package/src/tools/sshTool.js +130 -0
  127. package/src/tools/textToSpeech.js +5 -5
  128. package/src/tools/transcribeAudio.js +4 -4
  129. package/src/tools/useMCP.js +4 -4
  130. package/src/tools/webFetch.js +2 -2
  131. package/src/tools/webSearch.js +1 -1
  132. package/src/utils/Embeddings.js +79 -0
  133. package/src/voice/VoiceSessionManager.js +170 -0
  134. package/src/voice/VoiceWebhook.js +188 -0
@@ -1,182 +1,35 @@
1
1
  ---
2
2
  name: model-usage
3
- description: Track and report AI model API usage, costs, token counts, and spending across OpenAI, Anthropic, and Google. Use when the user asks how much they've spent, their API usage, token counts, cost breakdown, daily/monthly costs, or wants to optimize AI spending. Also check Daemora's built-in cost tracking.
3
+ description: Track and report AI model API usage, costs, token counts, and spending across OpenAI, Anthropic, and Google. Use when the user asks how much they've spent, their API usage, token counts, cost breakdown, daily/monthly costs, or wants to optimize AI spending.
4
4
  triggers: model usage, api cost, token usage, spending, how much spent, cost breakdown, openai usage, anthropic usage, api usage, cost report, ai spending, daily cost, monthly cost
5
+ metadata: {"daemora": {"emoji": "💰"}}
5
6
  ---
6
7
 
7
- ## When to Use
8
-
9
- ✅ Check today's/this month's API costs, per-model breakdown, token counts, cost trends, optimization suggestions, Daemora internal cost report
10
-
11
- ❌ Real-time streaming token counts — use the Daemora `/costs/today` endpoint for live data
12
-
13
- ## Daemora Built-in Cost Tracking
8
+ ## Daemora built-in cost tracking
14
9
 
15
10
  ```bash
16
- # Today's costs (all models, all tasks)
17
- curl -s http://localhost:8081/costs/today | python3 -c "
18
- import sys, json
19
- d = json.load(sys.stdin)
20
- print(f\"Today's spend: \${d.get('totalCost', 0):.4f}\")
21
- print(f\"Tasks: {d.get('taskCount', 0)}\")
22
- print(f\"Tokens: {d.get('totalTokens', 0):,} ({d.get('inputTokens',0):,} in / {d.get('outputTokens',0):,} out)\")
23
- if 'byModel' in d:
24
- print('\\nBy model:')
25
- for model, cost in sorted(d['byModel'].items(), key=lambda x: -x[1]):
26
- print(f\" {model:<40} \${cost:.4f}\")
27
- "
28
- ```
29
-
30
- ## Parse Daemora Cost Log Directly
31
-
32
- ```python
33
- #!/usr/bin/env python3
34
- """Parse Daemora's JSONL cost log for detailed analysis."""
35
- import json, os
36
- from pathlib import Path
37
- from datetime import datetime, date, timedelta
38
- from collections import defaultdict
39
-
40
- COST_DIR = Path("data/costs") # adjust if different
41
-
42
- def load_cost_log(days: int = 30) -> list[dict]:
43
- """Load cost entries from the last N days."""
44
- entries = []
45
- cutoff = date.today() - timedelta(days=days)
46
- for log_file in sorted(COST_DIR.glob("*.jsonl")):
47
- try:
48
- file_date = date.fromisoformat(log_file.stem)
49
- if file_date < cutoff:
50
- continue
51
- except:
52
- pass
53
- for line in log_file.read_text().splitlines():
54
- if line.strip():
55
- try:
56
- entries.append(json.loads(line))
57
- except:
58
- pass
59
- return entries
60
-
61
- def cost_report(days: int = 7):
62
- entries = load_cost_log(days)
63
- if not entries:
64
- print("No cost data found")
65
- return
66
-
67
- by_day = defaultdict(float)
68
- by_model = defaultdict(float)
69
- by_tenant = defaultdict(float)
70
- total_cost = 0
71
- total_in = 0
72
- total_out = 0
73
-
74
- for e in entries:
75
- cost = e.get("estimatedCost", 0) or 0
76
- ts = e.get("timestamp", "")[:10]
77
- model = e.get("modelId", "unknown")
78
- tenant = e.get("tenantId", "default")
79
-
80
- total_cost += cost
81
- total_in += e.get("inputTokens", 0) or 0
82
- total_out += e.get("outputTokens", 0) or 0
83
- by_day[ts] += cost
84
- by_model[model] += cost
85
- by_tenant[tenant] += cost
86
-
87
- print(f"📊 Cost Report — Last {days} days")
88
- print(f"{'═'*50}")
89
- print(f"Total spend: ${total_cost:.4f}")
90
- print(f"Total tokens: {total_in+total_out:,} ({total_in:,} in / {total_out:,} out)")
91
- print(f"Tasks: {len(entries)}")
92
-
93
- print(f"\n📅 Daily breakdown:")
94
- for day in sorted(by_day.keys(), reverse=True)[:days]:
95
- bar = '█' * int(by_day[day] / max(by_day.values()) * 20) if by_day else ''
96
- print(f" {day} {bar:<20} ${by_day[day]:.4f}")
97
-
98
- print(f"\n🤖 By model:")
99
- for model, cost in sorted(by_model.items(), key=lambda x: -x[1]):
100
- pct = cost / total_cost * 100 if total_cost else 0
101
- print(f" {model:<40} ${cost:.4f} ({pct:.0f}%)")
102
-
103
- if len(by_tenant) > 1:
104
- print(f"\n👤 By tenant:")
105
- for tenant, cost in sorted(by_tenant.items(), key=lambda x: -x[1]):
106
- print(f" {tenant:<30} ${cost:.4f}")
107
-
108
- cost_report(days=7)
109
- ```
110
-
111
- ## OpenAI Usage via API
112
-
113
- ```python
114
- #!/usr/bin/env python3
115
- """Check OpenAI API usage and costs for current month."""
116
- import os, json, urllib.request
117
- from datetime import date
118
-
119
- API_KEY = os.environ.get("OPENAI_API_KEY", "")
120
- if not API_KEY:
121
- print("OPENAI_API_KEY not set")
122
- exit(1)
123
-
124
- # OpenAI usage endpoint (requires org-level key for detailed breakdown)
125
- today = date.today()
126
- start = f"{today.year}-{today.month:02d}-01"
127
- end = today.isoformat()
128
-
129
- req = urllib.request.Request(
130
- f"https://api.openai.com/v1/usage?date={today.isoformat()}",
131
- headers={"Authorization": f"Bearer {API_KEY}"}
132
- )
133
- try:
134
- resp = json.loads(urllib.request.urlopen(req).read())
135
- # Parse usage data
136
- data = resp.get("data", [])
137
- total_requests = sum(d.get("n_requests", 0) for d in data)
138
- total_tokens = sum(d.get("n_context_tokens_total", 0) + d.get("n_generated_tokens_total", 0) for d in data)
139
- print(f"OpenAI Usage — {today}")
140
- print(f"Requests: {total_requests:,}")
141
- print(f"Tokens: {total_tokens:,}")
142
- print("\nNote: Cost data available in OpenAI dashboard → Usage")
143
- except Exception as e:
144
- print(f"Could not fetch OpenAI usage: {e}")
145
- print("Check usage at: https://platform.openai.com/usage")
146
- ```
147
-
148
- ## Cost Optimization Suggestions
149
-
150
- When a user asks about costs, always include optimization tips:
151
-
11
+ curl -s http://localhost:8081/costs/today
152
12
  ```
153
- 💡 Cost Optimization Tips:
154
13
 
155
- 1. **Model routing** Use CODE_MODEL=anthropic:claude-sonnet-4-6 for code,
156
- RESEARCH_MODEL=google:gemini-2.0-flash for research (fast + cheap).
157
- Simple tasks don't need the most expensive model.
14
+ Cost logs: `data/costs/YYYY-MM-DD.jsonl` - each entry has `modelId`, `estimatedCost`, `inputTokens`, `outputTokens`, `tenantId`.
158
15
 
159
- 2. **Sub-agent profiles** spawnAgent with profile="researcher" uses the
160
- cheaper RESEARCH_MODEL, not the default expensive model.
16
+ ## Model cost reference (2026)
161
17
 
162
- 3. **Per-tenant limits** Set maxDailyCost per user to prevent runaway spending:
163
- `daemora tenant set <id> maxDailyCost 1.00`
164
-
165
- 4. **Global daily cap** — Set MAX_DAILY_COST=5.00 in .env
166
-
167
- 5. **Task cost cap** — Set MAX_COST_PER_TASK=0.25 to kill expensive tasks early
168
-
169
- 6. **Check cost/task** — Use /costs/today to see which tasks are expensive,
170
- then route those to cheaper models
171
- ```
172
-
173
- ## Cost Estimates (approximate, 2026 pricing)
174
-
175
- | Model | Input (per 1M) | Output (per 1M) | Good for |
176
- |-------|---------------|-----------------|---------|
18
+ | Model | Input/1M | Output/1M | Best for |
19
+ |-------|---------|----------|---------|
177
20
  | gpt-4.1-mini | $0.15 | $0.60 | Most tasks |
178
21
  | gpt-4.1 | $2.00 | $8.00 | Complex reasoning |
179
22
  | claude-sonnet-4-6 | $3.00 | $15.00 | Code, analysis |
180
23
  | claude-opus-4-6 | $15.00 | $75.00 | Hard problems only |
181
24
  | gemini-2.0-flash | $0.075 | $0.30 | Research, summaries |
182
25
  | gemini-2.5-pro | $1.25 | $10.00 | Long context |
26
+
27
+ ## Cost optimization tips
28
+
29
+ Always include these when a user asks about costs:
30
+
31
+ 1. **Route by task type** - set `CODE_MODEL`, `RESEARCH_MODEL` in `.env` for automatic routing
32
+ 2. **Sub-agent profiles** - `spawnAgent` with `profile="researcher"` uses `RESEARCH_MODEL` automatically
33
+ 3. **Per-tenant limits** - `daemora tenant set <id> maxDailyCost 1.00`
34
+ 4. **Global daily cap** - `MAX_DAILY_COST=5.00` in `.env`
35
+ 5. **Per-task cap** - `MAX_COST_PER_TASK=0.25` to kill expensive tasks early
@@ -1,207 +1,61 @@
1
1
  ---
2
2
  name: obsidian
3
- description: Read, create, edit, search, and manage notes in an Obsidian vault. Use when the user asks to create a note, find a note, update an Obsidian file, link notes, search their vault, or manage their knowledge base. Obsidian vaults are plain Markdown files — no special tools needed for basic operations.
4
- triggers: obsidian, note, vault, knowledge base, zettelkasten, markdown note, create note, find note, link note, obsidian search, obsidian plugin
3
+ description: Read, create, edit, search, and manage notes in an Obsidian vault. Use when the user asks to create a note, find a note, update an Obsidian file, search the vault, or manage their knowledge base.
4
+ triggers: obsidian, note, vault, knowledge base, zettelkasten, markdown note, create note, find note, link note, obsidian search
5
+ metadata: {"daemora": {"emoji": "📓", "os": ["darwin"]}}
5
6
  ---
6
7
 
7
- ## When to Use
8
-
9
- ✅ Create notes, read notes, search vault, edit frontmatter, create links between notes, list all notes, tag management, daily notes
10
-
11
- ❌ Obsidian UI automation (open a specific note visually) — use `obsidian://` URI scheme for that
12
-
13
- ## Find the Vault Location
8
+ ## Find vault location
14
9
 
15
10
  ```bash
16
- # Obsidian tracks vaults here on macOS:
17
11
  python3 -c "
18
12
  import json, pathlib
19
13
  config = pathlib.Path.home() / 'Library/Application Support/obsidian/obsidian.json'
20
- if config.exists():
21
- data = json.loads(config.read_text())
22
- for vault in data.get('vaults', {}).values():
23
- status = '✅ (open)' if vault.get('open') else ''
24
- print(f\"{vault['path']} {status}\")
25
- else:
26
- print('Obsidian config not found — vault location unknown')
14
+ for v in json.loads(config.read_text()).get('vaults', {}).values():
15
+ print(v['path'], '(open)' if v.get('open') else '')
27
16
  "
28
- # Typical locations: ~/Documents/Obsidian/, ~/Notes/, ~/Documents/Notes/
17
+ # Common locations: ~/Documents/Obsidian/, ~/Notes/
29
18
  ```
30
19
 
31
- ## Read Notes
32
-
33
- ```bash
34
- # Find all notes in vault
35
- find ~/Documents/Obsidian -name "*.md" | head -20
36
-
37
- # Search by content
38
- grep -r "search term" ~/Documents/Obsidian --include="*.md" -l
20
+ Set `VAULT` to the path once found.
39
21
 
40
- # Search with context (2 lines around match)
41
- grep -r "search term" ~/Documents/Obsidian --include="*.md" -C 2
22
+ ## Read notes
42
23
 
43
- # Find notes with a specific tag
44
- grep -r "^tags:.*tagname\|#tagname" ~/Documents/Obsidian --include="*.md" -l
45
-
46
- # Find notes created today (macOS)
47
- find ~/Documents/Obsidian -name "*.md" -newer $(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "1 day ago" +%Y-%m-%d) 2>/dev/null
24
+ ```bash
25
+ find $VAULT -name "*.md" | head -20 # list all notes
26
+ grep -r "search term" $VAULT --include="*.md" -l # search by content
27
+ grep -r "#tagname" $VAULT --include="*.md" -l # find by tag
48
28
  ```
49
29
 
50
- ## Create a Note
30
+ ## Create a note
51
31
 
52
- ```python
53
- #!/usr/bin/env python3
54
- from pathlib import Path
55
- from datetime import datetime
32
+ Use the `writeFile` tool to create `$VAULT/Folder/Note Title.md` with YAML frontmatter:
56
33
 
57
- VAULT = Path.home() / "Documents/Obsidian" # adjust to actual vault path
58
-
59
- def create_note(title: str, content: str, folder: str = "", tags: list = None):
60
- """Create a note with YAML frontmatter."""
61
- tags = tags or []
62
- date = datetime.now().strftime("%Y-%m-%d")
63
- time = datetime.now().strftime("%H:%M")
64
-
65
- frontmatter = f"""---
66
- title: {title}
67
- date: {date}
68
- time: {time}
69
- tags: [{', '.join(tags)}]
34
+ ```markdown
70
35
  ---
71
-
72
- """
73
- target_dir = VAULT / folder if folder else VAULT
74
- target_dir.mkdir(parents=True, exist_ok=True)
75
-
76
- # Sanitize filename
77
- filename = title.replace("/", "-").replace(":", "-") + ".md"
78
- filepath = target_dir / filename
79
-
80
- # Don't overwrite — append timestamp if exists
81
- if filepath.exists():
82
- ts = datetime.now().strftime("%Y%m%d%H%M%S")
83
- filepath = target_dir / f"{title} ({ts}).md"
84
-
85
- filepath.write_text(frontmatter + content, encoding="utf-8")
86
- print(f"Created: {filepath}")
87
- return filepath
88
-
89
- # Example usage:
90
- create_note(
91
- title="Meeting Notes — Q1 Review",
92
- content="## Attendees\n- Alice\n- Bob\n\n## Key Points\n- ...",
93
- folder="Meetings",
94
- tags=["meeting", "q1-2026"]
95
- )
96
- ```
97
-
98
- ## Create Daily Note
99
-
100
- ```python
101
- #!/usr/bin/env python3
102
- from pathlib import Path
103
- from datetime import datetime
104
-
105
- VAULT = Path.home() / "Documents/Obsidian"
106
- DAILY_FOLDER = VAULT / "Daily Notes"
107
- DAILY_FOLDER.mkdir(parents=True, exist_ok=True)
108
-
109
- today = datetime.now()
110
- filename = today.strftime("%Y-%m-%d") + ".md"
111
- filepath = DAILY_FOLDER / filename
112
-
113
- if not filepath.exists():
114
- content = f"""---
115
- date: {today.strftime('%Y-%m-%d')}
116
- tags: [daily]
36
+ title: Note Title
37
+ date: 2026-03-03
38
+ tags: [tag1, tag2]
117
39
  ---
118
40
 
119
- # {today.strftime('%A, %B %d, %Y')}
120
-
121
- ## 🎯 Focus Today
122
-
123
- ## 📝 Notes
124
-
125
- ## ✅ Tasks
126
- - [ ]
127
-
128
- ## 💭 Reflections
129
-
130
- """
131
- filepath.write_text(content)
132
- print(f"Created daily note: {filepath}")
133
- else:
134
- print(f"Daily note already exists: {filepath}")
41
+ # Note Title
135
42
 
136
- # Open it in Obsidian
137
- import subprocess
138
- subprocess.run(["open", f"obsidian://open?vault=Obsidian&file=Daily Notes/{today.strftime('%Y-%m-%d')}"])
43
+ Content here. Link to other notes with [[Other Note]].
139
44
  ```
140
45
 
141
- ## Edit Existing Note
142
-
143
- ```python
144
- #!/usr/bin/env python3
145
- from pathlib import Path
146
-
147
- def append_to_note(note_path: str, content: str, section: str = None):
148
- """Append content to a note, optionally under a specific section."""
149
- path = Path(note_path)
150
- existing = path.read_text(encoding="utf-8")
151
-
152
- if section:
153
- # Insert under the section heading
154
- marker = f"## {section}"
155
- if marker in existing:
156
- idx = existing.index(marker) + len(marker)
157
- # Find next line after heading
158
- next_line = existing.find("\n", idx) + 1
159
- updated = existing[:next_line] + "\n" + content + "\n" + existing[next_line:]
160
- else:
161
- updated = existing + f"\n## {section}\n\n{content}\n"
162
- else:
163
- updated = existing.rstrip() + f"\n\n{content}\n"
164
-
165
- path.write_text(updated, encoding="utf-8")
166
- print(f"Updated: {path}")
167
-
168
- # Example: append a task under ## ✅ Tasks
169
- append_to_note(
170
- "/path/to/note.md",
171
- "- [ ] New task item",
172
- section="✅ Tasks"
173
- )
174
- ```
175
-
176
- ## Create Linked Notes (Wiki Links)
46
+ ## Daily note
177
47
 
178
- Obsidian uses `[[Note Title]]` syntax for links. When creating a note that references others:
48
+ Create `$VAULT/Daily Notes/YYYY-MM-DD.md` with today's date.
179
49
 
180
- ```python
181
- # Create a note with backlinks
182
- content = """
183
- ## Related Research
184
-
185
- See [[Market Analysis 2026]] for context.
186
- Building on [[Previous Meeting Notes]].
187
-
188
- ## Summary
189
- ...
190
- """
191
- ```
192
-
193
- ## Open Note in Obsidian (macOS)
50
+ ## Open in Obsidian (macOS)
194
51
 
195
52
  ```bash
196
- # Open specific note
197
53
  open "obsidian://open?vault=VAULT_NAME&file=PATH/TO/NOTE"
198
-
199
- # Open search in Obsidian
200
54
  open "obsidian://search?vault=VAULT_NAME&query=search+term"
201
55
  ```
202
56
 
203
- ## Error Handling
57
+ ## Errors
204
58
 
205
- - **Vault not found**: read `~/Library/Application Support/obsidian/obsidian.json` to locate vaults; ask user to confirm vault path if multiple exist
206
- - **File already exists**: always check before writing; either append content or create new file with timestamp suffix
207
- - **Encoding issues**: always use `encoding="utf-8"` when reading/writing `.md` files
59
+ - **Vault not found** read `~/Library/Application Support/obsidian/obsidian.json` for vault paths
60
+ - **File already exists** append content or create new file with timestamp suffix
61
+ - **Encoding** always use UTF-8 when writing `.md` files
package/skills/pdf.md CHANGED
@@ -1,211 +1,60 @@
1
1
  ---
2
2
  name: pdf
3
- description: Create, read, extract text from, merge, split, rotate, compress, and manipulate PDF files. Use when the user asks to create a PDF report/document, extract text from a PDF, merge PDFs, convert to PDF, or do any PDF operation. Works on macOS and Linux without paid tools.
3
+ description: Create, read, extract text from, merge, split, compress, and manipulate PDF files. Use when the user asks to create a PDF, extract text from a PDF, merge PDFs, convert to PDF, or do any PDF operation.
4
4
  triggers: pdf, create pdf, read pdf, extract pdf, merge pdf, split pdf, convert to pdf, pdf report, pdf document, compress pdf, rotate pdf
5
+ metadata: {"daemora": {"emoji": "📄", "requires": {"anyBins": ["wkhtmltopdf", "pandoc", "pdftotext"]}, "install": ["brew install wkhtmltopdf"]}}
5
6
  ---
6
7
 
7
- ## When to Use
8
-
9
- ✅ Create PDF reports/documents, extract text from PDFs, merge multiple PDFs, split pages, rotate, compress, convert HTML/Markdown to PDF
10
-
11
- ❌ PDF form filling with complex logic → use PyPDF2 + pdfrw; PDF digital signatures → use specialized tools
12
-
13
- ## Method 1: Create PDF from HTML (best quality, macOS)
8
+ ## Create PDF from HTML (best quality)
14
9
 
15
10
  ```bash
16
- # Install wkhtmltopdf (one-time)
17
11
  brew install wkhtmltopdf
18
-
19
- # Create HTML file, then convert
20
- cat > /tmp/report.html << 'EOF'
21
- <!DOCTYPE html>
22
- <html>
23
- <head>
24
- <meta charset="UTF-8">
25
- <style>
26
- body { font-family: -apple-system, Arial, sans-serif; margin: 40px; color: #222; }
27
- h1 { color: #1a1a2e; border-bottom: 2px solid #eee; padding-bottom: 10px; }
28
- h2 { color: #444; margin-top: 30px; }
29
- table { border-collapse: collapse; width: 100%; margin: 16px 0; }
30
- th { background: #f0f4ff; padding: 10px; text-align: left; border: 1px solid #ddd; }
31
- td { padding: 8px 10px; border: 1px solid #eee; }
32
- .highlight { background: #fffbea; padding: 12px; border-left: 4px solid #f0c040; }
33
- </style>
34
- </head>
35
- <body>
36
- <h1>Report Title</h1>
37
- <p>Content here</p>
38
- </body>
39
- </html>
40
- EOF
41
-
42
- wkhtmltopdf \
43
- --page-size A4 \
44
- --margin-top 20mm --margin-bottom 20mm \
45
- --margin-left 15mm --margin-right 15mm \
46
- --encoding UTF-8 \
47
- --footer-center "[page] / [topage]" \
48
- --footer-font-size 9 \
12
+ wkhtmltopdf --page-size A4 --margin-top 20mm --margin-bottom 20mm \
13
+ --encoding UTF-8 --footer-center "[page] / [topage]" \
49
14
  /tmp/report.html /tmp/report.pdf
50
-
51
- echo "Created: /tmp/report.pdf"
52
- open /tmp/report.pdf # preview on macOS
53
15
  ```
54
16
 
55
- ## Method 2: Create PDF with Python (cross-platform, no external binaries)
56
-
57
- ```python
58
- #!/usr/bin/env python3
59
- """Create a PDF without any system binaries — uses reportlab."""
60
- # Install once: pip install reportlab
61
- from reportlab.lib.pagesizes import A4
62
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
63
- from reportlab.lib.units import mm
64
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
65
- from reportlab.lib import colors
17
+ ## Create PDF with Python (no binary needed)
66
18
 
67
- output_path = "/tmp/report.pdf"
68
- doc = SimpleDocTemplate(output_path, pagesize=A4,
69
- leftMargin=20*mm, rightMargin=20*mm,
70
- topMargin=20*mm, bottomMargin=20*mm)
71
-
72
- styles = getSampleStyleSheet()
73
- story = []
74
-
75
- # Title
76
- title_style = ParagraphStyle('Title', parent=styles['Title'],
77
- fontSize=24, spaceAfter=12)
78
- story.append(Paragraph("Report Title", title_style))
79
- story.append(Spacer(1, 8*mm))
80
-
81
- # Body text
82
- story.append(Paragraph("Your content here. Supports <b>bold</b>, <i>italic</i>, and links.", styles['BodyText']))
83
- story.append(Spacer(1, 6*mm))
84
-
85
- # Table
86
- data = [['Column A', 'Column B', 'Column C'],
87
- ['Row 1', 'Value', '$100'],
88
- ['Row 2', 'Value', '$200']]
89
- table = Table(data, colWidths=[60*mm, 60*mm, 40*mm])
90
- table.setStyle(TableStyle([
91
- ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#e8eaf6')),
92
- ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
93
- ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#cccccc')),
94
- ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor('#f8f9ff')]),
95
- ('PADDING', (0,0), (-1,-1), 8),
96
- ]))
97
- story.append(table)
98
-
99
- doc.build(story)
100
- print(f"Created: {output_path}")
19
+ ```bash
20
+ pip install reportlab
21
+ # Use SimpleDocTemplate, Paragraph, Table from reportlab.platypus
101
22
  ```
102
23
 
103
- ## Read / Extract Text from PDF
104
-
105
- ```python
106
- #!/usr/bin/env python3
107
- # pip install pdfplumber (better than pypdf2 for text extraction)
108
- import pdfplumber, sys
24
+ ## Extract text from PDF
109
25
 
110
- pdf_path = sys.argv[1] if len(sys.argv) > 1 else "/path/to/file.pdf"
111
-
112
- with pdfplumber.open(pdf_path) as pdf:
113
- print(f"Pages: {len(pdf.pages)}")
114
- for i, page in enumerate(pdf.pages, 1):
115
- text = page.extract_text()
116
- if text:
117
- print(f"\n--- Page {i} ---\n{text}")
118
-
119
- # Also extract tables
120
- tables = page.extract_tables()
121
- for t in tables:
122
- print(f"\n[Table on page {i}]")
123
- for row in t:
124
- print(" | ".join(str(c or '') for c in row))
26
+ ```bash
27
+ pip install pdfplumber
28
+ python3 -c "import pdfplumber; pdf=pdfplumber.open('file.pdf'); print(pdf.pages[0].extract_text())"
29
+ # Or: brew install poppler && pdftotext file.pdf -
125
30
  ```
126
31
 
127
32
  ## Merge PDFs
128
33
 
129
- ```python
130
- #!/usr/bin/env python3
131
- # pip install pypdf
132
- from pypdf import PdfWriter, PdfReader
133
- import sys
134
-
135
- output = "/tmp/merged.pdf"
136
- writer = PdfWriter()
137
-
138
- for path in sys.argv[1:]:
139
- reader = PdfReader(path)
140
- for page in reader.pages:
141
- writer.add_page(page)
142
-
143
- with open(output, "wb") as f:
144
- writer.write(f)
145
- print(f"Merged {len(sys.argv)-1} PDFs → {output}")
146
- ```
147
-
148
- ## Split PDF (extract page range)
149
-
150
- ```python
151
- #!/usr/bin/env python3
152
- # pip install pypdf
153
- from pypdf import PdfWriter, PdfReader
154
-
155
- input_pdf = "/path/to/input.pdf"
156
- output_pdf = "/tmp/extracted_pages.pdf"
157
- start_page = 0 # 0-indexed
158
- end_page = 5 # exclusive
159
-
160
- reader = PdfReader(input_pdf)
161
- writer = PdfWriter()
162
- for i in range(start_page, min(end_page, len(reader.pages))):
163
- writer.add_page(reader.pages[i])
164
- with open(output_pdf, "wb") as f:
165
- writer.write(f)
166
- print(f"Extracted pages {start_page+1}-{end_page} → {output_pdf}")
34
+ ```bash
35
+ pip install pypdf
36
+ # PdfWriter + PdfReader from pypdf
37
+ # Or with ghostscript:
38
+ gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=merged.pdf a.pdf b.pdf
167
39
  ```
168
40
 
169
- ## Compress PDF (macOS, no install)
41
+ ## Markdown PDF
170
42
 
171
43
  ```bash
172
- # Uses macOS built-in Quartz filter
173
- "/System/Library/Printers/Libraries/quartzfilter" \
174
- /System/Library/Filters/Reduce\ File\ Size.qfilter \
175
- /path/to/input.pdf /tmp/compressed.pdf
176
-
177
- # Or via Python with ghostscript
178
- gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.5 \
179
- -dPDFSETTINGS=/ebook \
180
- -dNOPAUSE -dQUIET -dBATCH \
181
- -sOutputFile=/tmp/compressed.pdf /path/to/input.pdf
182
- # PDFSET options: /screen (72dpi) /ebook (150dpi) /printer (300dpi) /prepress (300dpi HQ)
44
+ brew install pandoc
45
+ pandoc input.md -o output.pdf --pdf-engine=wkhtmltopdf -V geometry:margin=25mm
183
46
  ```
184
47
 
185
- ## Convert Markdown → PDF
48
+ ## Compress PDF (macOS)
186
49
 
187
50
  ```bash
188
- # pip install md-to-pdf OR brew install pandoc
189
- # With pandoc (best quality):
190
- pandoc input.md -o output.pdf \
191
- --pdf-engine=wkhtmltopdf \
192
- -V geometry:margin=25mm \
193
- -V fontsize=11pt
194
-
195
- # Quick and dirty with Python:
196
- pip install markdown weasyprint
197
- python3 -c "
198
- import markdown, weasyprint, pathlib
199
- md = pathlib.Path('input.md').read_text()
200
- html = markdown.markdown(md, extensions=['tables','fenced_code'])
201
- weasyprint.HTML(string=f'<html><body style=\"font-family:sans-serif;margin:40px\">{html}</body></html>').write_pdf('output.pdf')
202
- print('output.pdf')
203
- "
51
+ gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.5 -dPDFSETTINGS=/ebook \
52
+ -dNOPAUSE -dQUIET -dBATCH -sOutputFile=/tmp/compressed.pdf input.pdf
53
+ # PDFSET options: /screen (72dpi) /ebook (150dpi) /printer (300dpi) /prepress (HQ)
204
54
  ```
205
55
 
206
- ## After Creating PDF
56
+ ## After creating
207
57
 
208
- 1. Report the output path: "PDF saved to `/tmp/report.pdf`"
209
- 2. On macOS, offer to open it: `executeCommand("open /tmp/report.pdf")`
210
- 3. If the user wants to send it, use `sendFile("/tmp/report.pdf", channel, sessionId)`
211
- 4. For large reports: mention page count and file size
58
+ 1. Report the path: "PDF saved to `/tmp/report.pdf`"
59
+ 2. On macOS: `executeCommand("open /tmp/report.pdf")` to preview
60
+ 3. To send: `sendFile("/tmp/report.pdf", channel, sessionId)`