alvin-bot 4.4.1

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 (136) hide show
  1. package/.env.example +43 -0
  2. package/BACKLOG.md +223 -0
  3. package/CHANGELOG.md +63 -0
  4. package/CLAUDE.example.md +152 -0
  5. package/CODE_OF_CONDUCT.md +52 -0
  6. package/CONTRIBUTING.md +72 -0
  7. package/LICENSE +21 -0
  8. package/README.md +529 -0
  9. package/SECURITY.md +38 -0
  10. package/SOUL.example.md +60 -0
  11. package/TOOLS.example.md +42 -0
  12. package/alvin-bot.config.example.json +24 -0
  13. package/bin/cli.js +1088 -0
  14. package/dist/.metadata_never_index +0 -0
  15. package/dist/claude.js +102 -0
  16. package/dist/config.js +65 -0
  17. package/dist/engine.js +90 -0
  18. package/dist/find-claude-binary.js +98 -0
  19. package/dist/handlers/commands.js +1489 -0
  20. package/dist/handlers/document.js +187 -0
  21. package/dist/handlers/message.js +200 -0
  22. package/dist/handlers/photo.js +154 -0
  23. package/dist/handlers/platform-message.js +275 -0
  24. package/dist/handlers/video.js +237 -0
  25. package/dist/handlers/voice.js +148 -0
  26. package/dist/i18n.js +299 -0
  27. package/dist/index.js +442 -0
  28. package/dist/init-data-dir.js +81 -0
  29. package/dist/middleware/auth.js +215 -0
  30. package/dist/migrate.js +139 -0
  31. package/dist/paths.js +87 -0
  32. package/dist/platforms/discord.js +161 -0
  33. package/dist/platforms/index.js +130 -0
  34. package/dist/platforms/signal.js +205 -0
  35. package/dist/platforms/slack.js +318 -0
  36. package/dist/platforms/telegram.js +111 -0
  37. package/dist/platforms/types.js +8 -0
  38. package/dist/platforms/whatsapp.js +648 -0
  39. package/dist/providers/claude-sdk-provider.js +173 -0
  40. package/dist/providers/codex-cli-provider.js +121 -0
  41. package/dist/providers/index.js +7 -0
  42. package/dist/providers/openai-compatible.js +388 -0
  43. package/dist/providers/registry.js +209 -0
  44. package/dist/providers/tool-executor.js +450 -0
  45. package/dist/providers/types.js +205 -0
  46. package/dist/services/access.js +144 -0
  47. package/dist/services/asset-index.js +230 -0
  48. package/dist/services/browser-manager.js +161 -0
  49. package/dist/services/browser.js +121 -0
  50. package/dist/services/compaction.js +129 -0
  51. package/dist/services/cron.js +462 -0
  52. package/dist/services/custom-tools.js +317 -0
  53. package/dist/services/delivery-queue.js +154 -0
  54. package/dist/services/elevenlabs.js +58 -0
  55. package/dist/services/embeddings.js +386 -0
  56. package/dist/services/exec-guard.js +46 -0
  57. package/dist/services/fallback-order.js +151 -0
  58. package/dist/services/heartbeat.js +192 -0
  59. package/dist/services/hooks.js +44 -0
  60. package/dist/services/imagegen.js +72 -0
  61. package/dist/services/language-detect.js +144 -0
  62. package/dist/services/markdown.js +63 -0
  63. package/dist/services/mcp.js +252 -0
  64. package/dist/services/memory.js +133 -0
  65. package/dist/services/personality.js +227 -0
  66. package/dist/services/plugins.js +171 -0
  67. package/dist/services/reminders.js +97 -0
  68. package/dist/services/restart.js +48 -0
  69. package/dist/services/security-audit.js +66 -0
  70. package/dist/services/self-search.js +129 -0
  71. package/dist/services/session.js +93 -0
  72. package/dist/services/skills.js +287 -0
  73. package/dist/services/standing-orders.js +29 -0
  74. package/dist/services/subagents.js +142 -0
  75. package/dist/services/sudo.js +243 -0
  76. package/dist/services/telegram.js +113 -0
  77. package/dist/services/tool-discovery.js +214 -0
  78. package/dist/services/usage-tracker.js +137 -0
  79. package/dist/services/users.js +199 -0
  80. package/dist/services/voice.js +95 -0
  81. package/dist/tui/index.js +507 -0
  82. package/dist/web/canvas.js +30 -0
  83. package/dist/web/doctor-api.js +606 -0
  84. package/dist/web/openai-compat.js +252 -0
  85. package/dist/web/server.js +1351 -0
  86. package/dist/web/setup-api.js +1078 -0
  87. package/docs/mcp.example.json +16 -0
  88. package/docs/screenshots/00-Login.png +0 -0
  89. package/docs/screenshots/01-Chat-Dark-Conversation.png +0 -0
  90. package/docs/screenshots/02-Chat.png +0 -0
  91. package/docs/screenshots/03-Dashboard-Overview.png +0 -0
  92. package/docs/screenshots/04-AI-Models-and-Providers.png +0 -0
  93. package/docs/screenshots/05-Personality-Editor.png +0 -0
  94. package/docs/screenshots/06-Memory-Manager.png +0 -0
  95. package/docs/screenshots/07-Active-Sessions.png +0 -0
  96. package/docs/screenshots/08-File-Browser.png +0 -0
  97. package/docs/screenshots/09-Scheduled-Jobs.png +0 -0
  98. package/docs/screenshots/10-Custom-Tools.png +0 -0
  99. package/docs/screenshots/11-Plugins-and-MCP.png +0 -0
  100. package/docs/screenshots/12-Messaging-Platforms.png +0 -0
  101. package/docs/screenshots/12.1-Messaging-Platforms-WhatsApp-Groups-List.png +0 -0
  102. package/docs/screenshots/12.2-Messaging-Platforms-WA-Group-Details.png +0 -0
  103. package/docs/screenshots/13-User-Management.png +0 -0
  104. package/docs/screenshots/14-Web-Terminal.png +0 -0
  105. package/docs/screenshots/15-Maintenance-and-Health.png +0 -0
  106. package/docs/screenshots/16-Settings-and-Env.png +0 -0
  107. package/docs/screenshots/TG-commands.png +0 -0
  108. package/docs/screenshots/TG.png +0 -0
  109. package/docs/screenshots/_Mac-Installer.png +0 -0
  110. package/docs/tools.example.json +33 -0
  111. package/install.sh +165 -0
  112. package/package.json +190 -0
  113. package/plugins/calendar/index.js +270 -0
  114. package/plugins/email/index.js +231 -0
  115. package/plugins/finance/index.js +254 -0
  116. package/plugins/notes/index.js +227 -0
  117. package/plugins/smarthome/index.js +230 -0
  118. package/plugins/weather/index.js +122 -0
  119. package/skills/apple-notes/SKILL.md +31 -0
  120. package/skills/browse/SKILL.md +136 -0
  121. package/skills/code-project/SKILL.md +43 -0
  122. package/skills/data-analysis/SKILL.md +39 -0
  123. package/skills/document-creation/SKILL.md +48 -0
  124. package/skills/email-summary/SKILL.md +46 -0
  125. package/skills/github/SKILL.md +42 -0
  126. package/skills/summarize/SKILL.md +28 -0
  127. package/skills/system-admin/SKILL.md +39 -0
  128. package/skills/weather/SKILL.md +34 -0
  129. package/skills/web-research/SKILL.md +35 -0
  130. package/web/public/canvas.html +52 -0
  131. package/web/public/css/style.css +555 -0
  132. package/web/public/index.html +189 -0
  133. package/web/public/js/app.js +3102 -0
  134. package/web/public/js/i18n.js +1048 -0
  135. package/web/public/js/icons.js +104 -0
  136. package/web/public/login.html +48 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Skill System — Specialized knowledge for complex tasks.
3
+ *
4
+ * Skills are SKILL.md files in the skills/ directory that provide
5
+ * domain-specific instructions, workflows, and best practices.
6
+ *
7
+ * When a user message matches a skill's triggers, the skill's content
8
+ * is injected into the system prompt — giving the agent deep expertise
9
+ * for that specific task type.
10
+ *
11
+ * Philosophy: A generalist agent with specialist knowledge on demand.
12
+ *
13
+ * Features:
14
+ * - Bundled skills (skills/ in repo) + User skills (~/.alvin-bot/skills/)
15
+ * - User skills override bundled skills with the same ID
16
+ * - Hot-reload via fs.watch() on both directories
17
+ * - Self-modification via createSkill()
18
+ */
19
+ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, watch } from "fs";
20
+ import { resolve } from "path";
21
+ import { SKILLS_DIR } from "../paths.js";
22
+ import { USER_SKILLS_DIR } from "../paths.js";
23
+ import { loadAssetIndex } from "./asset-index.js";
24
+ // ── Skill Registry ──────────────────────────────────────
25
+ let cachedSkills = [];
26
+ let lastScanAt = 0;
27
+ /**
28
+ * Parse SKILL.md frontmatter (simple YAML-like header).
29
+ *
30
+ * Format:
31
+ * ---
32
+ * name: Video Creation
33
+ * description: Create videos with Remotion
34
+ * triggers: video, remotion, animation, render
35
+ * priority: 5
36
+ * category: media
37
+ * ---
38
+ * (rest is the skill content)
39
+ */
40
+ function parseSkillFile(id, content, source) {
41
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
42
+ if (!fmMatch) {
43
+ // No frontmatter — treat entire file as content with defaults
44
+ return {
45
+ id,
46
+ name: id.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()),
47
+ description: "",
48
+ triggers: [id.replace(/-/g, " ")],
49
+ content: content.trim(),
50
+ priority: 1,
51
+ category: "general",
52
+ source,
53
+ };
54
+ }
55
+ const frontmatter = fmMatch[1];
56
+ const body = fmMatch[2].trim();
57
+ function getField(key) {
58
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
59
+ return match ? match[1].trim() : "";
60
+ }
61
+ const name = getField("name") || id;
62
+ const description = getField("description") || "";
63
+ const triggersRaw = getField("triggers") || id;
64
+ const priority = parseInt(getField("priority")) || 1;
65
+ const category = getField("category") || "general";
66
+ const assetCategoriesRaw = getField("assetCategories");
67
+ const assetCategories = assetCategoriesRaw
68
+ ? assetCategoriesRaw.replace(/[\[\]]/g, "").split(",").map(s => s.trim()).filter(Boolean)
69
+ : undefined;
70
+ const triggers = triggersRaw
71
+ .split(",")
72
+ .map(t => t.trim().toLowerCase())
73
+ .filter(Boolean);
74
+ return { id, name, description, triggers, content: body, priority, category, source, assetCategories };
75
+ }
76
+ /**
77
+ * Scan a single skills directory and return all parsed skills.
78
+ */
79
+ function scanDirectory(dir, source) {
80
+ if (!existsSync(dir))
81
+ return [];
82
+ const skills = [];
83
+ const entries = readdirSync(dir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ if (entry.isDirectory()) {
86
+ const skillFile = resolve(dir, entry.name, "SKILL.md");
87
+ if (existsSync(skillFile)) {
88
+ try {
89
+ const content = readFileSync(skillFile, "utf-8");
90
+ const skill = parseSkillFile(entry.name, content, source);
91
+ if (skill)
92
+ skills.push(skill);
93
+ }
94
+ catch (err) {
95
+ console.warn(`\u26a0\ufe0f Failed to load skill ${entry.name}:`, err);
96
+ }
97
+ }
98
+ }
99
+ // Also support flat .md files in skills/
100
+ if (entry.isFile() && entry.name.endsWith(".md")) {
101
+ const id = entry.name.replace(/\.md$/, "");
102
+ try {
103
+ const content = readFileSync(resolve(dir, entry.name), "utf-8");
104
+ const skill = parseSkillFile(id, content, source);
105
+ if (skill)
106
+ skills.push(skill);
107
+ }
108
+ catch (err) {
109
+ console.warn(`\u26a0\ufe0f Failed to load skill ${id}:`, err);
110
+ }
111
+ }
112
+ }
113
+ return skills;
114
+ }
115
+ /**
116
+ * Reload all skills from both directories.
117
+ * User skills override bundled skills with the same ID.
118
+ */
119
+ function reloadAllSkills() {
120
+ // Ensure bundled directory exists
121
+ if (!existsSync(SKILLS_DIR)) {
122
+ mkdirSync(SKILLS_DIR, { recursive: true });
123
+ }
124
+ const bundled = scanDirectory(SKILLS_DIR, "bundled");
125
+ const user = scanDirectory(USER_SKILLS_DIR, "user");
126
+ // Merge: user skills override bundled skills with same ID
127
+ const skillMap = new Map();
128
+ for (const s of bundled)
129
+ skillMap.set(s.id, s);
130
+ for (const s of user)
131
+ skillMap.set(s.id, s); // override
132
+ cachedSkills = [...skillMap.values()];
133
+ lastScanAt = Date.now();
134
+ if (cachedSkills.length > 0) {
135
+ const bundledCount = cachedSkills.filter(s => s.source === "bundled").length;
136
+ const userCount = cachedSkills.filter(s => s.source === "user").length;
137
+ console.log(`\ud83c\udfaf Skills loaded: ${cachedSkills.length} (${bundledCount} bundled, ${userCount} user) — ${cachedSkills.map(s => s.name).join(", ")}`);
138
+ }
139
+ }
140
+ /**
141
+ * Scan both skills directories and load all SKILL.md files.
142
+ * Sets up fs.watch() for hot-reload on both directories.
143
+ */
144
+ export function loadSkills() {
145
+ reloadAllSkills();
146
+ // Hot-reload watchers
147
+ try {
148
+ watch(SKILLS_DIR, { recursive: true }, () => {
149
+ console.log("Skills changed (bundled) \u2014 reloading");
150
+ reloadAllSkills();
151
+ });
152
+ }
153
+ catch { }
154
+ try {
155
+ if (existsSync(USER_SKILLS_DIR)) {
156
+ watch(USER_SKILLS_DIR, { recursive: true }, () => {
157
+ console.log("Skills changed (user) \u2014 reloading");
158
+ reloadAllSkills();
159
+ });
160
+ }
161
+ }
162
+ catch { }
163
+ return cachedSkills;
164
+ }
165
+ /**
166
+ * Get all loaded skills.
167
+ */
168
+ export function getSkills() {
169
+ if (cachedSkills.length === 0 || Date.now() - lastScanAt > 300_000) {
170
+ reloadAllSkills();
171
+ }
172
+ return cachedSkills;
173
+ }
174
+ /**
175
+ * Find a skill by its ID.
176
+ */
177
+ export function getSkillById(id) {
178
+ return cachedSkills.find(s => s.id === id);
179
+ }
180
+ /**
181
+ * Create or update a user skill (self-modification).
182
+ * Writes to USER_SKILLS_DIR and triggers reload.
183
+ */
184
+ export function createSkill(id, content) {
185
+ const dir = resolve(USER_SKILLS_DIR, id);
186
+ if (!existsSync(dir))
187
+ mkdirSync(dir, { recursive: true });
188
+ writeFileSync(resolve(dir, "SKILL.md"), content);
189
+ // Trigger reload
190
+ reloadAllSkills();
191
+ return true;
192
+ }
193
+ /**
194
+ * Find skills that match a user message.
195
+ * Returns matched skills sorted by priority (highest first).
196
+ */
197
+ export function matchSkills(userMessage, maxResults = 2) {
198
+ const skills = getSkills();
199
+ if (skills.length === 0)
200
+ return [];
201
+ const msgLower = userMessage.toLowerCase();
202
+ const words = msgLower.split(/[\s,.!?;:()[\]{}'"]+/).filter(w => w.length >= 2);
203
+ const wordSet = new Set(words);
204
+ const scored = [];
205
+ for (const skill of skills) {
206
+ let score = 0;
207
+ for (const trigger of skill.triggers) {
208
+ // Exact phrase match (strongest signal)
209
+ if (msgLower.includes(trigger)) {
210
+ score += trigger.split(" ").length * 3; // multi-word triggers score higher
211
+ }
212
+ // Single-word trigger match
213
+ else if (trigger.split(" ").length === 1 && wordSet.has(trigger)) {
214
+ score += 1;
215
+ }
216
+ }
217
+ if (score > 0) {
218
+ scored.push({ skill, score: score * skill.priority });
219
+ }
220
+ }
221
+ return scored
222
+ .sort((a, b) => b.score - a.score)
223
+ .slice(0, maxResults)
224
+ .map(s => s.skill);
225
+ }
226
+ // ── Skill-Asset Mapping ────────────────────────────────
227
+ /** Default mapping for skills that don't declare assetCategories in frontmatter. */
228
+ const SKILL_ASSET_MAP = {
229
+ "job-apply": ["cover-letters", "cv-templates", "photos"],
230
+ "cv-update": ["cv-templates", "photos"],
231
+ "cover-letter": ["cover-letters", "cv-templates"],
232
+ "formal-letter": ["legal", "cv-templates"],
233
+ };
234
+ /**
235
+ * Find assets relevant to a skill.
236
+ * Uses frontmatter assetCategories if declared, otherwise falls back to static map.
237
+ */
238
+ function findAssetsForSkill(skill) {
239
+ const categories = skill.assetCategories || SKILL_ASSET_MAP[skill.id];
240
+ if (!categories || categories.length === 0)
241
+ return [];
242
+ const index = loadAssetIndex();
243
+ return index.assets.filter(a => categories.includes(a.category));
244
+ }
245
+ /**
246
+ * Build a skill injection block for the system prompt.
247
+ * Includes matched skill content + relevant asset references.
248
+ */
249
+ export function buildSkillContext(userMessage) {
250
+ const matched = matchSkills(userMessage, 1); // inject top 1 skill only
251
+ if (matched.length === 0)
252
+ return "";
253
+ const skill = matched[0];
254
+ let context = `\n\n## 🎯 Active Skill: ${skill.name}\n\n${skill.content}`;
255
+ // Inject relevant assets for this skill
256
+ const assets = findAssetsForSkill(skill);
257
+ if (assets.length > 0) {
258
+ context += `\n\n### 📂 Relevant Assets\n`;
259
+ for (const a of assets) {
260
+ context += `- ${a.category}/${a.filename} → \`${a.absolutePath}\`\n`;
261
+ }
262
+ }
263
+ return context;
264
+ }
265
+ /**
266
+ * Get a summary of all available skills (for /skills command or status).
267
+ */
268
+ export function getSkillsSummary() {
269
+ const skills = getSkills();
270
+ if (skills.length === 0)
271
+ return "No skills installed.";
272
+ const byCategory = new Map();
273
+ for (const s of skills) {
274
+ const list = byCategory.get(s.category) || [];
275
+ list.push(s);
276
+ byCategory.set(s.category, list);
277
+ }
278
+ const lines = [`\ud83c\udfaf **Skills (${skills.length}):**\n`];
279
+ for (const [cat, list] of byCategory) {
280
+ lines.push(`**${cat}:**`);
281
+ for (const s of list) {
282
+ const badge = s.source === "user" ? " \ud83d\udc64" : "";
283
+ lines.push(` \u2022 ${s.name}${badge} \u2014 ${s.description || "(no description)"}`);
284
+ }
285
+ }
286
+ return lines.join("\n");
287
+ }
@@ -0,0 +1,29 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { AGENTS_FILE } from "../paths.js";
3
+ let cached = "";
4
+ /** Load standing orders from AGENTS.md. Called once at startup and on reload. */
5
+ export function loadStandingOrders() {
6
+ if (!existsSync(AGENTS_FILE))
7
+ return "";
8
+ try {
9
+ cached = readFileSync(AGENTS_FILE, "utf-8");
10
+ return cached;
11
+ }
12
+ catch {
13
+ return "";
14
+ }
15
+ }
16
+ /** Get cached standing orders (fast, no disk I/O) */
17
+ export function getStandingOrders() {
18
+ return cached;
19
+ }
20
+ /** Reload from disk (e.g., after editing via tools) */
21
+ export function reloadStandingOrders() {
22
+ try {
23
+ cached = readFileSync(AGENTS_FILE, "utf-8");
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Sub-Agent System — Parallel Task Execution
3
+ *
4
+ * Spawns isolated AI workers that run in the background using the engine registry.
5
+ * Each sub-agent gets its own query call (not a persistent session).
6
+ * Results are stored and can be retrieved by the caller.
7
+ */
8
+ import os from "os";
9
+ import crypto from "crypto";
10
+ import { config } from "../config.js";
11
+ // ── State ───────────────────────────────────────────────
12
+ const activeAgents = new Map();
13
+ // ── Core execution ──────────────────────────────────────
14
+ async function runSubAgent(id, agentConfig, abort) {
15
+ const startTime = Date.now();
16
+ const entry = activeAgents.get(id);
17
+ try {
18
+ const { getRegistry } = await import("../engine.js");
19
+ const registry = getRegistry();
20
+ const systemPrompt = `You are a sub-agent named "${agentConfig.name}". Complete the following task autonomously and report your results clearly when done. Working directory: ${agentConfig.workingDir || os.homedir()}`;
21
+ let finalText = "";
22
+ let inputTokens = 0;
23
+ let outputTokens = 0;
24
+ for await (const chunk of registry.queryWithFallback({
25
+ prompt: agentConfig.prompt,
26
+ systemPrompt,
27
+ workingDir: agentConfig.workingDir || os.homedir(),
28
+ effort: "high",
29
+ abortSignal: abort.signal,
30
+ })) {
31
+ if (chunk.type === "text")
32
+ finalText = chunk.text || "";
33
+ if (chunk.type === "done") {
34
+ inputTokens = chunk.inputTokens || 0;
35
+ outputTokens = chunk.outputTokens || 0;
36
+ }
37
+ }
38
+ entry.result = {
39
+ id,
40
+ name: agentConfig.name,
41
+ status: "completed",
42
+ output: finalText,
43
+ tokensUsed: { input: inputTokens, output: outputTokens },
44
+ duration: Date.now() - startTime,
45
+ };
46
+ entry.info.status = "completed";
47
+ }
48
+ catch (err) {
49
+ const isAbort = err instanceof Error && err.message.includes("abort");
50
+ const isTimeout = abort.signal.aborted;
51
+ const status = isTimeout
52
+ ? "timeout"
53
+ : isAbort
54
+ ? "cancelled"
55
+ : "error";
56
+ entry.result = {
57
+ id,
58
+ name: agentConfig.name,
59
+ status,
60
+ output: "",
61
+ tokensUsed: { input: 0, output: 0 },
62
+ duration: Date.now() - startTime,
63
+ error: err instanceof Error ? err.message : String(err),
64
+ };
65
+ entry.info.status = status;
66
+ }
67
+ }
68
+ // ── Public API ──────────────────────────────────────────
69
+ /**
70
+ * Spawn an isolated sub-agent that runs in the background.
71
+ * Returns the agent ID immediately (does NOT await completion).
72
+ */
73
+ export function spawnSubAgent(agentConfig) {
74
+ // Check concurrency limit
75
+ const runningCount = [...activeAgents.values()].filter((a) => a.info.status === "running").length;
76
+ if (runningCount >= config.maxSubAgents) {
77
+ return Promise.reject(new Error(`Sub-agent limit reached (${config.maxSubAgents}). Wait for a running agent to finish or cancel one.`));
78
+ }
79
+ const id = crypto.randomUUID();
80
+ const timeout = agentConfig.timeout ?? config.subAgentTimeout;
81
+ const abort = new AbortController();
82
+ // Set up timeout
83
+ const timeoutId = setTimeout(() => abort.abort(), timeout);
84
+ const info = {
85
+ id,
86
+ name: agentConfig.name,
87
+ status: "running",
88
+ startedAt: Date.now(),
89
+ model: agentConfig.model,
90
+ };
91
+ activeAgents.set(id, { info, abort });
92
+ // Run in background — don't await
93
+ runSubAgent(id, agentConfig, abort)
94
+ .finally(() => {
95
+ clearTimeout(timeoutId);
96
+ // Auto-cleanup: remove completed agents after 30 minutes
97
+ setTimeout(() => {
98
+ const entry = activeAgents.get(id);
99
+ if (entry && entry.info.status !== "running") {
100
+ activeAgents.delete(id);
101
+ }
102
+ }, 30 * 60 * 1000);
103
+ });
104
+ return Promise.resolve(id);
105
+ }
106
+ /**
107
+ * List all agents (active + recent completed).
108
+ */
109
+ export function listSubAgents() {
110
+ return [...activeAgents.values()].map((a) => ({ ...a.info }));
111
+ }
112
+ /**
113
+ * Cancel a running sub-agent by ID.
114
+ * Returns true if the agent was found and aborted.
115
+ */
116
+ export function cancelSubAgent(id) {
117
+ const entry = activeAgents.get(id);
118
+ if (!entry || entry.info.status !== "running")
119
+ return false;
120
+ entry.abort.abort();
121
+ entry.info.status = "cancelled";
122
+ return true;
123
+ }
124
+ /**
125
+ * Get the result of a completed sub-agent.
126
+ * Returns null if not found or still running.
127
+ */
128
+ export function getSubAgentResult(id) {
129
+ const entry = activeAgents.get(id);
130
+ return entry?.result ?? null;
131
+ }
132
+ /**
133
+ * Cancel all active sub-agents. Used during shutdown.
134
+ */
135
+ export function cancelAllSubAgents() {
136
+ for (const [id, entry] of activeAgents) {
137
+ if (entry.info.status === "running") {
138
+ entry.abort.abort();
139
+ entry.info.status = "cancelled";
140
+ }
141
+ }
142
+ }