daemora 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
@@ -0,0 +1,369 @@
1
+ import { runAgentLoop } from "../core/AgentLoop.js";
2
+ import { buildSystemPrompt } from "../systemPrompt.js";
3
+ import { toolFunctions } from "../tools/index.js";
4
+ import { agentProfiles, defaultSubAgentTools } from "../config/agentProfiles.js";
5
+ import eventBus from "../core/EventBus.js";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import tenantContext from "../tenants/TenantContext.js";
8
+ import { resolveModelForProfile } from "../models/ModelRouter.js";
9
+
10
+ /**
11
+ * Sub-Agent Manager — spawns, tracks, kills, and steers sub-agents.
12
+ *
13
+ * Each sub-agent entry stores:
14
+ * - taskDescription, startedAt, parentTaskId
15
+ * - abortController → hard-kills the agent (aborts mid-API-call too)
16
+ * - steerQueue → shared array; push here to inject a steering message
17
+ * into the running agent's next loop iteration
18
+ *
19
+ * Kill propagation:
20
+ * When the Supervisor kills a parent task, it emits "supervisor:kill".
21
+ * We listen to that event and abort all child agents of the killed task.
22
+ *
23
+ * Context sharing:
24
+ * Pass parentContext (string) to give the sub-agent summary info from
25
+ * the parent before it starts.
26
+ */
27
+
28
+ const MAX_CONCURRENT_SUB_AGENTS = 7;
29
+
30
+ /** Map<agentId, { taskDescription, startedAt, parentTaskId, abortController, steerQueue }> */
31
+ const activeSubAgents = new Map();
32
+
33
+ // ── Kill propagation: when Supervisor kills a parent, kill all its children ──
34
+ eventBus.on("supervisor:kill", ({ taskId }) => {
35
+ for (const [agentId, info] of activeSubAgents.entries()) {
36
+ const isChild = info.parentTaskId === taskId;
37
+ const isSelf = `subagent-${agentId}` === taskId;
38
+ if (isChild || isSelf) {
39
+ console.log(`[SubAgentManager] Killing sub-agent ${agentId} (parent ${taskId} killed)`);
40
+ info.abortController.abort();
41
+ activeSubAgents.delete(agentId);
42
+ eventBus.emitEvent("agent:killed", { agentId, reason: `parent task killed (${taskId})` });
43
+ }
44
+ }
45
+ });
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Spawn a sub-agent to handle a specific task.
51
+ *
52
+ * @param {string} taskDescription What the sub-agent should do
53
+ * @param {object} options
54
+ * @param {string} [options.model] Model override
55
+ * @param {string} [options.profile] Role preset: "researcher"|"coder"|"writer"|"analyst"
56
+ * @param {string[]} [options.extraTools] Additional tools on top of profile or default
57
+ * @param {string[]} [options.tools] Explicit tool list (overrides profile)
58
+ * @param {object} [options.toolOverride] Exact tool functions (specialist agents, bypasses all)
59
+ * @param {object} [options.systemPromptOverride] Replace system prompt entirely (specialist agents)
60
+ * @param {number} [options.maxCost] Cost budget
61
+ * @param {number} [options.timeout] Timeout in ms
62
+ * @param {number} [options.depth] Recursion depth (managed internally)
63
+ * @param {string} [options.parentTaskId] Parent task ID for kill propagation
64
+ * @param {string} [options.parentContext] Summary/context from parent agent
65
+ * @param {string} [options.approvalMode] Inherited approval mode
66
+ * @param {object} [options.channelMeta] Inherited channel meta for approvals
67
+ * @returns {Promise<string>} Sub-agent's final response
68
+ */
69
+ export async function spawnSubAgent(taskDescription, options = {}) {
70
+ const {
71
+ model = null,
72
+ profile = null, // role preset: researcher | coder | writer | analyst
73
+ 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
77
+ maxCost = 0.10,
78
+ timeout = 120_000,
79
+ depth = 0,
80
+ parentTaskId = null,
81
+ parentContext = null,
82
+ approvalMode = "auto",
83
+ channelMeta = null,
84
+ } = options;
85
+
86
+ const maxDepth = 3;
87
+ if (depth >= maxDepth) {
88
+ return `Cannot spawn sub-agent: maximum depth (${maxDepth}) reached. Complete this task directly.`;
89
+ }
90
+
91
+ if (activeSubAgents.size >= MAX_CONCURRENT_SUB_AGENTS) {
92
+ return `Cannot spawn sub-agent: maximum concurrent agents (${MAX_CONCURRENT_SUB_AGENTS}) reached. Wait for others to finish.`;
93
+ }
94
+
95
+ const agentId = uuidv4().slice(0, 8);
96
+ const taskId = `subagent-${agentId}`;
97
+
98
+ console.log(`[SubAgent:${agentId}] Spawning (depth=${depth}, parent=${parentTaskId?.slice(0, 8) ?? "root"}): "${taskDescription.slice(0, 80)}"`);
99
+
100
+ // ── Model resolution — priority: explicit > profile routing > parent > global default ───────
101
+ const store = tenantContext.getStore();
102
+ const resolvedModel = model
103
+ || resolveModelForProfile(profile, store?.resolvedConfig || {}, null)
104
+ || store?.resolvedModel
105
+ || config.defaultModel;
106
+
107
+ const apiKeys = store?.apiKeys || {};
108
+
109
+ // ── Tool set ──────────────────────────────────────────────────────────────
110
+ // 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)
115
+ let agentTools;
116
+ if (toolOverride) {
117
+ // Specialist agents (MCP, etc.) — bypass all filtering entirely
118
+ agentTools = { ...toolOverride };
119
+ } else {
120
+ let toolNames;
121
+
122
+ if (allowedTools) {
123
+ // Caller provided explicit list — use as-is
124
+ toolNames = [...allowedTools];
125
+ } else if (profile) {
126
+ // Named role preset
127
+ const preset = agentProfiles[profile];
128
+ if (!preset) {
129
+ console.warn(`[SubAgent:${agentId}] Unknown profile "${profile}", using default`);
130
+ toolNames = [...defaultSubAgentTools];
131
+ } else {
132
+ toolNames = [...preset];
133
+ }
134
+ } else {
135
+ // No profile specified — use sensible default (not all 33 tools)
136
+ toolNames = [...defaultSubAgentTools];
137
+ }
138
+
139
+ // Apply extraTools on top of whatever was resolved above
140
+ if (extraTools) {
141
+ for (const t of extraTools) {
142
+ if (!toolNames.includes(t)) toolNames.push(t);
143
+ }
144
+ }
145
+
146
+ agentTools = {};
147
+ for (const name of toolNames) {
148
+ if (toolFunctions[name]) agentTools[name] = toolFunctions[name];
149
+ }
150
+
151
+ // Inject depth-aware spawnAgent and parallelAgents at next depth level.
152
+ // These are NOT in any profile — they're always injected dynamically so
153
+ // depth propagation is always correct regardless of profile used.
154
+ if (depth + 1 < maxDepth) {
155
+ agentTools.spawnAgent = (desc, opts) => {
156
+ const parsedOpts = typeof opts === "string" ? JSON.parse(opts) : (opts || {});
157
+ return spawnSubAgent(desc, {
158
+ ...parsedOpts,
159
+ depth: depth + 1,
160
+ parentTaskId: taskId,
161
+ channelMeta,
162
+ approvalMode,
163
+ model: parsedOpts.model || resolvedModel, // inherit parent model if not explicitly set
164
+ });
165
+ };
166
+
167
+ // Wrap parallelAgents to correctly propagate depth.
168
+ // Without this, parallel agents spawned by sub-agents would get depth=0
169
+ // (the default), allowing infinite nesting and defeating the depth limit.
170
+ agentTools.parallelAgents = (tasksJson, sharedOptionsJson) => {
171
+ const tasks = typeof tasksJson === "string" ? JSON.parse(tasksJson) : (tasksJson || []);
172
+ const sharedOpts = typeof sharedOptionsJson === "string"
173
+ ? JSON.parse(sharedOptionsJson)
174
+ : (sharedOptionsJson || {});
175
+ const tasksWithDepth = tasks.map((t) => ({
176
+ ...t,
177
+ options: {
178
+ ...(t.options || {}),
179
+ depth: depth + 1,
180
+ parentTaskId: t.options?.parentTaskId || taskId,
181
+ channelMeta: t.options?.channelMeta || channelMeta,
182
+ approvalMode: t.options?.approvalMode || approvalMode,
183
+ },
184
+ }));
185
+ return spawnParallelAgents(tasksWithDepth, sharedOpts);
186
+ };
187
+ }
188
+ }
189
+
190
+ // ── Coordination primitives ───────────────────────────────────────────────
191
+ const abortController = new AbortController();
192
+ const steerQueue = []; // Shared mutable array — push here to steer the agent
193
+
194
+ activeSubAgents.set(agentId, {
195
+ taskDescription,
196
+ startedAt: Date.now(),
197
+ parentTaskId,
198
+ abortController,
199
+ steerQueue,
200
+ });
201
+
202
+ eventBus.emitEvent("agent:spawned", {
203
+ agentId,
204
+ taskId,
205
+ parentTaskId,
206
+ depth,
207
+ taskDescription: taskDescription.slice(0, 100),
208
+ });
209
+
210
+ // ── Build initial messages (optionally include parent context) ────────────
211
+ const initialMessages = [];
212
+
213
+ if (parentContext) {
214
+ // Give the sub-agent a quick summary of what the parent knows
215
+ initialMessages.push({
216
+ role: "user",
217
+ content: `[Context from parent agent]:\n${parentContext}\n\n[Your task]:\n${taskDescription}`,
218
+ });
219
+ } else {
220
+ initialMessages.push({ role: "user", content: taskDescription });
221
+ }
222
+
223
+ // ── Run with timeout and abort signal ─────────────────────────────────────
224
+ const startedAt = activeSubAgents.get(agentId).startedAt;
225
+
226
+ try {
227
+ const result = await Promise.race([
228
+ runAgentLoop({
229
+ messages: initialMessages,
230
+ systemPrompt: systemPromptOverride || await buildSystemPrompt(taskDescription),
231
+ tools: agentTools,
232
+ modelId: resolvedModel,
233
+ taskId,
234
+ approvalMode,
235
+ channelMeta,
236
+ signal: abortController.signal, // hard kill support
237
+ steerQueue, // steering support
238
+ apiKeys, // per-tenant API key overlay
239
+ }),
240
+ new Promise((_, reject) =>
241
+ setTimeout(() => {
242
+ abortController.abort();
243
+ reject(new Error(`Sub-agent timed out after ${timeout / 1000}s`));
244
+ }, timeout)
245
+ ),
246
+ ]);
247
+
248
+ console.log(`[SubAgent:${agentId}] Completed in ${Date.now() - startedAt}ms`);
249
+ eventBus.emitEvent("agent:finished", { agentId, taskId, parentTaskId, cost: result.cost });
250
+ return result.text;
251
+
252
+ } catch (error) {
253
+ const killed = abortController.signal.aborted;
254
+ console.log(`[SubAgent:${agentId}] ${killed ? "Killed" : "Failed"}: ${error.message}`);
255
+ eventBus.emitEvent("agent:finished", { agentId, taskId, parentTaskId, error: error.message, killed });
256
+ return killed
257
+ ? `Sub-agent was stopped by the supervisor.`
258
+ : `Sub-agent error: ${error.message}`;
259
+ } finally {
260
+ activeSubAgents.delete(agentId);
261
+ }
262
+ }
263
+
264
+ // ─────────────────────────────────────────────────────────────────────────────
265
+
266
+ /**
267
+ * Spawn multiple sub-agents in parallel and collect results.
268
+ *
269
+ * @param {Array<{description, options}>} tasks
270
+ * @param {object} sharedOptions
271
+ * @param {string} [sharedOptions.sharedContext] Spec/contract passed to ALL agents as parentContext.
272
+ * Use this to share HTML structure with CSS/JS agents,
273
+ * API schema with frontend/backend agents, etc.
274
+ * @param {string} [sharedOptions.parentTaskId] For kill propagation
275
+ * @param {string} [sharedOptions.approvalMode]
276
+ * @param {object} [sharedOptions.channelMeta]
277
+ */
278
+ export async function spawnParallelAgents(tasks, sharedOptions = {}) {
279
+ const { sharedContext = null, parentTaskId = null, approvalMode = "auto", channelMeta = null } = sharedOptions;
280
+
281
+ const results = await Promise.allSettled(
282
+ tasks.map((t) => {
283
+ const opts = t.options || {};
284
+ // Merge sharedContext: if task already has parentContext, prepend the shared spec
285
+ const mergedContext = sharedContext
286
+ ? (opts.parentContext
287
+ ? `[Shared spec for all agents]:\n${sharedContext}\n\n[Additional context]:\n${opts.parentContext}`
288
+ : sharedContext)
289
+ : opts.parentContext || null;
290
+
291
+ return spawnSubAgent(t.description, {
292
+ ...opts,
293
+ parentContext: mergedContext,
294
+ parentTaskId: opts.parentTaskId || parentTaskId,
295
+ approvalMode: opts.approvalMode || approvalMode,
296
+ channelMeta: opts.channelMeta || channelMeta,
297
+ });
298
+ })
299
+ );
300
+
301
+ return results.map((r, i) => ({
302
+ task: tasks[i].description.slice(0, 80),
303
+ status: r.status,
304
+ result: r.status === "fulfilled" ? r.value : (r.reason?.message || "Failed"),
305
+ }));
306
+ }
307
+
308
+ // ─────────────────────────────────────────────────────────────────────────────
309
+ // Management API
310
+ // ─────────────────────────────────────────────────────────────────────────────
311
+
312
+ export function getActiveSubAgentCount() {
313
+ return activeSubAgents.size;
314
+ }
315
+
316
+ export function listActiveAgents() {
317
+ return [...activeSubAgents.entries()].map(([id, info]) => ({
318
+ id,
319
+ taskId: `subagent-${id}`,
320
+ parentTaskId: info.parentTaskId,
321
+ task: info.taskDescription.slice(0, 120),
322
+ startedAt: new Date(info.startedAt).toISOString(),
323
+ elapsedMs: Date.now() - info.startedAt,
324
+ steerable: true,
325
+ }));
326
+ }
327
+
328
+ /**
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.
331
+ * Recursively kills all child and grandchild agents before killing the target.
332
+ */
333
+ export function killAgent(agentId) {
334
+ const agent = activeSubAgents.get(agentId);
335
+ if (!agent) {
336
+ return `No active agent found: ${agentId}. Use manageAgents("list") to see active agents.`;
337
+ }
338
+
339
+ // Cascade: kill all direct children first (recursive, so grandchildren are handled too)
340
+ const taskId = `subagent-${agentId}`;
341
+ const childIds = [...activeSubAgents.entries()]
342
+ .filter(([, info]) => info.parentTaskId === taskId)
343
+ .map(([id]) => id);
344
+
345
+ for (const childId of childIds) {
346
+ killAgent(childId); // recursive cascade
347
+ }
348
+
349
+ // Now kill this agent
350
+ agent.abortController.abort();
351
+ activeSubAgents.delete(agentId);
352
+ eventBus.emitEvent("agent:killed", { agentId, reason: "manual kill (cascade)" });
353
+ console.log(`[SubAgentManager] Hard-killed agent ${agentId}${childIds.length ? ` + ${childIds.length} child(ren)` : ""}`);
354
+ return `Agent ${agentId} killed${childIds.length ? ` (cascade: ${childIds.length} child agent(s) also stopped)` : ""}.`;
355
+ }
356
+
357
+ /**
358
+ * Inject a steering instruction into a running sub-agent.
359
+ * Picked up at the next loop iteration in AgentLoop (drains steerQueue).
360
+ */
361
+ export function steerAgent(agentId, message) {
362
+ const agent = activeSubAgents.get(agentId);
363
+ if (!agent) {
364
+ return `No active agent found: ${agentId}.`;
365
+ }
366
+ agent.steerQueue.push(message);
367
+ console.log(`[SubAgentManager] Steered agent ${agentId}: "${message.slice(0, 60)}"`);
368
+ return `Steering message sent to agent ${agentId}. It will be injected on the next loop iteration.`;
369
+ }
@@ -0,0 +1,192 @@
1
+ import eventBus from "../core/EventBus.js";
2
+ import { config } from "../config/default.js";
3
+
4
+ /**
5
+ * Supervisor Agent — monitors all agent activity for safety.
6
+ *
7
+ * Listens to EventBus events and detects:
8
+ * - Infinite loops (same tool called too many times)
9
+ * - Cost overruns (task exceeding budget)
10
+ * - Dangerous patterns (blocked commands, secret exposure)
11
+ * - Runaway agents (too many tool calls per minute)
12
+ *
13
+ * Actions: log warning, pause agent, kill agent, alert user.
14
+ */
15
+ class Supervisor {
16
+ constructor() {
17
+ this.toolCallCounts = new Map(); // taskId → count
18
+ this.toolCallTimestamps = new Map(); // taskId → [timestamps]
19
+ this.warnings = [];
20
+ this.killedTasks = new Set(); // taskIds that have been killed
21
+ this.maxToolCallsPerMinute = 30;
22
+ this.maxToolCallsPerTask = 100;
23
+ this.running = false;
24
+ }
25
+
26
+ /** Check if a task has been killed by the supervisor. Called by AgentLoop each iteration. */
27
+ isKilled(taskId) {
28
+ return taskId ? this.killedTasks.has(taskId) : false;
29
+ }
30
+
31
+ /** Kill a task — AgentLoop will detect this and stop. */
32
+ killTask(taskId, reason) {
33
+ if (!taskId || this.killedTasks.has(taskId)) return;
34
+ this.killedTasks.add(taskId);
35
+ console.log(`[Supervisor] KILLING task ${taskId?.slice(0, 8)}: ${reason}`);
36
+ eventBus.emitEvent("supervisor:kill", { taskId, reason });
37
+ eventBus.emitEvent("audit:event", { event: "supervisor_kill", taskId, reason });
38
+ }
39
+
40
+ /** Remove task from killed set after it ends (cleanup). */
41
+ cleanupKill(taskId) {
42
+ this.killedTasks.delete(taskId);
43
+ }
44
+
45
+ /**
46
+ * Start monitoring.
47
+ */
48
+ start() {
49
+ if (this.running) return;
50
+ this.running = true;
51
+
52
+ // Monitor tool calls
53
+ eventBus.on("tool:before", (data) => this.onToolBefore(data));
54
+ eventBus.on("tool:after", (data) => this.onToolAfter(data));
55
+ eventBus.on("model:called", (data) => this.onModelCalled(data));
56
+ eventBus.on("agent:spawned", (data) => this.onAgentSpawned(data));
57
+
58
+ console.log(`[Supervisor] Started monitoring`);
59
+ }
60
+
61
+ /**
62
+ * Stop monitoring.
63
+ */
64
+ stop() {
65
+ this.running = false;
66
+ eventBus.removeAllListeners("tool:before");
67
+ eventBus.removeAllListeners("tool:after");
68
+ eventBus.removeAllListeners("model:called");
69
+ eventBus.removeAllListeners("agent:spawned");
70
+ console.log(`[Supervisor] Stopped`);
71
+ }
72
+
73
+ /**
74
+ * Check before a tool is executed.
75
+ */
76
+ onToolBefore(data) {
77
+ const { taskId, tool_name, params } = data;
78
+
79
+ // Track call count
80
+ const count = (this.toolCallCounts.get(taskId) || 0) + 1;
81
+ this.toolCallCounts.set(taskId, count);
82
+
83
+ // Track call rate
84
+ const now = Date.now();
85
+ const timestamps = this.toolCallTimestamps.get(taskId) || [];
86
+ timestamps.push(now);
87
+ // Keep only last minute
88
+ const oneMinuteAgo = now - 60000;
89
+ const recentTimestamps = timestamps.filter((t) => t > oneMinuteAgo);
90
+ this.toolCallTimestamps.set(taskId, recentTimestamps);
91
+
92
+ // Check: too many calls per minute — warn first, kill at 2x
93
+ if (recentTimestamps.length > this.maxToolCallsPerMinute * 2) {
94
+ this.killTask(taskId, `Runaway agent: ${recentTimestamps.length} tool calls in last minute (hard limit: ${this.maxToolCallsPerMinute * 2})`);
95
+ } else if (recentTimestamps.length > this.maxToolCallsPerMinute) {
96
+ this.warn(taskId, `Rate limit: ${recentTimestamps.length} tool calls in last minute (max: ${this.maxToolCallsPerMinute})`);
97
+ }
98
+
99
+ // Check: too many total calls — warn first, kill at 1.5x
100
+ if (count > Math.floor(this.maxToolCallsPerTask * 1.5)) {
101
+ this.killTask(taskId, `Runaway agent: ${count} total tool calls (hard limit: ${Math.floor(this.maxToolCallsPerTask * 1.5)})`);
102
+ } else if (count > this.maxToolCallsPerTask) {
103
+ this.warn(taskId, `Total tool calls (${count}) exceeded max (${this.maxToolCallsPerTask})`);
104
+ }
105
+
106
+ // Check: dangerous tool patterns
107
+ if (tool_name === "executeCommand" && params) {
108
+ const cmd = Array.isArray(params) ? params[0] : params;
109
+ if (typeof cmd === "string") {
110
+ if (/rm\s+-rf\s+\//.test(cmd)) {
111
+ this.alert(taskId, `BLOCKED: Destructive command detected: ${cmd.slice(0, 50)}`);
112
+ }
113
+ if (/sudo/.test(cmd)) {
114
+ this.warn(taskId, `Sudo command detected: ${cmd.slice(0, 50)}`);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Check after a tool is executed.
122
+ */
123
+ onToolAfter(data) {
124
+ // Could add output scanning for secrets here
125
+ }
126
+
127
+ /**
128
+ * Monitor model cost.
129
+ */
130
+ onModelCalled(data) {
131
+ // Cost tracking happens in CostTracker via EventBus
132
+ }
133
+
134
+ /**
135
+ * Monitor sub-agent spawning.
136
+ */
137
+ onAgentSpawned(data) {
138
+ const { agentId, depth, parentTaskId } = data;
139
+ if (depth > 1) {
140
+ this.warn(parentTaskId, `Deep sub-agent spawning: depth=${depth}, agentId=${agentId}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Issue a warning.
146
+ */
147
+ warn(taskId, message) {
148
+ const warning = {
149
+ level: "warning",
150
+ taskId,
151
+ message,
152
+ timestamp: new Date().toISOString(),
153
+ };
154
+ this.warnings.push(warning);
155
+ console.log(`[Supervisor] WARNING (task ${taskId?.slice(0, 8)}): ${message}`);
156
+ eventBus.emitEvent("supervisor:warning", warning);
157
+ }
158
+
159
+ /**
160
+ * Issue a critical alert.
161
+ */
162
+ alert(taskId, message) {
163
+ const alert = {
164
+ level: "critical",
165
+ taskId,
166
+ message,
167
+ timestamp: new Date().toISOString(),
168
+ };
169
+ this.warnings.push(alert);
170
+ console.log(`[Supervisor] ALERT (task ${taskId?.slice(0, 8)}): ${message}`);
171
+ eventBus.emitEvent("supervisor:alert", alert);
172
+ }
173
+
174
+ /**
175
+ * Get recent warnings.
176
+ */
177
+ getWarnings(limit = 50) {
178
+ return this.warnings.slice(-limit);
179
+ }
180
+
181
+ /**
182
+ * Clean up tracking for a completed task.
183
+ */
184
+ cleanupTask(taskId) {
185
+ this.toolCallCounts.delete(taskId);
186
+ this.toolCallTimestamps.delete(taskId);
187
+ }
188
+ }
189
+
190
+ // Singleton
191
+ const supervisor = new Supervisor();
192
+ export default supervisor;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Base channel interface.
3
+ * All input channels (Telegram, WhatsApp, Email, Discord, Slack, LINE, Signal) extend this.
4
+ *
5
+ * A channel:
6
+ * 1. Receives raw input from its platform
7
+ * 2. Normalizes it into a Task (via taskQueue.enqueue)
8
+ * 3. Routes the agent's reply back to the originating platform
9
+ *
10
+ * Built-in capabilities (all channels get these for free):
11
+ * - Allowlist gating — set config.allowlist = [id, id, ...] to restrict who can send tasks.
12
+ * Empty / omitted = open to all (backward compatible).
13
+ * - Per-channel model — set config.model = "openai:gpt-4.1" to override the default model
14
+ * for all tasks coming from this channel.
15
+ * - Status reactions — sendReaction(channelMeta, emoji) is a no-op by default.
16
+ * Channels that support native reactions override this.
17
+ */
18
+ export class BaseChannel {
19
+ constructor(name, config) {
20
+ this.name = name;
21
+ this.config = config || {};
22
+ this.running = false;
23
+ }
24
+
25
+ /**
26
+ * Start listening for incoming messages.
27
+ */
28
+ async start() {
29
+ throw new Error(`${this.name}: start() not implemented`);
30
+ }
31
+
32
+ /**
33
+ * Stop listening.
34
+ */
35
+ async stop() {
36
+ throw new Error(`${this.name}: stop() not implemented`);
37
+ }
38
+
39
+ /**
40
+ * Send a reply back to the user on this channel.
41
+ * @param {object} channelMeta - Channel-specific metadata (chat_id, phone, email, etc.)
42
+ * @param {string} text - The response text
43
+ */
44
+ async sendReply(channelMeta, text) {
45
+ throw new Error(`${this.name}: sendReply() not implemented`);
46
+ }
47
+
48
+ /**
49
+ * Send a native reaction/emoji on the triggering message (optional feature).
50
+ * Channels that support reactions (Telegram, Discord, Slack) override this.
51
+ * Others silently ignore it.
52
+ *
53
+ * @param {object} channelMeta - Channel-specific metadata
54
+ * @param {string} emoji - Emoji to react with (e.g. "✅", "❌", "⏳")
55
+ */
56
+ async sendReaction(channelMeta, emoji) {
57
+ // Default no-op — channels that support reactions override this
58
+ }
59
+
60
+ /**
61
+ * Check whether a user is allowed to send tasks on this channel.
62
+ *
63
+ * If config.allowlist is empty or not set → everyone is allowed (open channel).
64
+ * If config.allowlist has entries → only those IDs/usernames are allowed.
65
+ *
66
+ * @param {string|number} userId - Platform-specific user identifier
67
+ * @returns {boolean}
68
+ */
69
+ isAllowed(userId) {
70
+ const allowlist = this.config?.allowlist;
71
+ if (!allowlist || !Array.isArray(allowlist) || allowlist.length === 0) return true;
72
+ return allowlist.map(String).includes(String(userId));
73
+ }
74
+
75
+ /**
76
+ * Get the model override for this channel (if configured).
77
+ * Returns null if no override — TaskRunner will use the global default.
78
+ * @returns {string|null}
79
+ */
80
+ getModel() {
81
+ return this.config?.model || null;
82
+ }
83
+
84
+ /**
85
+ * Map a platform user identifier to a session ID.
86
+ * Ensures continuity across messages from the same user.
87
+ * @param {string} userId - Platform-specific user identifier
88
+ * @returns {string} Session ID
89
+ */
90
+ getSessionId(userId) {
91
+ return `${this.name}-${userId}`;
92
+ }
93
+
94
+ /**
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
97
+ * in the original task's reply (like Claude Code's follow-up injection behaviour).
98
+ * @param {object} completedTask
99
+ * @returns {boolean}
100
+ */
101
+ isTaskMerged(completedTask) {
102
+ return completedTask?.merged === true;
103
+ }
104
+ }