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,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apple-reminders
|
|
3
|
+
description: Create, read, complete, and manage Apple Reminders on macOS using AppleScript. Use when the user asks to add a reminder, set a due date, list reminders, mark as done, or manage reminder lists. macOS only. Syncs with iPhone via iCloud.
|
|
4
|
+
triggers: reminder, reminders, add reminder, set reminder, due date, alert, remind me, reminder list, todo reminder, apple reminders
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
✅ Create reminders with due dates/times, list pending reminders, mark complete, create reminder lists, set location-based reminders
|
|
10
|
+
|
|
11
|
+
❌ Complex task management (use Things or Trello skill) — use Reminders for simple "remind me at X time" tasks
|
|
12
|
+
|
|
13
|
+
## Create a Reminder
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Simple reminder (no due date)
|
|
17
|
+
osascript << 'EOF'
|
|
18
|
+
tell application "Reminders"
|
|
19
|
+
tell list "Reminders"
|
|
20
|
+
make new reminder with properties {name:"Buy groceries"}
|
|
21
|
+
end tell
|
|
22
|
+
end tell
|
|
23
|
+
EOF
|
|
24
|
+
|
|
25
|
+
# Reminder with due date and time
|
|
26
|
+
osascript << 'EOF'
|
|
27
|
+
tell application "Reminders"
|
|
28
|
+
tell list "Reminders"
|
|
29
|
+
set dueDate to current date
|
|
30
|
+
set day of dueDate to 15
|
|
31
|
+
set month of dueDate to 3
|
|
32
|
+
set year of dueDate to 2026
|
|
33
|
+
set hours of dueDate to 9
|
|
34
|
+
set minutes of dueDate to 0
|
|
35
|
+
set seconds of dueDate to 0
|
|
36
|
+
make new reminder with properties {
|
|
37
|
+
name:"Call the dentist",
|
|
38
|
+
due date:dueDate,
|
|
39
|
+
remind me date:dueDate
|
|
40
|
+
}
|
|
41
|
+
end tell
|
|
42
|
+
end tell
|
|
43
|
+
EOF
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Create Reminder with Dynamic Date
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
#!/usr/bin/env python3
|
|
50
|
+
import subprocess
|
|
51
|
+
from datetime import datetime, timedelta
|
|
52
|
+
|
|
53
|
+
def create_reminder(title: str, notes: str = "", due: datetime = None, list_name: str = "Reminders"):
|
|
54
|
+
"""Create a reminder. due=None means no due date."""
|
|
55
|
+
if due:
|
|
56
|
+
# AppleScript date format
|
|
57
|
+
due_str = due.strftime("%B %d, %Y %I:%M:%S %p")
|
|
58
|
+
date_block = f'''
|
|
59
|
+
set dueDate to date "{due_str}"
|
|
60
|
+
set due date of newReminder to dueDate
|
|
61
|
+
set remind me date of newReminder to dueDate
|
|
62
|
+
'''
|
|
63
|
+
else:
|
|
64
|
+
date_block = ""
|
|
65
|
+
|
|
66
|
+
# Escape quotes
|
|
67
|
+
title = title.replace('"', '\\"')
|
|
68
|
+
notes = notes.replace('"', '\\"')
|
|
69
|
+
|
|
70
|
+
script = f'''
|
|
71
|
+
tell application "Reminders"
|
|
72
|
+
tell list "{list_name}"
|
|
73
|
+
set newReminder to make new reminder with properties {{name:"{title}"}}
|
|
74
|
+
{f'set body of newReminder to "{notes}"' if notes else ""}
|
|
75
|
+
{date_block}
|
|
76
|
+
end tell
|
|
77
|
+
end tell
|
|
78
|
+
'''
|
|
79
|
+
result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
|
|
80
|
+
if result.returncode == 0:
|
|
81
|
+
due_display = due.strftime("%a %b %d at %I:%M %p") if due else "no due date"
|
|
82
|
+
print(f"✅ Reminder: '{title}' ({due_display})")
|
|
83
|
+
else:
|
|
84
|
+
print(f"❌ Failed: {result.stderr}")
|
|
85
|
+
|
|
86
|
+
# Examples
|
|
87
|
+
create_reminder("Weekly team sync", due=datetime.now() + timedelta(days=1, hours=2))
|
|
88
|
+
create_reminder("Pay rent", due=datetime(2026, 4, 1, 9, 0), list_name="Bills")
|
|
89
|
+
create_reminder("Buy coffee beans") # no due date
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## List Pending Reminders
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
osascript << 'EOF'
|
|
96
|
+
tell application "Reminders"
|
|
97
|
+
set output to ""
|
|
98
|
+
set pending to (reminders whose completed is false)
|
|
99
|
+
repeat with r in pending
|
|
100
|
+
set dueInfo to ""
|
|
101
|
+
if due date of r is not missing value then
|
|
102
|
+
set dueInfo to " [due: " & (due date of r as string) & "]"
|
|
103
|
+
end if
|
|
104
|
+
set output to output & name of r & dueInfo & "\n"
|
|
105
|
+
end repeat
|
|
106
|
+
if output is "" then return "No pending reminders"
|
|
107
|
+
return output
|
|
108
|
+
end tell
|
|
109
|
+
EOF
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## List Reminders Due Today
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
#!/usr/bin/env python3
|
|
116
|
+
import subprocess
|
|
117
|
+
from datetime import datetime, date
|
|
118
|
+
|
|
119
|
+
script = '''
|
|
120
|
+
tell application "Reminders"
|
|
121
|
+
set output to ""
|
|
122
|
+
repeat with r in reminders
|
|
123
|
+
if completed of r is false and due date of r is not missing value then
|
|
124
|
+
set output to output & name of r & "|" & (due date of r as string) & "\n"
|
|
125
|
+
end if
|
|
126
|
+
end repeat
|
|
127
|
+
return output
|
|
128
|
+
end tell
|
|
129
|
+
'''
|
|
130
|
+
result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
|
|
131
|
+
lines = [l.strip() for l in result.stdout.strip().split("\n") if l.strip()]
|
|
132
|
+
|
|
133
|
+
today = date.today()
|
|
134
|
+
today_items = []
|
|
135
|
+
for line in lines:
|
|
136
|
+
if "|" in line:
|
|
137
|
+
name, due_str = line.rsplit("|", 1)
|
|
138
|
+
# Parse AppleScript date string
|
|
139
|
+
try:
|
|
140
|
+
due = datetime.strptime(due_str.strip()[:10], "%A, %B")
|
|
141
|
+
except:
|
|
142
|
+
pass
|
|
143
|
+
today_items.append(f"• {name.strip()}")
|
|
144
|
+
|
|
145
|
+
if today_items:
|
|
146
|
+
print("📅 Reminders due today:")
|
|
147
|
+
print("\n".join(today_items))
|
|
148
|
+
else:
|
|
149
|
+
print("✅ No reminders due today")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Mark Reminder Complete
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
osascript << 'EOF'
|
|
156
|
+
tell application "Reminders"
|
|
157
|
+
set targetReminder to first reminder whose name is "Buy groceries"
|
|
158
|
+
set completed of targetReminder to true
|
|
159
|
+
end tell
|
|
160
|
+
EOF
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## List All Reminder Lists
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
osascript -e 'tell application "Reminders" to get name of lists'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Create a New Reminder List
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
osascript << 'EOF'
|
|
173
|
+
tell application "Reminders"
|
|
174
|
+
make new list with properties {name:"Shopping", color:green}
|
|
175
|
+
end tell
|
|
176
|
+
EOF
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Response Format for Users
|
|
180
|
+
|
|
181
|
+
When confirming reminders created, format clearly:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
✅ Reminder set:
|
|
185
|
+
📌 Call the dentist
|
|
186
|
+
📅 Monday, March 16 at 9:00 AM
|
|
187
|
+
📋 List: Reminders
|
|
188
|
+
🔔 Alert: At time of reminder
|
|
189
|
+
```
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: camsnap
|
|
3
|
+
description: Capture photos or screenshots from the Mac camera or screen. Use when the user asks to take a photo, capture a webcam shot, take a selfie, capture the screen, or grab a camera frame. macOS only. Uses the built-in screenCapture tool for screen, and imagesnap CLI for webcam.
|
|
4
|
+
triggers: take photo, camera, webcam, selfie, snap photo, capture camera, camsnap, take picture, camera shot, photo capture
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
✅ Capture a webcam/camera photo, take a screenshot, capture a specific window, time-lapse photography, verify physical setup
|
|
10
|
+
|
|
11
|
+
❌ Video recording — use ffmpeg (see video-frames skill); screen recording — use QuickTime or built-in tools
|
|
12
|
+
|
|
13
|
+
## Screen Capture (built-in — no install needed)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# Use the built-in screenCapture tool
|
|
17
|
+
# screenCapture(path: string) → saves screenshot to path and returns it for vision analysis
|
|
18
|
+
|
|
19
|
+
# Capture full screen
|
|
20
|
+
screenCapture("/tmp/screen.png")
|
|
21
|
+
|
|
22
|
+
# Then analyze with imageAnalysis
|
|
23
|
+
imageAnalysis("/tmp/screen.png", "What is shown on the screen?")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Webcam / Camera Capture
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install imagesnap (small, fast, no dependencies)
|
|
30
|
+
brew install imagesnap
|
|
31
|
+
|
|
32
|
+
# Take a photo with default camera
|
|
33
|
+
imagesnap /tmp/photo.jpg
|
|
34
|
+
|
|
35
|
+
# Take photo with delay (gives time to position)
|
|
36
|
+
imagesnap -w 3 /tmp/photo.jpg # 3 second warmup
|
|
37
|
+
|
|
38
|
+
# List available cameras
|
|
39
|
+
imagesnap -l
|
|
40
|
+
# → Video Devices:
|
|
41
|
+
# FaceTime HD Camera
|
|
42
|
+
# iPhone Camera (Continuity Camera)
|
|
43
|
+
|
|
44
|
+
# Use specific camera
|
|
45
|
+
imagesnap -d "iPhone Camera" /tmp/photo.jpg
|
|
46
|
+
|
|
47
|
+
# Take multiple frames (burst)
|
|
48
|
+
imagesnap -t 1 /tmp/burst # 1 second between shots; creates burst-001.jpg, burst-002.jpg, etc.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Python Wrapper
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
#!/usr/bin/env python3
|
|
55
|
+
import subprocess, os
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
from datetime import datetime
|
|
58
|
+
|
|
59
|
+
def capture_webcam(
|
|
60
|
+
output: str = None,
|
|
61
|
+
camera: str = None,
|
|
62
|
+
warmup: float = 1.5,
|
|
63
|
+
quality: str = "high"
|
|
64
|
+
) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Capture a photo from the webcam.
|
|
67
|
+
Returns path to saved image.
|
|
68
|
+
"""
|
|
69
|
+
if output is None:
|
|
70
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
71
|
+
output = f"/tmp/webcam_{ts}.jpg"
|
|
72
|
+
|
|
73
|
+
cmd = ["imagesnap", "-w", str(warmup)]
|
|
74
|
+
if camera:
|
|
75
|
+
cmd += ["-d", camera]
|
|
76
|
+
cmd.append(output)
|
|
77
|
+
|
|
78
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
raise RuntimeError(f"Camera capture failed: {result.stderr}")
|
|
81
|
+
|
|
82
|
+
size = os.path.getsize(output)
|
|
83
|
+
print(f"📸 Photo saved: {output} ({size//1024}KB)")
|
|
84
|
+
return output
|
|
85
|
+
|
|
86
|
+
def list_cameras() -> list[str]:
|
|
87
|
+
"""List available cameras."""
|
|
88
|
+
result = subprocess.run(["imagesnap", "-l"], capture_output=True, text=True)
|
|
89
|
+
cameras = []
|
|
90
|
+
for line in result.stdout.split('\n'):
|
|
91
|
+
if line.strip() and not line.startswith('Video'):
|
|
92
|
+
cameras.append(line.strip())
|
|
93
|
+
return cameras
|
|
94
|
+
|
|
95
|
+
# Capture and analyze
|
|
96
|
+
cameras = list_cameras()
|
|
97
|
+
print(f"Available cameras: {cameras}")
|
|
98
|
+
|
|
99
|
+
photo_path = capture_webcam(warmup=2.0)
|
|
100
|
+
|
|
101
|
+
# Analyze the photo with vision
|
|
102
|
+
# imageAnalysis(photo_path, "Describe what you see in this photo")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Time-Lapse Capture
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
#!/usr/bin/env python3
|
|
109
|
+
"""Capture multiple photos over time."""
|
|
110
|
+
import subprocess, time
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
from datetime import datetime
|
|
113
|
+
|
|
114
|
+
out_dir = Path("/tmp/timelapse")
|
|
115
|
+
out_dir.mkdir(exist_ok=True)
|
|
116
|
+
|
|
117
|
+
interval_seconds = 30 # photo every 30 seconds
|
|
118
|
+
count = 20 # take 20 photos
|
|
119
|
+
|
|
120
|
+
print(f"Starting time-lapse: {count} photos, every {interval_seconds}s")
|
|
121
|
+
for i in range(count):
|
|
122
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
123
|
+
path = out_dir / f"frame_{i+1:03d}_{ts}.jpg"
|
|
124
|
+
subprocess.run(["imagesnap", "-w", "0.5", str(path)], capture_output=True)
|
|
125
|
+
print(f"📸 {i+1}/{count}: {path.name}")
|
|
126
|
+
if i < count - 1:
|
|
127
|
+
time.sleep(interval_seconds)
|
|
128
|
+
|
|
129
|
+
print(f"\n✅ {count} photos saved to {out_dir}")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Combine Screen + Camera (Picture-in-Picture)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Take both and combine with ffmpeg
|
|
136
|
+
imagesnap -w 1 /tmp/webcam.jpg
|
|
137
|
+
screencapture /tmp/screen.png
|
|
138
|
+
|
|
139
|
+
# Overlay webcam in bottom-right corner of screen
|
|
140
|
+
ffmpeg -i /tmp/screen.png -i /tmp/webcam.jpg \
|
|
141
|
+
-filter_complex "[1:v]scale=320:-1[cam]; [0:v][cam]overlay=W-w-20:H-h-20" \
|
|
142
|
+
/tmp/combined.jpg -y -loglevel quiet
|
|
143
|
+
echo "Combined: /tmp/combined.jpg"
|
|
144
|
+
open /tmp/combined.jpg
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## After Capturing
|
|
148
|
+
|
|
149
|
+
1. Report the saved path: "Photo saved to `/tmp/photo.jpg`"
|
|
150
|
+
2. Auto-analyze if user asked to "look at" something via camera
|
|
151
|
+
3. To send the image via channel: `sendFile("/tmp/photo.jpg", channel, sessionId)`
|
|
152
|
+
4. To open for review on macOS: `executeCommand("open /tmp/photo.jpg")`
|
|
153
|
+
|
|
154
|
+
## Error Handling
|
|
155
|
+
|
|
156
|
+
| Error | Fix |
|
|
157
|
+
|-------|-----|
|
|
158
|
+
| `imagesnap: command not found` | `brew install imagesnap` |
|
|
159
|
+
| `No cameras found` | Check System Settings → Privacy → Camera — grant permissions |
|
|
160
|
+
| Black/dark photo | Increase warmup: `-w 3` (camera needs time to adjust exposure) |
|
|
161
|
+
| Camera busy | Another app is using the camera; close FaceTime/Zoom/Photo Booth |
|
|
162
|
+
| Permission denied | System Settings → Privacy & Security → Camera → enable Terminal |
|
package/skills/coding.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coding
|
|
3
|
+
description: Use when writing, debugging, or reviewing code
|
|
4
|
+
triggers: code, function, bug, error, refactor, implement, class, module, typescript, javascript, python, api, endpoint, test, debug
|
|
5
|
+
---
|
|
6
|
+
You are an expert programmer. When working on code tasks:
|
|
7
|
+
|
|
8
|
+
1. **Read before writing** — Always read existing files before making changes. Understand the codebase patterns first.
|
|
9
|
+
2. **Edit, don't rewrite** — Use editFile for surgical changes. Never rewrite entire files unless asked.
|
|
10
|
+
3. **Test after changes** — Run tests or verify your changes work after making them.
|
|
11
|
+
4. **Follow existing patterns** — Match the existing code style, naming conventions, and architecture.
|
|
12
|
+
5. **Explain your reasoning** — Briefly explain why you made each change.
|
|
13
|
+
6. **Handle errors** — Add appropriate error handling but don't over-engineer.
|
|
14
|
+
7. **Security first** — Never introduce SQL injection, XSS, command injection, or other vulnerabilities.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: documents
|
|
3
|
+
description: Use when creating documents, reports, or PDFs
|
|
4
|
+
triggers: document, report, pdf, write up, summary, proposal, template, markdown, create doc
|
|
5
|
+
---
|
|
6
|
+
You are an expert technical writer. When creating documents:
|
|
7
|
+
|
|
8
|
+
1. **Structure first** — Start with an outline (headings, sections) before writing content.
|
|
9
|
+
2. **Use headings** — Organize with clear hierarchy: # Title, ## Section, ### Subsection.
|
|
10
|
+
3. **Bullet points** — Use bullets for lists, steps, and multiple items.
|
|
11
|
+
4. **Be concise** — Every sentence should add value. Remove filler words.
|
|
12
|
+
5. **Format for audience** — Technical docs for developers, simple language for non-technical users.
|
|
13
|
+
6. **Save properly** — Use createDocument with the right format (markdown or pdf).
|
package/skills/email.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: email
|
|
3
|
+
description: Use when drafting, sending, or managing emails
|
|
4
|
+
triggers: email, mail, send, draft, reply, compose, message, inbox
|
|
5
|
+
---
|
|
6
|
+
You are an expert email writer. When handling email tasks:
|
|
7
|
+
|
|
8
|
+
1. **Professional tone** — Default to professional but friendly. Match the tone of the conversation.
|
|
9
|
+
2. **Clear subject lines** — Write specific, actionable subject lines.
|
|
10
|
+
3. **Brief and scannable** — Keep emails concise. Use bullet points for multiple items.
|
|
11
|
+
4. **Call to action** — End with a clear next step or ask.
|
|
12
|
+
5. **Proofread** — Check for typos and clarity before sending.
|
|
13
|
+
6. **Confirm before sending** — Always confirm with the user before actually sending an email.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gif-search
|
|
3
|
+
description: Search for GIFs, download and send animated GIFs, create GIFs from images or video clips. Use when the user asks for a GIF, wants to find a funny/reaction GIF, send a GIF, or convert something to a GIF. Searches Giphy/Tenor by default; can create GIFs from local video too.
|
|
4
|
+
triggers: gif, find gif, send gif, reaction gif, funny gif, animated gif, search gif, giphy, tenor, make gif, create gif
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
✅ Find and send reaction GIFs, search Giphy/Tenor by keyword, create GIFs from video clips or image sequences, convert media to GIF
|
|
10
|
+
|
|
11
|
+
❌ Large video files → use video-frames skill instead; static images → just share the image directly
|
|
12
|
+
|
|
13
|
+
## Search Giphy (requires free API key)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
#!/usr/bin/env python3
|
|
17
|
+
"""Search Giphy for GIFs by keyword."""
|
|
18
|
+
import os, json, urllib.request, urllib.parse
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
GIPHY_KEY = os.environ.get("GIPHY_API_KEY", "") # free key at developers.giphy.com
|
|
22
|
+
|
|
23
|
+
def search_gif(query: str, limit: int = 5, rating: str = "g") -> list[dict]:
|
|
24
|
+
"""Search Giphy. Returns list of GIF info dicts."""
|
|
25
|
+
if not GIPHY_KEY:
|
|
26
|
+
# Fall back to Giphy public beta key (rate-limited)
|
|
27
|
+
key = "dc6zaTOxFJmzC"
|
|
28
|
+
else:
|
|
29
|
+
key = GIPHY_KEY
|
|
30
|
+
|
|
31
|
+
params = urllib.parse.urlencode({
|
|
32
|
+
"q": query, "api_key": key,
|
|
33
|
+
"limit": limit, "rating": rating,
|
|
34
|
+
"lang": "en"
|
|
35
|
+
})
|
|
36
|
+
url = f"https://api.giphy.com/v1/gifs/search?{params}"
|
|
37
|
+
data = json.loads(urllib.request.urlopen(url).read())
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
"title": g["title"],
|
|
41
|
+
"url": g["images"]["original"]["url"],
|
|
42
|
+
"mp4": g["images"]["original"]["mp4"],
|
|
43
|
+
"preview": g["images"]["fixed_height_small"]["url"],
|
|
44
|
+
"width": int(g["images"]["original"]["width"]),
|
|
45
|
+
"height": int(g["images"]["original"]["height"]),
|
|
46
|
+
}
|
|
47
|
+
for g in data.get("data", [])
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def download_gif(url: str, output: str = None) -> str:
|
|
51
|
+
"""Download a GIF to a local path."""
|
|
52
|
+
if not output:
|
|
53
|
+
from datetime import datetime
|
|
54
|
+
output = f"/tmp/gif_{datetime.now().strftime('%Y%m%d%H%M%S')}.gif"
|
|
55
|
+
urllib.request.urlretrieve(url, output)
|
|
56
|
+
size = Path(output).stat().st_size
|
|
57
|
+
print(f"✅ GIF downloaded: {output} ({size//1024}KB)")
|
|
58
|
+
return output
|
|
59
|
+
|
|
60
|
+
# Search and get first result
|
|
61
|
+
results = search_gif("excited celebration")
|
|
62
|
+
if results:
|
|
63
|
+
g = results[0]
|
|
64
|
+
print(f"Found: {g['title']}")
|
|
65
|
+
path = download_gif(g["url"])
|
|
66
|
+
# Then: sendFile(path, channel, sessionId) to send via Telegram/Slack/Discord
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Search Tenor (no API key needed for basic use)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
#!/usr/bin/env python3
|
|
73
|
+
import json, urllib.request, urllib.parse
|
|
74
|
+
|
|
75
|
+
def search_tenor(query: str, limit: int = 5) -> list[dict]:
|
|
76
|
+
"""Search Tenor GIFs. Uses public key."""
|
|
77
|
+
params = urllib.parse.urlencode({
|
|
78
|
+
"q": query,
|
|
79
|
+
"key": "LIVDSRZULELA", # Tenor public demo key
|
|
80
|
+
"limit": limit,
|
|
81
|
+
"media_filter": "minimal",
|
|
82
|
+
"contentfilter": "low"
|
|
83
|
+
})
|
|
84
|
+
url = f"https://tenor.googleapis.com/v2/search?{params}"
|
|
85
|
+
data = json.loads(urllib.request.urlopen(url).read())
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
"title": r.get("content_description", ""),
|
|
89
|
+
"url": r["media_formats"]["gif"]["url"],
|
|
90
|
+
"mp4": r["media_formats"].get("mp4", {}).get("url", ""),
|
|
91
|
+
"size": r["media_formats"]["gif"].get("size", 0),
|
|
92
|
+
}
|
|
93
|
+
for r in data.get("results", [])
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
results = search_tenor("happy dance")
|
|
97
|
+
for r in results[:3]:
|
|
98
|
+
print(f" • {r['title'][:60]} ({r['size']//1024}KB)")
|
|
99
|
+
print(f" {r['url']}")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Create GIF from Video Clip
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Requires ffmpeg (brew install ffmpeg)
|
|
106
|
+
VIDEO="/path/to/video.mp4"
|
|
107
|
+
START="00:00:05"
|
|
108
|
+
DURATION=3 # seconds
|
|
109
|
+
OUTPUT="/tmp/clip.gif"
|
|
110
|
+
WIDTH=480
|
|
111
|
+
|
|
112
|
+
ffmpeg -ss "$START" -t "$DURATION" -i "$VIDEO" \
|
|
113
|
+
-vf "fps=10,scale=${WIDTH}:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer" \
|
|
114
|
+
-loop 0 "$OUTPUT" -y -loglevel quiet
|
|
115
|
+
|
|
116
|
+
SIZE=$(du -h "$OUTPUT" | cut -f1)
|
|
117
|
+
echo "✅ GIF: $OUTPUT ($SIZE)"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Create GIF from Image Sequence
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Convert a series of images to GIF
|
|
124
|
+
INPUT_DIR="/tmp/frames"
|
|
125
|
+
OUTPUT="/tmp/animation.gif"
|
|
126
|
+
DELAY=10 # delay between frames (1/100 sec) — 10 = 100ms = 10fps
|
|
127
|
+
|
|
128
|
+
# Using ImageMagick (brew install imagemagick)
|
|
129
|
+
convert -delay $DELAY -loop 0 "${INPUT_DIR}"/*.png "$OUTPUT"
|
|
130
|
+
|
|
131
|
+
# Or with ffmpeg (better quality)
|
|
132
|
+
ffmpeg -framerate 10 -pattern_type glob -i "${INPUT_DIR}/*.png" \
|
|
133
|
+
-vf "scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
|
|
134
|
+
-loop 0 "$OUTPUT" -y -loglevel quiet
|
|
135
|
+
|
|
136
|
+
echo "✅ GIF: $OUTPUT"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Optimize GIF Size
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# GIFs can be huge — optimize before sending
|
|
143
|
+
INPUT="/tmp/original.gif"
|
|
144
|
+
OUTPUT="/tmp/optimized.gif"
|
|
145
|
+
|
|
146
|
+
# Using gifsicle (brew install gifsicle)
|
|
147
|
+
gifsicle --optimize=3 --lossy=80 "$INPUT" -o "$OUTPUT"
|
|
148
|
+
|
|
149
|
+
BEFORE=$(du -h "$INPUT" | cut -f1)
|
|
150
|
+
AFTER=$(du -h "$OUTPUT" | cut -f1)
|
|
151
|
+
echo "Before: $BEFORE → After: $AFTER"
|
|
152
|
+
|
|
153
|
+
# Or reduce size by lowering resolution
|
|
154
|
+
ffmpeg -i "$INPUT" -vf "scale=320:-1:flags=lanczos" "$OUTPUT" -y -loglevel quiet
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Full Workflow: Search → Download → Send
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
#!/usr/bin/env python3
|
|
161
|
+
"""Search for a GIF and prepare it to send via a channel."""
|
|
162
|
+
|
|
163
|
+
def get_gif_for_message(query: str, channel: str = "telegram", session_id: str = "") -> str:
|
|
164
|
+
"""
|
|
165
|
+
Search for a GIF matching the query, download it, return the local path.
|
|
166
|
+
Then caller uses sendFile(path, channel, session_id) to deliver it.
|
|
167
|
+
"""
|
|
168
|
+
# Try Tenor first (no key needed), fall back to Giphy
|
|
169
|
+
results = search_tenor(query, limit=3)
|
|
170
|
+
if not results:
|
|
171
|
+
results = search_gif(query, limit=3)
|
|
172
|
+
|
|
173
|
+
if not results:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
# Pick first result
|
|
177
|
+
gif_url = results[0]["url"]
|
|
178
|
+
path = download_gif(gif_url)
|
|
179
|
+
return path
|
|
180
|
+
|
|
181
|
+
# Example:
|
|
182
|
+
path = get_gif_for_message("thumbs up success")
|
|
183
|
+
if path:
|
|
184
|
+
print(f"Ready to send: {path}")
|
|
185
|
+
# sendFile(path, "telegram", session_id)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Response Format
|
|
189
|
+
|
|
190
|
+
When the user asks to send a GIF:
|
|
191
|
+
1. Search with relevant keywords
|
|
192
|
+
2. Download the best match
|
|
193
|
+
3. Send via `sendFile(path, channel, sessionId)`
|
|
194
|
+
4. Confirm: "Sent 🎉 GIF: [title]"
|
|
195
|
+
|
|
196
|
+
If the channel doesn't support GIFs (plain text email), share the URL instead.
|