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.
- package/README.md +106 -76
- package/SOUL.md +100 -28
- package/config/mcp.json +9 -9
- package/package.json +15 -8
- package/skills/apple-notes.md +0 -52
- package/skills/apple-reminders.md +1 -87
- package/skills/camsnap.md +20 -144
- package/skills/coding.md +7 -7
- package/skills/documents.md +6 -6
- package/skills/email.md +6 -6
- package/skills/gif-search.md +28 -171
- package/skills/healthcheck.md +21 -203
- package/skills/image-gen.md +24 -123
- package/skills/model-usage.md +18 -165
- package/skills/obsidian.md +28 -174
- package/skills/pdf.md +30 -181
- package/skills/research.md +6 -6
- package/skills/skill-creator.md +35 -111
- package/skills/spotify.md +2 -17
- package/skills/summarize.md +36 -193
- package/skills/things.md +23 -175
- package/skills/tmux.md +1 -91
- package/skills/trello.md +32 -157
- package/skills/video-frames.md +26 -166
- package/skills/weather.md +6 -6
- package/src/a2a/A2AClient.js +2 -2
- package/src/a2a/A2AServer.js +6 -6
- package/src/a2a/AgentCard.js +2 -2
- package/src/agents/SubAgentManager.js +61 -19
- package/src/agents/Supervisor.js +4 -4
- package/src/channels/BaseChannel.js +6 -6
- package/src/channels/BlueBubblesChannel.js +112 -0
- package/src/channels/DiscordChannel.js +8 -8
- package/src/channels/EmailChannel.js +54 -26
- package/src/channels/FeishuChannel.js +140 -0
- package/src/channels/GoogleChatChannel.js +8 -8
- package/src/channels/HttpChannel.js +2 -2
- package/src/channels/IRCChannel.js +144 -0
- package/src/channels/LineChannel.js +13 -13
- package/src/channels/MatrixChannel.js +97 -0
- package/src/channels/MattermostChannel.js +119 -0
- package/src/channels/NextcloudChannel.js +133 -0
- package/src/channels/NostrChannel.js +175 -0
- package/src/channels/SignalChannel.js +9 -9
- package/src/channels/SlackChannel.js +10 -10
- package/src/channels/TeamsChannel.js +10 -10
- package/src/channels/TelegramChannel.js +8 -8
- package/src/channels/TwitchChannel.js +128 -0
- package/src/channels/WhatsAppChannel.js +10 -10
- package/src/channels/ZaloChannel.js +119 -0
- package/src/channels/iMessageChannel.js +150 -0
- package/src/channels/index.js +241 -11
- package/src/cli.js +835 -38
- package/src/config/agentProfiles.js +19 -19
- package/src/config/channels.js +1 -1
- package/src/config/default.js +12 -7
- package/src/config/models.js +3 -3
- package/src/config/permissions.js +2 -2
- package/src/core/AgentLoop.js +13 -13
- package/src/core/Compaction.js +3 -3
- package/src/core/CostTracker.js +2 -2
- package/src/core/EventBus.js +15 -15
- package/src/core/TaskQueue.js +24 -7
- package/src/core/TaskRunner.js +19 -6
- package/src/daemon/DaemonManager.js +4 -4
- package/src/hooks/HookRunner.js +4 -4
- package/src/index.js +6 -2
- package/src/mcp/MCPAgentRunner.js +3 -3
- package/src/mcp/MCPClient.js +9 -9
- package/src/mcp/MCPManager.js +14 -14
- package/src/models/ModelRouter.js +2 -2
- package/src/safety/AuditLog.js +3 -3
- package/src/safety/CircuitBreaker.js +2 -2
- package/src/safety/CommandGuard.js +132 -0
- package/src/safety/FilesystemGuard.js +23 -3
- package/src/safety/GitRollback.js +5 -5
- package/src/safety/HumanApproval.js +9 -9
- package/src/safety/InputSanitizer.js +81 -8
- package/src/safety/PermissionGuard.js +2 -2
- package/src/safety/Sandbox.js +1 -1
- package/src/safety/SecretScanner.js +90 -28
- package/src/safety/SecretVault.js +2 -2
- package/src/scheduler/Heartbeat.js +3 -3
- package/src/scheduler/Scheduler.js +6 -6
- package/src/setup/theme.js +171 -66
- package/src/setup/wizard.js +432 -57
- package/src/skills/SkillLoader.js +145 -8
- package/src/storage/TaskStore.js +39 -15
- package/src/systemPrompt.js +45 -43
- package/src/tenants/TenantManager.js +79 -22
- package/src/tools/ToolRegistry.js +3 -3
- package/src/tools/applyPatch.js +2 -2
- package/src/tools/browserAutomation.js +4 -4
- package/src/tools/calendar.js +155 -0
- package/src/tools/clipboard.js +71 -0
- package/src/tools/contacts.js +138 -0
- package/src/tools/createDocument.js +2 -2
- package/src/tools/cronTool.js +14 -14
- package/src/tools/database.js +165 -0
- package/src/tools/editFile.js +10 -10
- package/src/tools/executeCommand.js +11 -3
- package/src/tools/generateImage.js +79 -0
- package/src/tools/gitTool.js +141 -0
- package/src/tools/glob.js +1 -1
- package/src/tools/googlePlaces.js +136 -0
- package/src/tools/grep.js +2 -2
- package/src/tools/iMessageTool.js +86 -0
- package/src/tools/imageAnalysis.js +3 -3
- package/src/tools/index.js +56 -2
- package/src/tools/makeVoiceCall.js +283 -0
- package/src/tools/manageAgents.js +2 -2
- package/src/tools/manageMCP.js +38 -20
- package/src/tools/memory.js +25 -32
- package/src/tools/messageChannel.js +1 -1
- package/src/tools/notification.js +90 -0
- package/src/tools/philipsHue.js +147 -0
- package/src/tools/projectTracker.js +8 -8
- package/src/tools/readFile.js +1 -1
- package/src/tools/readPDF.js +73 -0
- package/src/tools/screenCapture.js +6 -6
- package/src/tools/searchContent.js +2 -2
- package/src/tools/searchFiles.js +1 -1
- package/src/tools/sendEmail.js +79 -24
- package/src/tools/sendFile.js +4 -4
- package/src/tools/sonos.js +137 -0
- package/src/tools/sshTool.js +130 -0
- package/src/tools/textToSpeech.js +5 -5
- package/src/tools/transcribeAudio.js +4 -4
- package/src/tools/useMCP.js +4 -4
- package/src/tools/webFetch.js +2 -2
- package/src/tools/webSearch.js +1 -1
- package/src/utils/Embeddings.js +79 -0
- package/src/voice/VoiceSessionManager.js +170 -0
- package/src/voice/VoiceWebhook.js +188 -0
package/skills/model-usage.md
CHANGED
|
@@ -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.
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
cheaper RESEARCH_MODEL, not the default expensive model.
|
|
16
|
+
## Model cost reference (2026)
|
|
161
17
|
|
|
162
|
-
|
|
163
|
-
|
|
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
|
package/skills/obsidian.md
CHANGED
|
@@ -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,
|
|
4
|
-
triggers: obsidian, note, vault, knowledge base, zettelkasten, markdown note, create note, find note, link note, obsidian search
|
|
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
|
-
##
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
#
|
|
17
|
+
# Common locations: ~/Documents/Obsidian/, ~/Notes/
|
|
29
18
|
```
|
|
30
19
|
|
|
31
|
-
|
|
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
|
-
|
|
41
|
-
grep -r "search term" ~/Documents/Obsidian --include="*.md" -C 2
|
|
22
|
+
## Read notes
|
|
42
23
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
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
|
|
30
|
+
## Create a note
|
|
51
31
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
48
|
+
Create `$VAULT/Daily Notes/YYYY-MM-DD.md` with today's date.
|
|
179
49
|
|
|
180
|
-
|
|
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
|
-
##
|
|
57
|
+
## Errors
|
|
204
58
|
|
|
205
|
-
- **Vault not found
|
|
206
|
-
- **File already exists
|
|
207
|
-
- **Encoding
|
|
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,
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
```
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
##
|
|
41
|
+
## Markdown → PDF
|
|
170
42
|
|
|
171
43
|
```bash
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
##
|
|
48
|
+
## Compress PDF (macOS)
|
|
186
49
|
|
|
187
50
|
```bash
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
56
|
+
## After creating
|
|
207
57
|
|
|
208
|
-
1. Report the
|
|
209
|
-
2. On macOS
|
|
210
|
-
3.
|
|
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)`
|