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.
Files changed (134) hide show
  1. package/README.md +106 -76
  2. package/SOUL.md +100 -28
  3. package/config/mcp.json +9 -9
  4. package/package.json +15 -8
  5. package/skills/apple-notes.md +0 -52
  6. package/skills/apple-reminders.md +1 -87
  7. package/skills/camsnap.md +20 -144
  8. package/skills/coding.md +7 -7
  9. package/skills/documents.md +6 -6
  10. package/skills/email.md +6 -6
  11. package/skills/gif-search.md +28 -171
  12. package/skills/healthcheck.md +21 -203
  13. package/skills/image-gen.md +24 -123
  14. package/skills/model-usage.md +18 -165
  15. package/skills/obsidian.md +28 -174
  16. package/skills/pdf.md +30 -181
  17. package/skills/research.md +6 -6
  18. package/skills/skill-creator.md +35 -111
  19. package/skills/spotify.md +2 -17
  20. package/skills/summarize.md +36 -193
  21. package/skills/things.md +23 -175
  22. package/skills/tmux.md +1 -91
  23. package/skills/trello.md +32 -157
  24. package/skills/video-frames.md +26 -166
  25. package/skills/weather.md +6 -6
  26. package/src/a2a/A2AClient.js +2 -2
  27. package/src/a2a/A2AServer.js +6 -6
  28. package/src/a2a/AgentCard.js +2 -2
  29. package/src/agents/SubAgentManager.js +61 -19
  30. package/src/agents/Supervisor.js +4 -4
  31. package/src/channels/BaseChannel.js +6 -6
  32. package/src/channels/BlueBubblesChannel.js +112 -0
  33. package/src/channels/DiscordChannel.js +8 -8
  34. package/src/channels/EmailChannel.js +54 -26
  35. package/src/channels/FeishuChannel.js +140 -0
  36. package/src/channels/GoogleChatChannel.js +8 -8
  37. package/src/channels/HttpChannel.js +2 -2
  38. package/src/channels/IRCChannel.js +144 -0
  39. package/src/channels/LineChannel.js +13 -13
  40. package/src/channels/MatrixChannel.js +97 -0
  41. package/src/channels/MattermostChannel.js +119 -0
  42. package/src/channels/NextcloudChannel.js +133 -0
  43. package/src/channels/NostrChannel.js +175 -0
  44. package/src/channels/SignalChannel.js +9 -9
  45. package/src/channels/SlackChannel.js +10 -10
  46. package/src/channels/TeamsChannel.js +10 -10
  47. package/src/channels/TelegramChannel.js +8 -8
  48. package/src/channels/TwitchChannel.js +128 -0
  49. package/src/channels/WhatsAppChannel.js +10 -10
  50. package/src/channels/ZaloChannel.js +119 -0
  51. package/src/channels/iMessageChannel.js +150 -0
  52. package/src/channels/index.js +241 -11
  53. package/src/cli.js +835 -38
  54. package/src/config/agentProfiles.js +19 -19
  55. package/src/config/channels.js +1 -1
  56. package/src/config/default.js +12 -7
  57. package/src/config/models.js +3 -3
  58. package/src/config/permissions.js +2 -2
  59. package/src/core/AgentLoop.js +13 -13
  60. package/src/core/Compaction.js +3 -3
  61. package/src/core/CostTracker.js +2 -2
  62. package/src/core/EventBus.js +15 -15
  63. package/src/core/TaskQueue.js +24 -7
  64. package/src/core/TaskRunner.js +19 -6
  65. package/src/daemon/DaemonManager.js +4 -4
  66. package/src/hooks/HookRunner.js +4 -4
  67. package/src/index.js +6 -2
  68. package/src/mcp/MCPAgentRunner.js +3 -3
  69. package/src/mcp/MCPClient.js +9 -9
  70. package/src/mcp/MCPManager.js +14 -14
  71. package/src/models/ModelRouter.js +2 -2
  72. package/src/safety/AuditLog.js +3 -3
  73. package/src/safety/CircuitBreaker.js +2 -2
  74. package/src/safety/CommandGuard.js +132 -0
  75. package/src/safety/FilesystemGuard.js +23 -3
  76. package/src/safety/GitRollback.js +5 -5
  77. package/src/safety/HumanApproval.js +9 -9
  78. package/src/safety/InputSanitizer.js +81 -8
  79. package/src/safety/PermissionGuard.js +2 -2
  80. package/src/safety/Sandbox.js +1 -1
  81. package/src/safety/SecretScanner.js +90 -28
  82. package/src/safety/SecretVault.js +2 -2
  83. package/src/scheduler/Heartbeat.js +3 -3
  84. package/src/scheduler/Scheduler.js +6 -6
  85. package/src/setup/theme.js +171 -66
  86. package/src/setup/wizard.js +432 -57
  87. package/src/skills/SkillLoader.js +145 -8
  88. package/src/storage/TaskStore.js +39 -15
  89. package/src/systemPrompt.js +45 -43
  90. package/src/tenants/TenantManager.js +79 -22
  91. package/src/tools/ToolRegistry.js +3 -3
  92. package/src/tools/applyPatch.js +2 -2
  93. package/src/tools/browserAutomation.js +4 -4
  94. package/src/tools/calendar.js +155 -0
  95. package/src/tools/clipboard.js +71 -0
  96. package/src/tools/contacts.js +138 -0
  97. package/src/tools/createDocument.js +2 -2
  98. package/src/tools/cronTool.js +14 -14
  99. package/src/tools/database.js +165 -0
  100. package/src/tools/editFile.js +10 -10
  101. package/src/tools/executeCommand.js +11 -3
  102. package/src/tools/generateImage.js +79 -0
  103. package/src/tools/gitTool.js +141 -0
  104. package/src/tools/glob.js +1 -1
  105. package/src/tools/googlePlaces.js +136 -0
  106. package/src/tools/grep.js +2 -2
  107. package/src/tools/iMessageTool.js +86 -0
  108. package/src/tools/imageAnalysis.js +3 -3
  109. package/src/tools/index.js +56 -2
  110. package/src/tools/makeVoiceCall.js +283 -0
  111. package/src/tools/manageAgents.js +2 -2
  112. package/src/tools/manageMCP.js +38 -20
  113. package/src/tools/memory.js +25 -32
  114. package/src/tools/messageChannel.js +1 -1
  115. package/src/tools/notification.js +90 -0
  116. package/src/tools/philipsHue.js +147 -0
  117. package/src/tools/projectTracker.js +8 -8
  118. package/src/tools/readFile.js +1 -1
  119. package/src/tools/readPDF.js +73 -0
  120. package/src/tools/screenCapture.js +6 -6
  121. package/src/tools/searchContent.js +2 -2
  122. package/src/tools/searchFiles.js +1 -1
  123. package/src/tools/sendEmail.js +79 -24
  124. package/src/tools/sendFile.js +4 -4
  125. package/src/tools/sonos.js +137 -0
  126. package/src/tools/sshTool.js +130 -0
  127. package/src/tools/textToSpeech.js +5 -5
  128. package/src/tools/transcribeAudio.js +4 -4
  129. package/src/tools/useMCP.js +4 -4
  130. package/src/tools/webFetch.js +2 -2
  131. package/src/tools/webSearch.js +1 -1
  132. package/src/utils/Embeddings.js +79 -0
  133. package/src/voice/VoiceSessionManager.js +170 -0
  134. package/src/voice/VoiceWebhook.js +188 -0
@@ -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 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.
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
- ## When to Use
8
+ Install: `brew install ffmpeg`
8
9
 
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
10
+ ## Get video info
14
11
 
15
12
  ```bash
16
- # Install ffmpeg (handles everything)
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
- ## Get Video Info / Metadata
16
+ ## Extract frame at timestamp
24
17
 
25
18
  ```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
- "
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 Single Frame at Timestamp
22
+ ## Extract frames at regular intervals
44
23
 
45
24
  ```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
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
- ## 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
29
+ ## Thumbnail (10% into video)
80
30
 
81
31
  ```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:"
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
- ## Create Thumbnail (Best Frame)
37
+ ## Video clip GIF
93
38
 
94
39
  ```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"
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
- ## 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
45
+ ## Extract audio
169
46
 
170
47
  ```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
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
- ## Create Contact Sheet (Grid of Frames)
52
+ ## Analyze video visually
180
53
 
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
- ```
54
+ Extract 6-8 evenly-spaced frames, then analyze each with `imageAnalysis(frame, "What's happening here?")`.
193
55
 
194
- ## Error Handling
56
+ ## Errors
195
57
 
196
58
  | Error | Fix |
197
59
  |-------|-----|
198
60
  | `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 |
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 use official sources for those.
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 best for quick answers
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 parse programmatically
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 sunrise/sunset
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?" Rain Probability
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 {period['chanceofrain']}% rain, {period['tempC']}°C\")
98
+ print(f\" {t[:2]}:00 - {period['chanceofrain']}% rain, {period['tempC']}°C\")
99
99
  "
100
100
  ```
101
101
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A2A Client delegates tasks to external agents via A2A protocol.
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} ${card.skills?.length || 0} skills`
111
+ ` [A2A] Agent: ${card.name} - ${card.skills?.length || 0} skills`
112
112
  );
113
113
 
114
114
  // Send task
@@ -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 receives tasks from other agents via A2A protocol.
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 Receive a task from another agent.
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 treat with caution]\n\n${inputSanitizer.wrapUntrusted(input, "a2a-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 Get task status (requires auth).
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 SSE stream of task progress (requires auth).
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 ${status}`);
315
+ console.log(`[A2A] Server endpoints mounted - ${status}`);
316
316
  }
@@ -1,7 +1,7 @@
1
1
  import { config } from "../config/default.js";
2
2
 
3
3
  /**
4
- * A2A Agent Card serves agent capabilities at /.well-known/agent.json
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 NOT internal tools
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 spawns, tracks, kills, and steers sub-agents.
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 overrides profile
75
- toolOverride = null, // exact tool functions specialist agents only (e.g. MCP)
76
- systemPromptOverride = null, // replace system prompt specialist agents only
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
- console.log(`[SubAgent:${agentId}] Spawning (depth=${depth}, parent=${parentTaskId?.slice(0, 8) ?? "root"}): "${taskDescription.slice(0, 80)}"`);
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 priority: explicit > profile routing > parent > global default ───────
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 exact functions, specialist agents only (e.g. MCP agents)
112
- // 2. allowedTools explicit name list from caller
113
- // 3. profile role preset ("researcher", "coder", etc.) + optional extraTools
114
- // 4. default defaultSubAgentTools (27 tools, excludes blast-radius tools)
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.) bypass all filtering entirely
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 use as-is
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 use sensible default (not all 33 tools)
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 they're always injected dynamically so
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 push here to steer the agent
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
- console.log(`[SubAgent:${agentId}] Completed in ${Date.now() - startedAt}ms`);
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
- console.log(`[SubAgent:${agentId}] ${killed ? "Killed" : "Failed"}: ${error.message}`);
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 with cascade kill to all descendants.
330
- * Aborts mid-API-call via AbortController breaks out immediately.
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) {
@@ -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 monitors all agent activity for safety.
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 AgentLoop will detect this and stop. */
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 warn first, kill at 2x
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 warn first, kill at 1.5x
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 set config.allowlist = [id, id, ...] to restrict who can send tasks.
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 set config.model = "openai:gpt-4.1" to override the default 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 sendReaction(channelMeta, emoji) is a no-op by default.
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 channels that support reactions override this
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 TaskRunner will use the global default.
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 the response was already included
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}