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
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
|
+
```
|
package/skills/trello.md
ADDED
|
@@ -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 |
|