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
package/skills/tmux.md ADDED
@@ -0,0 +1,204 @@
1
+ ---
2
+ name: tmux
3
+ description: Create, manage, and control tmux sessions, windows, and panes. Send commands to running sessions, monitor output, split panes, run background processes in named sessions, and supervise long-running tasks. Use when the user asks to manage terminal sessions, run something in the background in tmux, monitor a running process, or control tmux.
4
+ triggers: tmux, terminal session, background session, tmux window, tmux pane, split terminal, persistent session, attach session, detach session, monitor process
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ ✅ Running long background tasks in named sessions, monitoring running processes, splitting terminal workspace, sending input to interactive CLIs, managing multiple parallel jobs
10
+
11
+ ❌ Simple one-off background commands → use `executeCommand` with `background:true` directly
12
+
13
+ ## Check tmux is Available
14
+
15
+ ```bash
16
+ which tmux && tmux -V || echo "tmux not found — install with: brew install tmux"
17
+ ```
18
+
19
+ ## Session Management
20
+
21
+ ```bash
22
+ # List all sessions
23
+ tmux ls
24
+ # → main: 3 windows (created Mon Mar 3 09:00:00 2026) [220x50]
25
+
26
+ # Create a new named session (detached, runs in background)
27
+ tmux new-session -d -s mywork
28
+
29
+ # Create session and run a command immediately
30
+ tmux new-session -d -s server -x 220 -y 50 \; send-keys "npm run dev" Enter
31
+
32
+ # Attach to a session (interactive)
33
+ tmux attach -t mywork
34
+
35
+ # Kill a session
36
+ tmux kill-session -t mywork
37
+
38
+ # Kill all sessions
39
+ tmux kill-server
40
+ ```
41
+
42
+ ## Run Commands in a Session
43
+
44
+ ```bash
45
+ # Send a command to an existing session
46
+ tmux send-keys -t mywork "ls -la" Enter
47
+
48
+ # Send to a specific window
49
+ tmux send-keys -t mywork:0 "npm test" Enter
50
+
51
+ # Send to a specific pane (window:pane)
52
+ tmux send-keys -t mywork:0.1 "python3 script.py" Enter
53
+
54
+ # Send without pressing Enter (useful for pre-filling input)
55
+ tmux send-keys -t mywork "git commit -m '"
56
+ ```
57
+
58
+ ## Read Output from a Session
59
+
60
+ ```bash
61
+ # Capture what's currently visible in a pane
62
+ tmux capture-pane -t mywork -p
63
+
64
+ # Capture last 200 lines (including scrollback)
65
+ tmux capture-pane -t mywork -p -S -200
66
+
67
+ # Save pane content to file
68
+ tmux capture-pane -t mywork -p -S -500 > /tmp/session_output.txt
69
+
70
+ # Check if a process is still running in a pane
71
+ tmux capture-pane -t mywork -p | tail -5
72
+ ```
73
+
74
+ ## Python Helpers
75
+
76
+ ```python
77
+ #!/usr/bin/env python3
78
+ import subprocess, time
79
+
80
+ def tmux_run(session: str, command: str, wait_seconds: float = 0.5) -> str:
81
+ """Send a command to a tmux session and capture output."""
82
+ subprocess.run(["tmux", "send-keys", "-t", session, command, "Enter"])
83
+ time.sleep(wait_seconds)
84
+ result = subprocess.run(
85
+ ["tmux", "capture-pane", "-t", session, "-p", "-S", "-100"],
86
+ capture_output=True, text=True
87
+ )
88
+ return result.stdout.strip()
89
+
90
+ def tmux_session_exists(session: str) -> bool:
91
+ result = subprocess.run(["tmux", "has-session", "-t", session], capture_output=True)
92
+ return result.returncode == 0
93
+
94
+ def tmux_get_output(session: str, lines: int = 50) -> str:
95
+ result = subprocess.run(
96
+ ["tmux", "capture-pane", "-t", session, "-p", "-S", f"-{lines}"],
97
+ capture_output=True, text=True
98
+ )
99
+ return result.stdout.strip()
100
+
101
+ def tmux_ensure_session(session: str) -> None:
102
+ """Create session if it doesn't exist."""
103
+ if not tmux_session_exists(session):
104
+ subprocess.run(["tmux", "new-session", "-d", "-s", session, "-x", "220", "-y", "50"])
105
+ print(f"Created tmux session: {session}")
106
+ else:
107
+ print(f"Using existing session: {session}")
108
+
109
+ # Example: run a dev server in tmux and monitor it
110
+ tmux_ensure_session("devserver")
111
+ output = tmux_run("devserver", "npm run dev", wait_seconds=2.0)
112
+ print(f"Server output:\n{output}")
113
+
114
+ # Later: check if server is still running
115
+ current = tmux_get_output("devserver", lines=10)
116
+ print(f"Current output:\n{current}")
117
+ ```
118
+
119
+ ## Split Panes (Parallel Work)
120
+
121
+ ```bash
122
+ # Split current window vertically (left | right)
123
+ tmux split-window -h -t mywork
124
+
125
+ # Split horizontally (top / bottom)
126
+ tmux split-window -v -t mywork
127
+
128
+ # Run different commands in each pane
129
+ tmux send-keys -t mywork:0.0 "npm run dev" Enter # left pane: dev server
130
+ tmux send-keys -t mywork:0.1 "npm run test -- --watch" Enter # right pane: test watcher
131
+
132
+ # Create a full monitoring layout
133
+ tmux new-session -d -s monitor -x 220 -y 50
134
+ tmux send-keys -t monitor "htop" Enter
135
+ tmux split-window -h -t monitor
136
+ tmux send-keys -t monitor:0.1 "tail -f /var/log/system.log" Enter
137
+ tmux split-window -v -t monitor:0.1
138
+ tmux send-keys -t monitor:0.2 "watch -n 2 df -h" Enter
139
+ ```
140
+
141
+ ## Monitor a Long-Running Task
142
+
143
+ ```python
144
+ #!/usr/bin/env python3
145
+ """Start a task in tmux and poll until it's done."""
146
+ import subprocess, time, re
147
+
148
+ def run_and_monitor(session: str, command: str, done_pattern: str, timeout: int = 300):
149
+ """
150
+ Run command in tmux and wait until output matches done_pattern.
151
+ Returns final output.
152
+ """
153
+ # Ensure session exists
154
+ subprocess.run(["tmux", "new-session", "-d", "-s", session, "-x", "220", "-y", "50"],
155
+ capture_output=True) # ignore error if exists
156
+
157
+ subprocess.run(["tmux", "send-keys", "-t", session, command, "Enter"])
158
+ print(f"Started: {command}")
159
+
160
+ start = time.time()
161
+ while time.time() - start < timeout:
162
+ time.sleep(3)
163
+ result = subprocess.run(
164
+ ["tmux", "capture-pane", "-t", session, "-p", "-S", "-50"],
165
+ capture_output=True, text=True
166
+ )
167
+ output = result.stdout
168
+ if re.search(done_pattern, output, re.IGNORECASE):
169
+ print(f"✅ Done (matched: {done_pattern})")
170
+ return output
171
+
172
+ print(f"⚠️ Timed out after {timeout}s")
173
+ return subprocess.run(["tmux", "capture-pane", "-t", session, "-p", "-S", "-50"],
174
+ capture_output=True, text=True).stdout
175
+
176
+ # Example
177
+ output = run_and_monitor(
178
+ session="build",
179
+ command="npm run build",
180
+ done_pattern=r"build complete|error|failed",
181
+ timeout=120
182
+ )
183
+ print(output[-500:]) # show last 500 chars
184
+ ```
185
+
186
+ ## Useful Shortcuts Reference
187
+
188
+ ```
189
+ tmux new -s NAME New session named NAME
190
+ tmux ls List sessions
191
+ tmux a -t NAME Attach to session
192
+ tmux kill-session -t N Kill session
193
+
194
+ Inside tmux (prefix is Ctrl+b by default):
195
+ d Detach from session
196
+ c New window
197
+ n/p Next/previous window
198
+ % Split vertically
199
+ " Split horizontally
200
+ arrow Move between panes
201
+ z Zoom current pane (toggle)
202
+ q Show pane numbers
203
+ [ Scroll mode (q to exit)
204
+ ```
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: trello
3
+ description: Manage Trello boards, lists, and cards using the Trello REST API. Use when the user asks to create a Trello card, move a card, list boards, add a checklist, archive a card, or automate Trello workflows. Requires TRELLO_API_KEY and TRELLO_TOKEN environment variables.
4
+ triggers: trello, kanban, trello card, trello board, create card, move card, trello list, trello checklist, trello archive, trello label
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ ✅ Create/update/move/archive cards, list boards and cards, add checklists, labels, due dates, comments, attach files, automate Trello workflows
10
+
11
+ ❌ Real-time push notifications (use Trello webhooks + a server endpoint) — use polling for monitoring instead
12
+
13
+ ## Setup
14
+
15
+ ```bash
16
+ # Get your API key: https://trello.com/app-key
17
+ # Generate token from that page
18
+
19
+ export TRELLO_API_KEY="your_api_key"
20
+ export TRELLO_TOKEN="your_token"
21
+
22
+ # Quick test
23
+ curl -s "https://api.trello.com/1/members/me/boards?key=${TRELLO_API_KEY}&token=${TRELLO_TOKEN}" \
24
+ | python3 -c "import sys,json; [print(b['name'], '→', b['id']) for b in json.load(sys.stdin)]"
25
+ ```
26
+
27
+ ## Python Helper (use for all Trello calls)
28
+
29
+ ```python
30
+ #!/usr/bin/env python3
31
+ import os, json, urllib.request, urllib.parse
32
+
33
+ API_KEY = os.environ["TRELLO_API_KEY"]
34
+ TOKEN = os.environ["TRELLO_TOKEN"]
35
+ BASE = "https://api.trello.com/1"
36
+
37
+ def trello(method: str, path: str, **kwargs) -> dict | list:
38
+ """Make a Trello API call. kwargs become query params for GET, form data for POST/PUT."""
39
+ params = {"key": API_KEY, "token": TOKEN, **kwargs}
40
+ url = f"{BASE}{path}"
41
+ if method == "GET":
42
+ url += "?" + urllib.parse.urlencode(params)
43
+ req = urllib.request.Request(url)
44
+ else:
45
+ data = urllib.parse.urlencode(params).encode()
46
+ req = urllib.request.Request(url, data=data, method=method)
47
+ with urllib.request.urlopen(req) as resp:
48
+ return json.loads(resp.read())
49
+ ```
50
+
51
+ ## List Boards
52
+
53
+ ```python
54
+ boards = trello("GET", "/members/me/boards", fields="name,id,url,closed")
55
+ open_boards = [b for b in boards if not b.get("closed")]
56
+ for b in open_boards:
57
+ print(f"📋 {b['name']} (id: {b['id']})")
58
+ ```
59
+
60
+ ## Get Lists on a Board
61
+
62
+ ```python
63
+ board_id = "BOARD_ID_HERE"
64
+ lists = trello("GET", f"/boards/{board_id}/lists", filter="open")
65
+ for lst in lists:
66
+ print(f" 📁 {lst['name']} (id: {lst['id']})")
67
+ ```
68
+
69
+ ## Get Cards on a Board / List
70
+
71
+ ```python
72
+ # All cards on a board
73
+ board_id = "BOARD_ID_HERE"
74
+ cards = trello("GET", f"/boards/{board_id}/cards", fields="name,idList,due,desc,labels")
75
+ for card in cards:
76
+ print(f" 🃏 {card['name']} (due: {card.get('due', 'none')})")
77
+
78
+ # Cards in a specific list
79
+ list_id = "LIST_ID_HERE"
80
+ cards = trello("GET", f"/lists/{list_id}/cards", fields="name,due,desc")
81
+ ```
82
+
83
+ ## Create a Card
84
+
85
+ ```python
86
+ new_card = trello("POST", "/cards",
87
+ idList="LIST_ID_HERE",
88
+ name="Task: Fix login bug",
89
+ desc="## Description\nThe login form fails when email contains special characters.\n\n## Steps\n1. Go to /login\n2. Enter: user+test@example.com\n3. See error",
90
+ due="2026-03-15T09:00:00.000Z", # ISO 8601
91
+ pos="top" # or "bottom" or a position number
92
+ )
93
+ print(f"✅ Card created: {new_card['name']} → {new_card['url']}")
94
+ ```
95
+
96
+ ## Move Card to Another List
97
+
98
+ ```python
99
+ card_id = "CARD_ID_HERE"
100
+ target_list_id = "TARGET_LIST_ID"
101
+ trello("PUT", f"/cards/{card_id}", idList=target_list_id)
102
+ print("✅ Card moved")
103
+ ```
104
+
105
+ ## Add Checklist to Card
106
+
107
+ ```python
108
+ card_id = "CARD_ID_HERE"
109
+
110
+ # Create checklist
111
+ checklist = trello("POST", "/checklists", idCard=card_id, name="Acceptance Criteria")
112
+
113
+ # Add items
114
+ for item in ["Tests pass", "Code reviewed", "Deployed to staging", "QA sign-off"]:
115
+ trello("POST", f"/checklists/{checklist['id']}/checkItems", name=item)
116
+
117
+ print(f"✅ Checklist created with {4} items")
118
+ ```
119
+
120
+ ## Add Comment to Card
121
+
122
+ ```python
123
+ card_id = "CARD_ID_HERE"
124
+ trello("POST", f"/cards/{card_id}/actions/comments",
125
+ text="Investigated this issue — root cause is in auth.js:142. PR coming shortly.")
126
+ ```
127
+
128
+ ## Add Label to Card
129
+
130
+ ```python
131
+ # First get or create a label
132
+ board_id = "BOARD_ID_HERE"
133
+ labels = trello("GET", f"/boards/{board_id}/labels")
134
+ bug_label = next((l for l in labels if l.get("name") == "Bug"), None)
135
+
136
+ if bug_label:
137
+ card_id = "CARD_ID_HERE"
138
+ trello("POST", f"/cards/{card_id}/idLabels", value=bug_label["id"])
139
+ ```
140
+
141
+ ## Archive (Close) a Card
142
+
143
+ ```python
144
+ card_id = "CARD_ID_HERE"
145
+ trello("PUT", f"/cards/{card_id}", closed="true")
146
+ print("✅ Card archived")
147
+ ```
148
+
149
+ ## Full Workflow: Create Card from Task Description
150
+
151
+ ```python
152
+ def add_task_to_trello(title: str, description: str, board_name: str = None, list_name: str = "To Do"):
153
+ """Find the right board/list and create a card."""
154
+ boards = trello("GET", "/members/me/boards", fields="name,id")
155
+ board = next((b for b in boards if board_name and board_name.lower() in b["name"].lower()), boards[0])
156
+
157
+ lists = trello("GET", f"/boards/{board['id']}/lists", filter="open")
158
+ target_list = next((l for l in lists if list_name.lower() in l["name"].lower()), lists[0])
159
+
160
+ card = trello("POST", "/cards",
161
+ idList=target_list["id"],
162
+ name=title,
163
+ desc=description,
164
+ pos="top"
165
+ )
166
+ return card["url"]
167
+
168
+ url = add_task_to_trello(
169
+ "Research competitor pricing",
170
+ "## Goal\nAnalyze top 5 competitor pricing pages\n\n## Deliverable\nComparison spreadsheet",
171
+ board_name="Marketing"
172
+ )
173
+ print(f"Card created: {url}")
174
+ ```
175
+
176
+ ## Error Handling
177
+
178
+ | Error | Fix |
179
+ |-------|-----|
180
+ | 401 Unauthorized | Check `TRELLO_API_KEY` and `TRELLO_TOKEN` are set and valid |
181
+ | 404 Not Found | Board/list/card ID is wrong; list boards/lists first to get correct IDs |
182
+ | 429 Rate Limited | Trello allows ~300 req/10s; add `time.sleep(0.1)` between bulk calls |
183
+ | Card won't move | Verify target list is on same board as card |
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: video-frames
3
+ description: Extract frames from video files, create thumbnails, generate GIFs from video clips, analyze video content frame-by-frame, and process video files. Use when the user asks to extract frames from a video, create a thumbnail, get a screenshot from a video at a specific timestamp, convert a clip to GIF, or analyze video content visually.
4
+ triggers: video frames, extract frames, video thumbnail, screenshot from video, video to gif, analyze video, video clip, video timestamp, ffmpeg, frame extraction
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ ✅ Extract specific frames at timestamps, create video thumbnails, batch extract frames, convert clips to GIF, analyze video content visually, get video metadata
10
+
11
+ ❌ Full video editing, transcoding for streaming — use dedicated video editors for complex production work
12
+
13
+ ## Requirements
14
+
15
+ ```bash
16
+ # Install ffmpeg (handles everything)
17
+ brew install ffmpeg
18
+
19
+ # Verify
20
+ ffmpeg -version | head -1
21
+ ```
22
+
23
+ ## Get Video Info / Metadata
24
+
25
+ ```bash
26
+ VIDEO="/path/to/video.mp4"
27
+
28
+ # Full metadata
29
+ ffprobe -v quiet -print_format json -show_format -show_streams "$VIDEO" | python3 -c "
30
+ import sys, json
31
+ d = json.load(sys.stdin)
32
+ fmt = d['format']
33
+ vid = next(s for s in d['streams'] if s['codec_type'] == 'video')
34
+ print(f\"File: {fmt['filename'].split('/')[-1]}\")
35
+ print(f\"Duration: {float(fmt['duration']):.1f}s ({float(fmt['duration'])/60:.1f} min)\")
36
+ print(f\"Size: {int(fmt['size'])/1024/1024:.1f} MB\")
37
+ print(f\"Resolution: {vid['width']}x{vid['height']}\")
38
+ print(f\"FPS: {eval(vid['r_frame_rate']):.2f}\")
39
+ print(f\"Codec: {vid['codec_name']}\")
40
+ "
41
+ ```
42
+
43
+ ## Extract Single Frame at Timestamp
44
+
45
+ ```bash
46
+ VIDEO="/path/to/video.mp4"
47
+ TIMESTAMP="00:01:30" # HH:MM:SS or seconds like "90"
48
+ OUTPUT="/tmp/frame.png"
49
+
50
+ ffmpeg -ss "$TIMESTAMP" -i "$VIDEO" -vframes 1 -q:v 2 "$OUTPUT" -y -loglevel quiet
51
+ echo "Frame saved: $OUTPUT"
52
+ open "$OUTPUT" # preview on macOS
53
+ ```
54
+
55
+ ## Extract Multiple Frames at Specific Times
56
+
57
+ ```python
58
+ #!/usr/bin/env python3
59
+ import subprocess
60
+ from pathlib import Path
61
+
62
+ video = "/path/to/video.mp4"
63
+ timestamps = ["00:00:10", "00:00:30", "00:01:00", "00:02:00"]
64
+ out_dir = Path("/tmp/video-frames")
65
+ out_dir.mkdir(exist_ok=True)
66
+
67
+ for i, ts in enumerate(timestamps):
68
+ out = out_dir / f"frame_{i+1:02d}_{ts.replace(':','')}.png"
69
+ subprocess.run([
70
+ "ffmpeg", "-ss", ts, "-i", video,
71
+ "-vframes", "1", "-q:v", "2",
72
+ str(out), "-y", "-loglevel", "quiet"
73
+ ])
74
+ print(f"✅ {ts} → {out}")
75
+
76
+ print(f"\nAll frames in: {out_dir}")
77
+ ```
78
+
79
+ ## Extract Frames at Regular Intervals
80
+
81
+ ```bash
82
+ VIDEO="/path/to/video.mp4"
83
+ OUT_DIR="/tmp/frames"
84
+ FPS=1 # 1 frame per second (use 0.5 for every 2 seconds, 2 for 2 per second)
85
+
86
+ mkdir -p "$OUT_DIR"
87
+ ffmpeg -i "$VIDEO" -vf "fps=${FPS}" "$OUT_DIR/frame_%04d.png" -loglevel quiet
88
+ echo "Frames extracted to: $OUT_DIR"
89
+ ls "$OUT_DIR" | wc -l | xargs echo "Total frames:"
90
+ ```
91
+
92
+ ## Create Thumbnail (Best Frame)
93
+
94
+ ```bash
95
+ VIDEO="/path/to/video.mp4"
96
+ OUTPUT="/tmp/thumbnail.jpg"
97
+
98
+ # Take frame at 10% into the video (usually better than frame 1)
99
+ DURATION=$(ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$VIDEO")
100
+ SEEK=$(python3 -c "print(f'{float('$DURATION') * 0.1:.2f}')")
101
+
102
+ ffmpeg -ss "$SEEK" -i "$VIDEO" -vframes 1 -vf "scale=1280:-1" -q:v 2 "$OUTPUT" -y -loglevel quiet
103
+ echo "Thumbnail: $OUTPUT"
104
+ ```
105
+
106
+ ## Convert Video Clip to GIF
107
+
108
+ ```bash
109
+ VIDEO="/path/to/video.mp4"
110
+ START="00:00:10" # start time
111
+ DURATION=5 # seconds
112
+ OUTPUT="/tmp/clip.gif"
113
+ WIDTH=480 # gif width (height auto-scaled)
114
+
115
+ ffmpeg -ss "$START" -t "$DURATION" -i "$VIDEO" \
116
+ -vf "fps=12,scale=${WIDTH}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
117
+ -loop 0 "$OUTPUT" -y -loglevel quiet
118
+
119
+ SIZE=$(du -h "$OUTPUT" | cut -f1)
120
+ echo "GIF created: $OUTPUT ($SIZE)"
121
+ open "$OUTPUT"
122
+ ```
123
+
124
+ ## Analyze Video Content Visually
125
+
126
+ ```python
127
+ #!/usr/bin/env python3
128
+ """Extract key frames and analyze with vision."""
129
+ import subprocess
130
+ from pathlib import Path
131
+
132
+ def analyze_video(video_path: str, sample_count: int = 8) -> list[str]:
133
+ """
134
+ Extract evenly-spaced frames from video and return list of frame paths.
135
+ Use with imageAnalysis() tool to describe video content.
136
+ """
137
+ out_dir = Path("/tmp/video-analysis")
138
+ out_dir.mkdir(exist_ok=True)
139
+
140
+ # Get duration
141
+ result = subprocess.run(
142
+ ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
143
+ "-of", "csv=p=0", video_path],
144
+ capture_output=True, text=True
145
+ )
146
+ duration = float(result.stdout.strip())
147
+
148
+ # Extract evenly-spaced frames
149
+ frames = []
150
+ for i in range(sample_count):
151
+ timestamp = duration * (i + 1) / (sample_count + 1)
152
+ out = out_dir / f"frame_{i+1:02d}.jpg"
153
+ subprocess.run([
154
+ "ffmpeg", "-ss", str(timestamp), "-i", video_path,
155
+ "-vframes", "1", "-q:v", "3", str(out), "-y", "-loglevel", "quiet"
156
+ ])
157
+ if out.exists():
158
+ frames.append(str(out))
159
+
160
+ return frames
161
+
162
+ # After getting frames, analyze each with imageAnalysis tool:
163
+ frames = analyze_video("/path/to/video.mp4", sample_count=6)
164
+ print(f"Extracted {len(frames)} frames for analysis")
165
+ # Then: for frame in frames: imageAnalysis(frame, "Describe what's happening in this scene")
166
+ ```
167
+
168
+ ## Extract Audio from Video
169
+
170
+ ```bash
171
+ VIDEO="/path/to/video.mp4"
172
+ OUTPUT="/tmp/audio.mp3"
173
+
174
+ ffmpeg -i "$VIDEO" -vn -acodec libmp3lame -q:a 2 "$OUTPUT" -y -loglevel quiet
175
+ echo "Audio extracted: $OUTPUT"
176
+ # Then use transcribeAudio() to transcribe it
177
+ ```
178
+
179
+ ## Create Contact Sheet (Grid of Frames)
180
+
181
+ ```bash
182
+ VIDEO="/path/to/video.mp4"
183
+ OUTPUT="/tmp/contact_sheet.jpg"
184
+
185
+ # Extract 16 evenly-spaced frames and tile into a 4x4 grid
186
+ DURATION=$(ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$VIDEO")
187
+ ffmpeg -i "$VIDEO" \
188
+ -vf "fps=16/${DURATION},scale=320:-1,tile=4x4" \
189
+ -frames:v 1 "$OUTPUT" -y -loglevel quiet
190
+ echo "Contact sheet: $OUTPUT"
191
+ open "$OUTPUT"
192
+ ```
193
+
194
+ ## Error Handling
195
+
196
+ | Error | Fix |
197
+ |-------|-----|
198
+ | `ffmpeg: command not found` | `brew install ffmpeg` |
199
+ | `Invalid data found` | Video file may be corrupted; try `ffprobe -v error "$VIDEO"` to check |
200
+ | Blank/black frames | Video has black intro — use higher timestamp (e.g., 10% in) |
201
+ | GIF too large | Lower fps (use 8 instead of 15), smaller width, shorter duration |
202
+ | `No such file` | Check the video path is absolute and the file exists |