daemora 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
@@ -0,0 +1,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 |
@@ -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).
@@ -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.