daemora 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -76
- package/SOUL.md +100 -28
- package/config/mcp.json +9 -9
- package/package.json +15 -8
- package/skills/apple-notes.md +0 -52
- package/skills/apple-reminders.md +1 -87
- package/skills/camsnap.md +20 -144
- package/skills/coding.md +7 -7
- package/skills/documents.md +6 -6
- package/skills/email.md +6 -6
- package/skills/gif-search.md +28 -171
- package/skills/healthcheck.md +21 -203
- package/skills/image-gen.md +24 -123
- package/skills/model-usage.md +18 -165
- package/skills/obsidian.md +28 -174
- package/skills/pdf.md +30 -181
- package/skills/research.md +6 -6
- package/skills/skill-creator.md +35 -111
- package/skills/spotify.md +2 -17
- package/skills/summarize.md +36 -193
- package/skills/things.md +23 -175
- package/skills/tmux.md +1 -91
- package/skills/trello.md +32 -157
- package/skills/video-frames.md +26 -166
- package/skills/weather.md +6 -6
- package/src/a2a/A2AClient.js +2 -2
- package/src/a2a/A2AServer.js +6 -6
- package/src/a2a/AgentCard.js +2 -2
- package/src/agents/SubAgentManager.js +61 -19
- package/src/agents/Supervisor.js +4 -4
- package/src/channels/BaseChannel.js +6 -6
- package/src/channels/BlueBubblesChannel.js +112 -0
- package/src/channels/DiscordChannel.js +8 -8
- package/src/channels/EmailChannel.js +54 -26
- package/src/channels/FeishuChannel.js +140 -0
- package/src/channels/GoogleChatChannel.js +8 -8
- package/src/channels/HttpChannel.js +2 -2
- package/src/channels/IRCChannel.js +144 -0
- package/src/channels/LineChannel.js +13 -13
- package/src/channels/MatrixChannel.js +97 -0
- package/src/channels/MattermostChannel.js +119 -0
- package/src/channels/NextcloudChannel.js +133 -0
- package/src/channels/NostrChannel.js +175 -0
- package/src/channels/SignalChannel.js +9 -9
- package/src/channels/SlackChannel.js +10 -10
- package/src/channels/TeamsChannel.js +10 -10
- package/src/channels/TelegramChannel.js +8 -8
- package/src/channels/TwitchChannel.js +128 -0
- package/src/channels/WhatsAppChannel.js +10 -10
- package/src/channels/ZaloChannel.js +119 -0
- package/src/channels/iMessageChannel.js +150 -0
- package/src/channels/index.js +241 -11
- package/src/cli.js +835 -38
- package/src/config/agentProfiles.js +19 -19
- package/src/config/channels.js +1 -1
- package/src/config/default.js +12 -7
- package/src/config/models.js +3 -3
- package/src/config/permissions.js +2 -2
- package/src/core/AgentLoop.js +13 -13
- package/src/core/Compaction.js +3 -3
- package/src/core/CostTracker.js +2 -2
- package/src/core/EventBus.js +15 -15
- package/src/core/TaskQueue.js +24 -7
- package/src/core/TaskRunner.js +19 -6
- package/src/daemon/DaemonManager.js +4 -4
- package/src/hooks/HookRunner.js +4 -4
- package/src/index.js +6 -2
- package/src/mcp/MCPAgentRunner.js +3 -3
- package/src/mcp/MCPClient.js +9 -9
- package/src/mcp/MCPManager.js +14 -14
- package/src/models/ModelRouter.js +2 -2
- package/src/safety/AuditLog.js +3 -3
- package/src/safety/CircuitBreaker.js +2 -2
- package/src/safety/CommandGuard.js +132 -0
- package/src/safety/FilesystemGuard.js +23 -3
- package/src/safety/GitRollback.js +5 -5
- package/src/safety/HumanApproval.js +9 -9
- package/src/safety/InputSanitizer.js +81 -8
- package/src/safety/PermissionGuard.js +2 -2
- package/src/safety/Sandbox.js +1 -1
- package/src/safety/SecretScanner.js +90 -28
- package/src/safety/SecretVault.js +2 -2
- package/src/scheduler/Heartbeat.js +3 -3
- package/src/scheduler/Scheduler.js +6 -6
- package/src/setup/theme.js +171 -66
- package/src/setup/wizard.js +432 -57
- package/src/skills/SkillLoader.js +145 -8
- package/src/storage/TaskStore.js +39 -15
- package/src/systemPrompt.js +45 -43
- package/src/tenants/TenantManager.js +79 -22
- package/src/tools/ToolRegistry.js +3 -3
- package/src/tools/applyPatch.js +2 -2
- package/src/tools/browserAutomation.js +4 -4
- package/src/tools/calendar.js +155 -0
- package/src/tools/clipboard.js +71 -0
- package/src/tools/contacts.js +138 -0
- package/src/tools/createDocument.js +2 -2
- package/src/tools/cronTool.js +14 -14
- package/src/tools/database.js +165 -0
- package/src/tools/editFile.js +10 -10
- package/src/tools/executeCommand.js +11 -3
- package/src/tools/generateImage.js +79 -0
- package/src/tools/gitTool.js +141 -0
- package/src/tools/glob.js +1 -1
- package/src/tools/googlePlaces.js +136 -0
- package/src/tools/grep.js +2 -2
- package/src/tools/iMessageTool.js +86 -0
- package/src/tools/imageAnalysis.js +3 -3
- package/src/tools/index.js +56 -2
- package/src/tools/makeVoiceCall.js +283 -0
- package/src/tools/manageAgents.js +2 -2
- package/src/tools/manageMCP.js +38 -20
- package/src/tools/memory.js +25 -32
- package/src/tools/messageChannel.js +1 -1
- package/src/tools/notification.js +90 -0
- package/src/tools/philipsHue.js +147 -0
- package/src/tools/projectTracker.js +8 -8
- package/src/tools/readFile.js +1 -1
- package/src/tools/readPDF.js +73 -0
- package/src/tools/screenCapture.js +6 -6
- package/src/tools/searchContent.js +2 -2
- package/src/tools/searchFiles.js +1 -1
- package/src/tools/sendEmail.js +79 -24
- package/src/tools/sendFile.js +4 -4
- package/src/tools/sonos.js +137 -0
- package/src/tools/sshTool.js +130 -0
- package/src/tools/textToSpeech.js +5 -5
- package/src/tools/transcribeAudio.js +4 -4
- package/src/tools/useMCP.js +4 -4
- package/src/tools/webFetch.js +2 -2
- package/src/tools/webSearch.js +1 -1
- package/src/utils/Embeddings.js +79 -0
- package/src/voice/VoiceSessionManager.js +170 -0
- package/src/voice/VoiceWebhook.js +188 -0
package/skills/video-frames.md
CHANGED
|
@@ -1,202 +1,62 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: video-frames
|
|
3
|
-
description: Extract frames from video files, create thumbnails, generate GIFs from video clips, analyze video content
|
|
3
|
+
description: Extract frames from video files, create thumbnails, generate GIFs from video clips, and analyze video content visually. Use when asked to extract frames, create thumbnails, screenshot at a timestamp, convert to GIF, or analyze video content.
|
|
4
4
|
triggers: video frames, extract frames, video thumbnail, screenshot from video, video to gif, analyze video, video clip, video timestamp, ffmpeg, frame extraction
|
|
5
|
+
metadata: {"daemora": {"emoji": "🎬", "requires": {"bins": ["ffmpeg"]}, "install": ["brew install ffmpeg"]}}
|
|
5
6
|
---
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Install: `brew install ffmpeg`
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
❌ Full video editing, transcoding for streaming — use dedicated video editors for complex production work
|
|
12
|
-
|
|
13
|
-
## Requirements
|
|
10
|
+
## Get video info
|
|
14
11
|
|
|
15
12
|
```bash
|
|
16
|
-
|
|
17
|
-
brew install ffmpeg
|
|
18
|
-
|
|
19
|
-
# Verify
|
|
20
|
-
ffmpeg -version | head -1
|
|
13
|
+
ffprobe -v quiet -print_format json -show_format -show_streams video.mp4
|
|
21
14
|
```
|
|
22
15
|
|
|
23
|
-
##
|
|
16
|
+
## Extract frame at timestamp
|
|
24
17
|
|
|
25
18
|
```bash
|
|
26
|
-
|
|
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
|
-
"
|
|
19
|
+
ffmpeg -ss 00:01:30 -i video.mp4 -vframes 1 -q:v 2 /tmp/frame.png -y -loglevel quiet
|
|
41
20
|
```
|
|
42
21
|
|
|
43
|
-
## Extract
|
|
22
|
+
## Extract frames at regular intervals
|
|
44
23
|
|
|
45
24
|
```bash
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
25
|
+
ffmpeg -i video.mp4 -vf "fps=1" /tmp/frames/frame_%04d.png -loglevel quiet
|
|
26
|
+
# fps=1 = 1/sec, fps=0.5 = every 2s, fps=2 = 2/sec
|
|
53
27
|
```
|
|
54
28
|
|
|
55
|
-
##
|
|
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
|
|
29
|
+
## Thumbnail (10% into video)
|
|
80
30
|
|
|
81
31
|
```bash
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:"
|
|
32
|
+
DURATION=$(ffprobe -v quiet -show_entries format=duration -of csv=p=0 video.mp4)
|
|
33
|
+
SEEK=$(python3 -c "print(f'{float($DURATION)*0.1:.2f}')")
|
|
34
|
+
ffmpeg -ss $SEEK -i video.mp4 -vframes 1 -vf "scale=1280:-1" -q:v 2 /tmp/thumb.jpg -y -loglevel quiet
|
|
90
35
|
```
|
|
91
36
|
|
|
92
|
-
##
|
|
37
|
+
## Video clip → GIF
|
|
93
38
|
|
|
94
39
|
```bash
|
|
95
|
-
|
|
96
|
-
|
|
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"
|
|
40
|
+
ffmpeg -ss 00:00:10 -t 5 -i video.mp4 \
|
|
41
|
+
-vf "fps=12,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
|
|
42
|
+
-loop 0 /tmp/clip.gif -y -loglevel quiet
|
|
122
43
|
```
|
|
123
44
|
|
|
124
|
-
##
|
|
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
|
|
45
|
+
## Extract audio
|
|
169
46
|
|
|
170
47
|
```bash
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
48
|
+
ffmpeg -i video.mp4 -vn -acodec libmp3lame -q:a 2 /tmp/audio.mp3 -y -loglevel quiet
|
|
49
|
+
# Then use transcribeAudio() to transcribe
|
|
177
50
|
```
|
|
178
51
|
|
|
179
|
-
##
|
|
52
|
+
## Analyze video visually
|
|
180
53
|
|
|
181
|
-
|
|
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
|
-
```
|
|
54
|
+
Extract 6-8 evenly-spaced frames, then analyze each with `imageAnalysis(frame, "What's happening here?")`.
|
|
193
55
|
|
|
194
|
-
##
|
|
56
|
+
## Errors
|
|
195
57
|
|
|
196
58
|
| Error | Fix |
|
|
197
59
|
|-------|-----|
|
|
198
60
|
| `ffmpeg: command not found` | `brew install ffmpeg` |
|
|
199
|
-
|
|
|
200
|
-
|
|
|
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 |
|
|
61
|
+
| Blank/black frames | Skip black intro - use a higher timestamp |
|
|
62
|
+
| GIF too large | Lower fps (8), smaller width, shorter duration |
|
package/skills/weather.md
CHANGED
|
@@ -8,12 +8,12 @@ triggers: weather, temperature, rain, forecast, wind, humidity, hot, cold, snow,
|
|
|
8
8
|
|
|
9
9
|
✅ Current conditions, today's forecast, multi-day forecast, rain prediction, travel weather, "will it rain?", "is it cold in [city]?"
|
|
10
10
|
|
|
11
|
-
❌ Historical weather archives, climate trends, aviation METAR/TAF, severe weather emergency alerts
|
|
11
|
+
❌ Historical weather archives, climate trends, aviation METAR/TAF, severe weather emergency alerts - use official sources for those.
|
|
12
12
|
|
|
13
13
|
## Quick Commands (no API key)
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
# One-line summary
|
|
16
|
+
# One-line summary - best for quick answers
|
|
17
17
|
curl -s "wttr.in/London?format=3"
|
|
18
18
|
# → London: ⛅️ +18°C
|
|
19
19
|
|
|
@@ -29,7 +29,7 @@ curl -s "wttr.in/JFK"
|
|
|
29
29
|
# Coordinates
|
|
30
30
|
curl -s "wttr.in/48.8566,2.3522" # Paris lat/lon
|
|
31
31
|
|
|
32
|
-
# JSON
|
|
32
|
+
# JSON - parse programmatically
|
|
33
33
|
curl -s "wttr.in/London?format=j1" | python3 -c "
|
|
34
34
|
import sys, json
|
|
35
35
|
d = json.load(sys.stdin)
|
|
@@ -81,11 +81,11 @@ Build exactly the output you want:
|
|
|
81
81
|
curl -s "wttr.in/London?format=%l:+%c+%t+(feels+%f)+💨%w+💧%h+☔%p"
|
|
82
82
|
# → London: ⛅ +17°C (feels +14°C) 💨 18km/h W 💧 72% ☔ 0.0mm
|
|
83
83
|
|
|
84
|
-
# Travel check
|
|
84
|
+
# Travel check - sunrise/sunset
|
|
85
85
|
curl -s "wttr.in/Dubai?format=%l:+%c+%t+🌅%S+🌇%s"
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
## "Will it rain?"
|
|
88
|
+
## "Will it rain?" - Rain Probability
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
# Parse hourly rain chance from JSON
|
|
@@ -95,7 +95,7 @@ d = json.load(sys.stdin)
|
|
|
95
95
|
print('Rain chance by period today:')
|
|
96
96
|
for period in d['weather'][0]['hourly']:
|
|
97
97
|
t = period['time'].zfill(4)
|
|
98
|
-
print(f\" {t[:2]}:00
|
|
98
|
+
print(f\" {t[:2]}:00 - {period['chanceofrain']}% rain, {period['tempC']}°C\")
|
|
99
99
|
"
|
|
100
100
|
```
|
|
101
101
|
|
package/src/a2a/A2AClient.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A2A Client
|
|
2
|
+
* A2A Client - delegates tasks to external agents via A2A protocol.
|
|
3
3
|
*
|
|
4
4
|
* Flow:
|
|
5
5
|
* 1. Discover agent: fetch /.well-known/agent.json
|
|
@@ -108,7 +108,7 @@ export async function delegateToAgent(agentUrl, taskInput) {
|
|
|
108
108
|
// Discover agent capabilities
|
|
109
109
|
const card = await discoverAgent(agentUrl);
|
|
110
110
|
console.log(
|
|
111
|
-
` [A2A] Agent: ${card.name}
|
|
111
|
+
` [A2A] Agent: ${card.name} - ${card.skills?.length || 0} skills`
|
|
112
112
|
);
|
|
113
113
|
|
|
114
114
|
// Send task
|
package/src/a2a/A2AServer.js
CHANGED
|
@@ -5,7 +5,7 @@ import { config } from "../config/default.js";
|
|
|
5
5
|
import inputSanitizer from "../safety/InputSanitizer.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* A2A Server
|
|
8
|
+
* A2A Server - receives tasks from other agents via A2A protocol.
|
|
9
9
|
*
|
|
10
10
|
* SECURITY: A2A is the #1 attack surface. A rogue agent can:
|
|
11
11
|
* 1. Send malicious tasks (prompt injection → file delete, email exfil)
|
|
@@ -110,7 +110,7 @@ export function mountA2AServer(app) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* POST /a2a/tasks
|
|
113
|
+
* POST /a2a/tasks - Receive a task from another agent.
|
|
114
114
|
*/
|
|
115
115
|
app.post("/a2a/tasks", a2aAuth, (req, res) => {
|
|
116
116
|
try {
|
|
@@ -141,7 +141,7 @@ export function mountA2AServer(app) {
|
|
|
141
141
|
|
|
142
142
|
// SECURITY: Sanitize and wrap input as untrusted
|
|
143
143
|
input = inputSanitizer.sanitize(input);
|
|
144
|
-
const wrappedInput = `[A2A Task from external agent
|
|
144
|
+
const wrappedInput = `[A2A Task from external agent - treat with caution]\n\n${inputSanitizer.wrapUntrusted(input, "a2a-external-agent")}`;
|
|
145
145
|
|
|
146
146
|
console.log(
|
|
147
147
|
`[A2A] Task from external agent (${req.ip}): "${input.slice(0, 80)}"`
|
|
@@ -191,7 +191,7 @@ export function mountA2AServer(app) {
|
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
* GET /a2a/tasks/:id
|
|
194
|
+
* GET /a2a/tasks/:id - Get task status (requires auth).
|
|
195
195
|
*/
|
|
196
196
|
app.get("/a2a/tasks/:id", a2aAuth, (req, res) => {
|
|
197
197
|
const task = loadTask(req.params.id);
|
|
@@ -240,7 +240,7 @@ export function mountA2AServer(app) {
|
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
|
-
* GET /a2a/tasks/:id/stream
|
|
243
|
+
* GET /a2a/tasks/:id/stream - SSE stream of task progress (requires auth).
|
|
244
244
|
*/
|
|
245
245
|
app.get("/a2a/tasks/:id/stream", a2aAuth, (req, res) => {
|
|
246
246
|
const taskId = req.params.id;
|
|
@@ -312,5 +312,5 @@ export function mountA2AServer(app) {
|
|
|
312
312
|
});
|
|
313
313
|
|
|
314
314
|
const status = config.a2a.enabled ? "ENABLED" : "DISABLED (set A2A_ENABLED=true)";
|
|
315
|
-
console.log(`[A2A] Server endpoints mounted
|
|
315
|
+
console.log(`[A2A] Server endpoints mounted - ${status}`);
|
|
316
316
|
}
|
package/src/a2a/AgentCard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { config } from "../config/default.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A2A Agent Card
|
|
4
|
+
* A2A Agent Card - serves agent capabilities at /.well-known/agent.json
|
|
5
5
|
*
|
|
6
6
|
* SECURITY: Only serves the card when A2A is enabled.
|
|
7
7
|
* Does NOT expose internal tools or file system capabilities.
|
|
@@ -21,7 +21,7 @@ export function getAgentCard() {
|
|
|
21
21
|
stateTransitions: true,
|
|
22
22
|
},
|
|
23
23
|
|
|
24
|
-
// Only expose safe, high-level skill categories
|
|
24
|
+
// Only expose safe, high-level skill categories - NOT internal tools
|
|
25
25
|
skills: [
|
|
26
26
|
{
|
|
27
27
|
id: "research",
|
|
@@ -8,7 +8,7 @@ import tenantContext from "../tenants/TenantContext.js";
|
|
|
8
8
|
import { resolveModelForProfile } from "../models/ModelRouter.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Sub-Agent Manager
|
|
11
|
+
* Sub-Agent Manager - spawns, tracks, kills, and steers sub-agents.
|
|
12
12
|
*
|
|
13
13
|
* Each sub-agent entry stores:
|
|
14
14
|
* - taskDescription, startedAt, parentTaskId
|
|
@@ -30,6 +30,25 @@ const MAX_CONCURRENT_SUB_AGENTS = 7;
|
|
|
30
30
|
/** Map<agentId, { taskDescription, startedAt, parentTaskId, abortController, steerQueue }> */
|
|
31
31
|
const activeSubAgents = new Map();
|
|
32
32
|
|
|
33
|
+
// ── Demo-friendly colored logging ─────────────────────────────────────────────
|
|
34
|
+
const C = {
|
|
35
|
+
cyan: "\x1b[36m",
|
|
36
|
+
green: "\x1b[32m",
|
|
37
|
+
yellow: "\x1b[33m",
|
|
38
|
+
red: "\x1b[31m",
|
|
39
|
+
magenta: "\x1b[35m",
|
|
40
|
+
bold: "\x1b[1m",
|
|
41
|
+
dim: "\x1b[2m",
|
|
42
|
+
reset: "\x1b[0m",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function _agentLog(color, icon, agentId, depth, message) {
|
|
46
|
+
const indent = " ".repeat(depth);
|
|
47
|
+
const tag = `${C.dim}[${agentId}]${C.reset}`;
|
|
48
|
+
const active = activeSubAgents.size > 0 ? `${C.dim} (${activeSubAgents.size} active)${C.reset}` : "";
|
|
49
|
+
console.log(`${indent}${color}${icon} ${tag} ${message}${C.reset}${active}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
// ── Kill propagation: when Supervisor kills a parent, kill all its children ──
|
|
34
53
|
eventBus.on("supervisor:kill", ({ taskId }) => {
|
|
35
54
|
for (const [agentId, info] of activeSubAgents.entries()) {
|
|
@@ -71,9 +90,9 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
71
90
|
model = null,
|
|
72
91
|
profile = null, // role preset: researcher | coder | writer | analyst
|
|
73
92
|
extraTools = null, // additional tools on top of profile or default
|
|
74
|
-
tools: allowedTools = null, // explicit list
|
|
75
|
-
toolOverride = null, // exact tool functions
|
|
76
|
-
systemPromptOverride = null, // replace system prompt
|
|
93
|
+
tools: allowedTools = null, // explicit list - overrides profile
|
|
94
|
+
toolOverride = null, // exact tool functions - specialist agents only (e.g. MCP)
|
|
95
|
+
systemPromptOverride = null, // replace system prompt - specialist agents only
|
|
77
96
|
maxCost = 0.10,
|
|
78
97
|
timeout = 120_000,
|
|
79
98
|
depth = 0,
|
|
@@ -95,9 +114,12 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
95
114
|
const agentId = uuidv4().slice(0, 8);
|
|
96
115
|
const taskId = `subagent-${agentId}`;
|
|
97
116
|
|
|
98
|
-
|
|
117
|
+
const profileLabel = profile ? ` [${profile}]` : "";
|
|
118
|
+
const modelLabel = resolvedModel ? ` ${C.dim}(${resolvedModel})${C.reset}` : "";
|
|
119
|
+
_agentLog(C.cyan + C.bold, "🤖 SPAWN", agentId, depth,
|
|
120
|
+
`${C.cyan}${C.bold}${profileLabel}${C.reset}${modelLabel} "${taskDescription.slice(0, 80)}${taskDescription.length > 80 ? "…" : ""}"`);
|
|
99
121
|
|
|
100
|
-
// ── Model resolution
|
|
122
|
+
// ── Model resolution - priority: explicit > profile routing > parent > global default ───────
|
|
101
123
|
const store = tenantContext.getStore();
|
|
102
124
|
const resolvedModel = model
|
|
103
125
|
|| resolveModelForProfile(profile, store?.resolvedConfig || {}, null)
|
|
@@ -108,19 +130,19 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
108
130
|
|
|
109
131
|
// ── Tool set ──────────────────────────────────────────────────────────────
|
|
110
132
|
// Resolution order (highest priority first):
|
|
111
|
-
// 1. toolOverride
|
|
112
|
-
// 2. allowedTools
|
|
113
|
-
// 3. profile
|
|
114
|
-
// 4. default
|
|
133
|
+
// 1. toolOverride - exact functions, specialist agents only (e.g. MCP agents)
|
|
134
|
+
// 2. allowedTools - explicit name list from caller
|
|
135
|
+
// 3. profile - role preset ("researcher", "coder", etc.) + optional extraTools
|
|
136
|
+
// 4. default - defaultSubAgentTools (27 tools, excludes blast-radius tools)
|
|
115
137
|
let agentTools;
|
|
116
138
|
if (toolOverride) {
|
|
117
|
-
// Specialist agents (MCP, etc.)
|
|
139
|
+
// Specialist agents (MCP, etc.) - bypass all filtering entirely
|
|
118
140
|
agentTools = { ...toolOverride };
|
|
119
141
|
} else {
|
|
120
142
|
let toolNames;
|
|
121
143
|
|
|
122
144
|
if (allowedTools) {
|
|
123
|
-
// Caller provided explicit list
|
|
145
|
+
// Caller provided explicit list - use as-is
|
|
124
146
|
toolNames = [...allowedTools];
|
|
125
147
|
} else if (profile) {
|
|
126
148
|
// Named role preset
|
|
@@ -132,7 +154,7 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
132
154
|
toolNames = [...preset];
|
|
133
155
|
}
|
|
134
156
|
} else {
|
|
135
|
-
// No profile specified
|
|
157
|
+
// No profile specified - use sensible default (not all 33 tools)
|
|
136
158
|
toolNames = [...defaultSubAgentTools];
|
|
137
159
|
}
|
|
138
160
|
|
|
@@ -149,7 +171,7 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
// Inject depth-aware spawnAgent and parallelAgents at next depth level.
|
|
152
|
-
// These are NOT in any profile
|
|
174
|
+
// These are NOT in any profile - they're always injected dynamically so
|
|
153
175
|
// depth propagation is always correct regardless of profile used.
|
|
154
176
|
if (depth + 1 < maxDepth) {
|
|
155
177
|
agentTools.spawnAgent = (desc, opts) => {
|
|
@@ -189,7 +211,7 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
189
211
|
|
|
190
212
|
// ── Coordination primitives ───────────────────────────────────────────────
|
|
191
213
|
const abortController = new AbortController();
|
|
192
|
-
const steerQueue = []; // Shared mutable array
|
|
214
|
+
const steerQueue = []; // Shared mutable array - push here to steer the agent
|
|
193
215
|
|
|
194
216
|
activeSubAgents.set(agentId, {
|
|
195
217
|
taskDescription,
|
|
@@ -245,13 +267,17 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
245
267
|
),
|
|
246
268
|
]);
|
|
247
269
|
|
|
248
|
-
|
|
270
|
+
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
271
|
+
const costStr = result.cost ? ` $${result.cost.toFixed(4)}` : "";
|
|
272
|
+
_agentLog(C.green + C.bold, "✅ DONE ", agentId, depth,
|
|
273
|
+
`${C.green}${C.bold}completed in ${elapsed}s${costStr}${C.reset}`);
|
|
249
274
|
eventBus.emitEvent("agent:finished", { agentId, taskId, parentTaskId, cost: result.cost });
|
|
250
275
|
return result.text;
|
|
251
276
|
|
|
252
277
|
} catch (error) {
|
|
253
278
|
const killed = abortController.signal.aborted;
|
|
254
|
-
|
|
279
|
+
_agentLog(killed ? C.yellow : C.red, killed ? "⛔ KILL " : "❌ FAIL ", agentId, depth,
|
|
280
|
+
`${error.message}`);
|
|
255
281
|
eventBus.emitEvent("agent:finished", { agentId, taskId, parentTaskId, error: error.message, killed });
|
|
256
282
|
return killed
|
|
257
283
|
? `Sub-agent was stopped by the supervisor.`
|
|
@@ -278,6 +304,14 @@ export async function spawnSubAgent(taskDescription, options = {}) {
|
|
|
278
304
|
export async function spawnParallelAgents(tasks, sharedOptions = {}) {
|
|
279
305
|
const { sharedContext = null, parentTaskId = null, approvalMode = "auto", channelMeta = null } = sharedOptions;
|
|
280
306
|
|
|
307
|
+
const parallelStart = Date.now();
|
|
308
|
+
console.log(`\n${C.magenta}${C.bold}🚀 PARALLEL - launching ${tasks.length} agents simultaneously${C.reset}`);
|
|
309
|
+
tasks.forEach((t, i) => {
|
|
310
|
+
const profile = t.options?.profile ? ` [${t.options.profile}]` : "";
|
|
311
|
+
console.log(`${C.magenta} ${i + 1}/${tasks.length}${profile} - "${(t.description || "").slice(0, 70)}${(t.description || "").length > 70 ? "…" : ""}"${C.reset}`);
|
|
312
|
+
});
|
|
313
|
+
console.log();
|
|
314
|
+
|
|
281
315
|
const results = await Promise.allSettled(
|
|
282
316
|
tasks.map((t) => {
|
|
283
317
|
const opts = t.options || {};
|
|
@@ -298,6 +332,14 @@ export async function spawnParallelAgents(tasks, sharedOptions = {}) {
|
|
|
298
332
|
})
|
|
299
333
|
);
|
|
300
334
|
|
|
335
|
+
const elapsed = ((Date.now() - parallelStart) / 1000).toFixed(1);
|
|
336
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
337
|
+
const failed = results.length - succeeded;
|
|
338
|
+
const summary = failed === 0
|
|
339
|
+
? `${C.green}${C.bold}all ${succeeded} completed${C.reset}`
|
|
340
|
+
: `${C.green}${succeeded} ok${C.reset} / ${C.red}${failed} failed${C.reset}`;
|
|
341
|
+
console.log(`\n${C.magenta}${C.bold}🏁 PARALLEL DONE - ${summary}${C.magenta}${C.bold} in ${elapsed}s total${C.reset}\n`);
|
|
342
|
+
|
|
301
343
|
return results.map((r, i) => ({
|
|
302
344
|
task: tasks[i].description.slice(0, 80),
|
|
303
345
|
status: r.status,
|
|
@@ -326,8 +368,8 @@ export function listActiveAgents() {
|
|
|
326
368
|
}
|
|
327
369
|
|
|
328
370
|
/**
|
|
329
|
-
* Hard-kill a sub-agent by agent ID
|
|
330
|
-
* Aborts mid-API-call via AbortController
|
|
371
|
+
* Hard-kill a sub-agent by agent ID - with cascade kill to all descendants.
|
|
372
|
+
* Aborts mid-API-call via AbortController - breaks out immediately.
|
|
331
373
|
* Recursively kills all child and grandchild agents before killing the target.
|
|
332
374
|
*/
|
|
333
375
|
export function killAgent(agentId) {
|
package/src/agents/Supervisor.js
CHANGED
|
@@ -2,7 +2,7 @@ import eventBus from "../core/EventBus.js";
|
|
|
2
2
|
import { config } from "../config/default.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Supervisor Agent
|
|
5
|
+
* Supervisor Agent - monitors all agent activity for safety.
|
|
6
6
|
*
|
|
7
7
|
* Listens to EventBus events and detects:
|
|
8
8
|
* - Infinite loops (same tool called too many times)
|
|
@@ -28,7 +28,7 @@ class Supervisor {
|
|
|
28
28
|
return taskId ? this.killedTasks.has(taskId) : false;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** Kill a task
|
|
31
|
+
/** Kill a task - AgentLoop will detect this and stop. */
|
|
32
32
|
killTask(taskId, reason) {
|
|
33
33
|
if (!taskId || this.killedTasks.has(taskId)) return;
|
|
34
34
|
this.killedTasks.add(taskId);
|
|
@@ -89,14 +89,14 @@ class Supervisor {
|
|
|
89
89
|
const recentTimestamps = timestamps.filter((t) => t > oneMinuteAgo);
|
|
90
90
|
this.toolCallTimestamps.set(taskId, recentTimestamps);
|
|
91
91
|
|
|
92
|
-
// Check: too many calls per minute
|
|
92
|
+
// Check: too many calls per minute - warn first, kill at 2x
|
|
93
93
|
if (recentTimestamps.length > this.maxToolCallsPerMinute * 2) {
|
|
94
94
|
this.killTask(taskId, `Runaway agent: ${recentTimestamps.length} tool calls in last minute (hard limit: ${this.maxToolCallsPerMinute * 2})`);
|
|
95
95
|
} else if (recentTimestamps.length > this.maxToolCallsPerMinute) {
|
|
96
96
|
this.warn(taskId, `Rate limit: ${recentTimestamps.length} tool calls in last minute (max: ${this.maxToolCallsPerMinute})`);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// Check: too many total calls
|
|
99
|
+
// Check: too many total calls - warn first, kill at 1.5x
|
|
100
100
|
if (count > Math.floor(this.maxToolCallsPerTask * 1.5)) {
|
|
101
101
|
this.killTask(taskId, `Runaway agent: ${count} total tool calls (hard limit: ${Math.floor(this.maxToolCallsPerTask * 1.5)})`);
|
|
102
102
|
} else if (count > this.maxToolCallsPerTask) {
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* 3. Routes the agent's reply back to the originating platform
|
|
9
9
|
*
|
|
10
10
|
* Built-in capabilities (all channels get these for free):
|
|
11
|
-
* - Allowlist gating
|
|
11
|
+
* - Allowlist gating - set config.allowlist = [id, id, ...] to restrict who can send tasks.
|
|
12
12
|
* Empty / omitted = open to all (backward compatible).
|
|
13
|
-
* - Per-channel model
|
|
13
|
+
* - Per-channel model - set config.model = "openai:gpt-4.1" to override the default model
|
|
14
14
|
* for all tasks coming from this channel.
|
|
15
|
-
* - Status reactions
|
|
15
|
+
* - Status reactions - sendReaction(channelMeta, emoji) is a no-op by default.
|
|
16
16
|
* Channels that support native reactions override this.
|
|
17
17
|
*/
|
|
18
18
|
export class BaseChannel {
|
|
@@ -54,7 +54,7 @@ export class BaseChannel {
|
|
|
54
54
|
* @param {string} emoji - Emoji to react with (e.g. "✅", "❌", "⏳")
|
|
55
55
|
*/
|
|
56
56
|
async sendReaction(channelMeta, emoji) {
|
|
57
|
-
// Default no-op
|
|
57
|
+
// Default no-op - channels that support reactions override this
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -74,7 +74,7 @@ export class BaseChannel {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Get the model override for this channel (if configured).
|
|
77
|
-
* Returns null if no override
|
|
77
|
+
* Returns null if no override - TaskRunner will use the global default.
|
|
78
78
|
* @returns {string|null}
|
|
79
79
|
*/
|
|
80
80
|
getModel() {
|
|
@@ -93,7 +93,7 @@ export class BaseChannel {
|
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Returns true if this task was silently absorbed into a concurrent agent session.
|
|
96
|
-
* When true, the channel should NOT send a reply
|
|
96
|
+
* When true, the channel should NOT send a reply - the response was already included
|
|
97
97
|
* in the original task's reply (like Claude Code's follow-up injection behaviour).
|
|
98
98
|
* @param {object} completedTask
|
|
99
99
|
* @returns {boolean}
|