skimpyclaw 0.1.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 (219) hide show
  1. package/README.md +230 -0
  2. package/dist/__tests__/agent.test.d.ts +1 -0
  3. package/dist/__tests__/agent.test.js +131 -0
  4. package/dist/__tests__/api.test.d.ts +1 -0
  5. package/dist/__tests__/api.test.js +1227 -0
  6. package/dist/__tests__/audit.test.d.ts +1 -0
  7. package/dist/__tests__/audit.test.js +122 -0
  8. package/dist/__tests__/cache.test.d.ts +1 -0
  9. package/dist/__tests__/cache.test.js +65 -0
  10. package/dist/__tests__/channels.test.d.ts +1 -0
  11. package/dist/__tests__/channels.test.js +85 -0
  12. package/dist/__tests__/cli.integration.test.d.ts +1 -0
  13. package/dist/__tests__/cli.integration.test.js +16 -0
  14. package/dist/__tests__/cli.test.d.ts +1 -0
  15. package/dist/__tests__/cli.test.js +230 -0
  16. package/dist/__tests__/code-agents-executor.test.d.ts +1 -0
  17. package/dist/__tests__/code-agents-executor.test.js +75 -0
  18. package/dist/__tests__/code-agents-orchestrator.test.d.ts +1 -0
  19. package/dist/__tests__/code-agents-orchestrator.test.js +149 -0
  20. package/dist/__tests__/code-agents-parser.test.d.ts +1 -0
  21. package/dist/__tests__/code-agents-parser.test.js +39 -0
  22. package/dist/__tests__/code-agents-utils.test.d.ts +1 -0
  23. package/dist/__tests__/code-agents-utils.test.js +41 -0
  24. package/dist/__tests__/config.test.d.ts +1 -0
  25. package/dist/__tests__/config.test.js +46 -0
  26. package/dist/__tests__/cron.test.d.ts +1 -0
  27. package/dist/__tests__/cron.test.js +66 -0
  28. package/dist/__tests__/dashboard-mode.test.d.ts +1 -0
  29. package/dist/__tests__/dashboard-mode.test.js +145 -0
  30. package/dist/__tests__/dashboard.test.d.ts +1 -0
  31. package/dist/__tests__/dashboard.test.js +43 -0
  32. package/dist/__tests__/doctor.formatters.test.d.ts +1 -0
  33. package/dist/__tests__/doctor.formatters.test.js +65 -0
  34. package/dist/__tests__/doctor.index.test.d.ts +1 -0
  35. package/dist/__tests__/doctor.index.test.js +48 -0
  36. package/dist/__tests__/doctor.runner.test.d.ts +1 -0
  37. package/dist/__tests__/doctor.runner.test.js +204 -0
  38. package/dist/__tests__/exec-approval.test.d.ts +1 -0
  39. package/dist/__tests__/exec-approval.test.js +323 -0
  40. package/dist/__tests__/file-lock.test.d.ts +1 -0
  41. package/dist/__tests__/file-lock.test.js +92 -0
  42. package/dist/__tests__/langfuse.test.d.ts +1 -0
  43. package/dist/__tests__/langfuse.test.js +40 -0
  44. package/dist/__tests__/model-selection.test.d.ts +1 -0
  45. package/dist/__tests__/model-selection.test.js +62 -0
  46. package/dist/__tests__/orchestrator.test.d.ts +1 -0
  47. package/dist/__tests__/orchestrator.test.js +425 -0
  48. package/dist/__tests__/providers-init.test.d.ts +1 -0
  49. package/dist/__tests__/providers-init.test.js +32 -0
  50. package/dist/__tests__/providers-routing.test.d.ts +1 -0
  51. package/dist/__tests__/providers-routing.test.js +25 -0
  52. package/dist/__tests__/providers-utils.test.d.ts +1 -0
  53. package/dist/__tests__/providers-utils.test.js +54 -0
  54. package/dist/__tests__/security.test.d.ts +1 -0
  55. package/dist/__tests__/security.test.js +22 -0
  56. package/dist/__tests__/sessions.test.d.ts +1 -0
  57. package/dist/__tests__/sessions.test.js +147 -0
  58. package/dist/__tests__/setup.test.d.ts +1 -0
  59. package/dist/__tests__/setup.test.js +114 -0
  60. package/dist/__tests__/skills.test.d.ts +1 -0
  61. package/dist/__tests__/skills.test.js +333 -0
  62. package/dist/__tests__/subagent.test.d.ts +1 -0
  63. package/dist/__tests__/subagent.test.js +240 -0
  64. package/dist/__tests__/telegram-utils.test.d.ts +1 -0
  65. package/dist/__tests__/telegram-utils.test.js +22 -0
  66. package/dist/__tests__/telegram.test.d.ts +1 -0
  67. package/dist/__tests__/telegram.test.js +42 -0
  68. package/dist/__tests__/token-efficiency.test.d.ts +1 -0
  69. package/dist/__tests__/token-efficiency.test.js +38 -0
  70. package/dist/__tests__/tool-guard.test.d.ts +1 -0
  71. package/dist/__tests__/tool-guard.test.js +105 -0
  72. package/dist/__tests__/tools.test.d.ts +1 -0
  73. package/dist/__tests__/tools.test.js +589 -0
  74. package/dist/__tests__/usage.test.d.ts +1 -0
  75. package/dist/__tests__/usage.test.js +197 -0
  76. package/dist/__tests__/voice.test.d.ts +1 -0
  77. package/dist/__tests__/voice.test.js +214 -0
  78. package/dist/agent.d.ts +24 -0
  79. package/dist/agent.js +269 -0
  80. package/dist/api.d.ts +3 -0
  81. package/dist/api.js +943 -0
  82. package/dist/audit.d.ts +26 -0
  83. package/dist/audit.js +121 -0
  84. package/dist/cache.d.ts +8 -0
  85. package/dist/cache.js +24 -0
  86. package/dist/channels/telegram/handlers.d.ts +41 -0
  87. package/dist/channels/telegram/handlers.js +498 -0
  88. package/dist/channels/telegram/index.d.ts +14 -0
  89. package/dist/channels/telegram/index.js +326 -0
  90. package/dist/channels/telegram/types.d.ts +26 -0
  91. package/dist/channels/telegram/types.js +31 -0
  92. package/dist/channels/telegram/utils.d.ts +25 -0
  93. package/dist/channels/telegram/utils.js +256 -0
  94. package/dist/channels.d.ts +11 -0
  95. package/dist/channels.js +118 -0
  96. package/dist/cli.d.ts +5 -0
  97. package/dist/cli.js +768 -0
  98. package/dist/code-agents/executor.d.ts +5 -0
  99. package/dist/code-agents/executor.js +463 -0
  100. package/dist/code-agents/index.d.ts +22 -0
  101. package/dist/code-agents/index.js +199 -0
  102. package/dist/code-agents/orchestrator.d.ts +23 -0
  103. package/dist/code-agents/orchestrator.js +403 -0
  104. package/dist/code-agents/parser.d.ts +21 -0
  105. package/dist/code-agents/parser.js +197 -0
  106. package/dist/code-agents/registry.d.ts +27 -0
  107. package/dist/code-agents/registry.js +147 -0
  108. package/dist/code-agents/types.d.ts +66 -0
  109. package/dist/code-agents/types.js +4 -0
  110. package/dist/code-agents/utils.d.ts +36 -0
  111. package/dist/code-agents/utils.js +236 -0
  112. package/dist/config.d.ts +19 -0
  113. package/dist/config.js +123 -0
  114. package/dist/cron.d.ts +49 -0
  115. package/dist/cron.js +400 -0
  116. package/dist/dashboard/assets/index-CZJCvMSN.js +65 -0
  117. package/dist/dashboard/assets/index-EAg6lqF5.css +1 -0
  118. package/dist/dashboard/favicon.svg +3 -0
  119. package/dist/dashboard/index.html +21 -0
  120. package/dist/dashboard-frontend.d.ts +7 -0
  121. package/dist/dashboard-frontend.js +86 -0
  122. package/dist/dashboard.d.ts +8 -0
  123. package/dist/dashboard.js +4071 -0
  124. package/dist/digests.d.ts +36 -0
  125. package/dist/digests.js +338 -0
  126. package/dist/discord.d.ts +8 -0
  127. package/dist/discord.js +828 -0
  128. package/dist/doctor/checks.d.ts +18 -0
  129. package/dist/doctor/checks.js +368 -0
  130. package/dist/doctor/formatters.d.ts +3 -0
  131. package/dist/doctor/formatters.js +44 -0
  132. package/dist/doctor/index.d.ts +8 -0
  133. package/dist/doctor/index.js +7 -0
  134. package/dist/doctor/runner.d.ts +3 -0
  135. package/dist/doctor/runner.js +109 -0
  136. package/dist/doctor/types.d.ts +20 -0
  137. package/dist/doctor/types.js +1 -0
  138. package/dist/exec-approval.d.ts +101 -0
  139. package/dist/exec-approval.js +432 -0
  140. package/dist/file-lock.d.ts +34 -0
  141. package/dist/file-lock.js +81 -0
  142. package/dist/gateway.d.ts +8 -0
  143. package/dist/gateway.js +114 -0
  144. package/dist/heartbeat.d.ts +4 -0
  145. package/dist/heartbeat.js +101 -0
  146. package/dist/index.d.ts +1 -0
  147. package/dist/index.js +75 -0
  148. package/dist/langfuse.d.ts +34 -0
  149. package/dist/langfuse.js +145 -0
  150. package/dist/mcp-context-a8c.d.ts +13 -0
  151. package/dist/mcp-context-a8c.js +34 -0
  152. package/dist/model-selection.d.ts +18 -0
  153. package/dist/model-selection.js +50 -0
  154. package/dist/orchestrator.d.ts +15 -0
  155. package/dist/orchestrator.js +676 -0
  156. package/dist/providers/anthropic.d.ts +7 -0
  157. package/dist/providers/anthropic.js +319 -0
  158. package/dist/providers/codex.d.ts +17 -0
  159. package/dist/providers/codex.js +508 -0
  160. package/dist/providers/content.d.ts +21 -0
  161. package/dist/providers/content.js +55 -0
  162. package/dist/providers/index.d.ts +13 -0
  163. package/dist/providers/index.js +138 -0
  164. package/dist/providers/observability.d.ts +19 -0
  165. package/dist/providers/observability.js +94 -0
  166. package/dist/providers/openai.d.ts +10 -0
  167. package/dist/providers/openai.js +310 -0
  168. package/dist/providers/tool-guard.d.ts +30 -0
  169. package/dist/providers/tool-guard.js +89 -0
  170. package/dist/providers/types.d.ts +34 -0
  171. package/dist/providers/types.js +2 -0
  172. package/dist/providers/utils.d.ts +65 -0
  173. package/dist/providers/utils.js +199 -0
  174. package/dist/security.d.ts +8 -0
  175. package/dist/security.js +113 -0
  176. package/dist/service.d.ts +8 -0
  177. package/dist/service.js +38 -0
  178. package/dist/sessions.d.ts +35 -0
  179. package/dist/sessions.js +142 -0
  180. package/dist/setup.d.ts +36 -0
  181. package/dist/setup.js +821 -0
  182. package/dist/skills-types.d.ts +65 -0
  183. package/dist/skills-types.js +2 -0
  184. package/dist/skills.d.ts +32 -0
  185. package/dist/skills.js +260 -0
  186. package/dist/subagent.d.ts +19 -0
  187. package/dist/subagent.js +376 -0
  188. package/dist/telegram.d.ts +2 -0
  189. package/dist/telegram.js +11 -0
  190. package/dist/tools/bash-tool.d.ts +3 -0
  191. package/dist/tools/bash-tool.js +59 -0
  192. package/dist/tools/browser-tool.d.ts +3 -0
  193. package/dist/tools/browser-tool.js +265 -0
  194. package/dist/tools/definitions.d.ts +432 -0
  195. package/dist/tools/definitions.js +181 -0
  196. package/dist/tools/execute-context.d.ts +26 -0
  197. package/dist/tools/execute-context.js +1 -0
  198. package/dist/tools/file-tools.d.ts +8 -0
  199. package/dist/tools/file-tools.js +67 -0
  200. package/dist/tools/path-utils.d.ts +1 -0
  201. package/dist/tools/path-utils.js +8 -0
  202. package/dist/tools.d.ts +24 -0
  203. package/dist/tools.js +281 -0
  204. package/dist/types.d.ts +259 -0
  205. package/dist/types.js +2 -0
  206. package/dist/usage.d.ts +76 -0
  207. package/dist/usage.js +150 -0
  208. package/dist/voice.d.ts +37 -0
  209. package/dist/voice.js +461 -0
  210. package/package.json +70 -0
  211. package/templates/AGENTS.md +38 -0
  212. package/templates/BOOT.md +23 -0
  213. package/templates/BOOTSTRAP.md +26 -0
  214. package/templates/HEARTBEAT.md +5 -0
  215. package/templates/IDENTITY.md +5 -0
  216. package/templates/MEMORY.md +24 -0
  217. package/templates/SOUL.md +92 -0
  218. package/templates/TOOLS.md +30 -0
  219. package/templates/USER.md +31 -0
@@ -0,0 +1,376 @@
1
+ // Subagent system: background task dispatch with concurrency control
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
5
+ import { runAgentTurn } from './agent.js';
6
+ import { getCurrentModel } from './gateway.js';
7
+ import { getAgentDir } from './config.js';
8
+ import { releaseAllLocks } from './file-lock.js';
9
+ import { resolveModel } from './providers/utils.js';
10
+ const DEFAULT_MAX_CONCURRENT = 5;
11
+ const DEFAULT_MAX_RETRIES = 2;
12
+ const REGISTRY_PATH = join(homedir(), '.skimpyclaw', 'logs', 'subagent-runs.jsonl');
13
+ const PRESETS = {
14
+ coding: {
15
+ agentId: 'coding',
16
+ defaultModel: 'claude-opus',
17
+ toolConfig: {
18
+ enabled: true,
19
+ allowedPaths: [join(homedir(), '.skimpyclaw')],
20
+ maxIterations: 50,
21
+ bashTimeout: 30000
22
+ },
23
+ description: 'Code tasks with broad file + bash access'
24
+ },
25
+ research: {
26
+ agentId: 'research',
27
+ defaultModel: 'claude-think',
28
+ toolConfig: {
29
+ enabled: true,
30
+ allowedPaths: [join(homedir(), '.skimpyclaw')],
31
+ maxIterations: 30,
32
+ bashTimeout: 15000
33
+ },
34
+ description: 'Research tasks with configurable file access'
35
+ },
36
+ };
37
+ // --- Starter Templates ---
38
+ const STARTER_TEMPLATES = {
39
+ coding: {
40
+ identity: `# IDENTITY.md - Coding Subagent
41
+
42
+ Name: Coding Agent
43
+ Emoji: 🔧
44
+
45
+ You are a coding subagent dispatched for a specific task. You have file and bash access
46
+ within your allowed paths (provided at runtime).
47
+
48
+ ## Your Role
49
+ - Execute coding tasks: write code, fix bugs, refactor, run commands
50
+ - Act on instructions directly — don't narrate what you're going to do
51
+ - Return concise results when done
52
+
53
+ ## Your Limitations
54
+ - You are short-lived: complete the task and return the result
55
+ - You have ONLY 4 tools: Read, Write, Glob, Bash
56
+ - Do NOT invent or hallucinate tools that don't exist
57
+ `,
58
+ tools: `# TOOLS.md - Coding Subagent Tools
59
+
60
+ ## CRITICAL: Act, Don't Narrate
61
+
62
+ **NEVER say "let me write the code" or "now I'll update the file" — just call the tool.**
63
+ You have a LIMITED number of tool iterations. Every message you spend talking about what
64
+ you're going to do is one less chance to actually do it.
65
+
66
+ ## Your 4 Tools
67
+
68
+ ### Read
69
+ Read the contents of a file. Parameter: \`file_path\` (string, required)
70
+
71
+ ### Write
72
+ Write content to a file. Parameters: \`file_path\` (string, required), \`content\` (string, required)
73
+
74
+ ### Glob
75
+ List files and directories at a path. Parameter: \`path\` (string, required)
76
+
77
+ ### Bash
78
+ Execute a shell command. Parameters: \`command\` (string, required), \`cwd\` (string, optional)
79
+
80
+ ## Key Paths
81
+ - Config: ~/.skimpyclaw/config.json
82
+ - Agent templates: ~/.skimpyclaw/agents/
83
+ `
84
+ },
85
+ research: {
86
+ identity: `# IDENTITY.md - Research Subagent
87
+
88
+ Name: Research Agent
89
+ Emoji: 🔍
90
+
91
+ You are a research subagent dispatched for a specific task.
92
+
93
+ ## Your Role
94
+ - Research questions using vault notes and files
95
+ - Summarize findings concisely
96
+ - Act on instructions directly — don't narrate what you're going to do
97
+
98
+ ## Your Limitations
99
+ - You are short-lived: complete the task and return the result
100
+ - You have ONLY 4 tools: Read, Write, Glob, Bash
101
+ - Do NOT invent or hallucinate tools that don't exist
102
+ `,
103
+ tools: `# TOOLS.md - Research Subagent Tools
104
+
105
+ ## CRITICAL: Act, Don't Narrate
106
+
107
+ **NEVER say "let me check" or "I'll look into that" — just call the tool.**
108
+
109
+ ## Your 4 Tools
110
+
111
+ ### Read
112
+ Read the contents of a file. Parameter: \`file_path\` (string, required)
113
+
114
+ ### Write
115
+ Write content to a file. Parameters: \`file_path\` (string, required), \`content\` (string, required)
116
+
117
+ ### Glob
118
+ List files and directories at a path. Parameter: \`path\` (string, required)
119
+
120
+ ### Bash
121
+ Execute a shell command. Parameters: \`command\` (string, required), \`cwd\` (string, optional)
122
+
123
+ ## Key Paths
124
+ - Config: ~/.skimpyclaw/config.json
125
+ - Agent templates: ~/.skimpyclaw/agents/
126
+ - Add your notes/vault paths to allowedPaths in config before using them
127
+ `
128
+ },
129
+ };
130
+ // Agent identity metadata for in-memory registration
131
+ const AGENT_IDENTITIES = {
132
+ coding: { name: 'Coding Agent', emoji: '🔧' },
133
+ research: { name: 'Research Agent', emoji: '🔍' }
134
+ };
135
+ /**
136
+ * Ensure agent directory and templates exist for a subagent type.
137
+ * Creates the dir + starter IDENTITY.md + TOOLS.md if missing.
138
+ * Registers the agent in config.agents.list (in-memory only).
139
+ */
140
+ export function ensureAgentSetup(type, config) {
141
+ const preset = PRESETS[type];
142
+ const agentDir = getAgentDir(preset.agentId);
143
+ // Create dir + starter templates if missing
144
+ if (!existsSync(agentDir)) {
145
+ mkdirSync(agentDir, { recursive: true });
146
+ const templates = STARTER_TEMPLATES[type];
147
+ writeFileSync(join(agentDir, 'IDENTITY.md'), templates.identity, 'utf-8');
148
+ writeFileSync(join(agentDir, 'TOOLS.md'), templates.tools, 'utf-8');
149
+ console.log(`[subagent] Created agent dir with templates: ${agentDir}`);
150
+ }
151
+ // Register in config.agents.list if not already there (in-memory only)
152
+ if (!config.agents.list[preset.agentId]) {
153
+ const identity = AGENT_IDENTITIES[type];
154
+ config.agents.list[preset.agentId] = {
155
+ identity,
156
+ model: resolveModel(preset.defaultModel || 'anthropic/claude-sonnet-4-5', config),
157
+ thinking: 'medium'
158
+ };
159
+ console.log(`[subagent] Registered agent in config: ${preset.agentId}`);
160
+ }
161
+ }
162
+ // Task tracking
163
+ let taskCounter = 0;
164
+ const tasks = new Map();
165
+ let deliverMessage = null;
166
+ export function initSubagentSystem(deliverFn) {
167
+ deliverMessage = deliverFn;
168
+ console.log('[subagent] System initialized');
169
+ }
170
+ export function getPresetDescriptions() {
171
+ return Object.entries(PRESETS)
172
+ .map(([type, preset]) => ` ${type} — ${preset.description} (default: ${preset.defaultModel || 'current model'})`)
173
+ .join('\n');
174
+ }
175
+ // --- Disk Registry ---
176
+ function ensureRegistryDir() {
177
+ const dir = join(homedir(), '.skimpyclaw', 'logs');
178
+ if (!existsSync(dir)) {
179
+ mkdirSync(dir, { recursive: true });
180
+ }
181
+ }
182
+ function appendRegistryEvent(event) {
183
+ try {
184
+ ensureRegistryDir();
185
+ const line = JSON.stringify({ ...event, timestamp: new Date().toISOString() }) + '\n';
186
+ appendFileSync(REGISTRY_PATH, line, 'utf-8');
187
+ }
188
+ catch (err) {
189
+ console.warn('[subagent] Failed to write registry event:', err instanceof Error ? err.message : err);
190
+ }
191
+ }
192
+ // --- Dispatch ---
193
+ export function dispatchSubagent(type, prompt, chatId, config, modelOverride, history, options) {
194
+ const maxConcurrent = config.subagents?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
195
+ const active = [...tasks.values()].filter((t) => t.status === 'running' || t.status === 'pending');
196
+ if (active.length >= maxConcurrent) {
197
+ throw new Error(`Max concurrent agents reached (${maxConcurrent}). Use /tasks to see running agents or /cancel to stop one.`);
198
+ }
199
+ const preset = PRESETS[type];
200
+ if (!preset) {
201
+ throw new Error(`Unknown agent type: ${type}. Use: ${Object.keys(PRESETS).join(', ')}`);
202
+ }
203
+ taskCounter++;
204
+ const id = `t${taskCounter}`;
205
+ const model = resolveModel(modelOverride || preset.defaultModel || getCurrentModel(), config);
206
+ const maxRetries = options?.maxRetries ?? config.subagents?.maxRetries ?? DEFAULT_MAX_RETRIES;
207
+ const task = {
208
+ id,
209
+ type,
210
+ prompt,
211
+ status: 'pending',
212
+ chatId,
213
+ model,
214
+ label: options?.label,
215
+ createdAt: new Date(),
216
+ retryCount: 0,
217
+ maxRetries,
218
+ abortController: new AbortController()
219
+ };
220
+ tasks.set(id, task);
221
+ // Log to disk registry
222
+ appendRegistryEvent({
223
+ type: 'task_created',
224
+ taskId: id,
225
+ agentType: type,
226
+ model,
227
+ label: options?.label,
228
+ prompt: prompt.slice(0, 500),
229
+ });
230
+ // Build tool config with merged paths
231
+ const toolConfig = {
232
+ ...preset.toolConfig,
233
+ allowedPaths: [
234
+ ...preset.toolConfig.allowedPaths,
235
+ ...(options?.allowedPaths || []),
236
+ ],
237
+ };
238
+ // Fire and forget — don't await
239
+ executeTask(task, config, toolConfig, history).catch((err) => {
240
+ console.error(`[subagent] Unhandled error in task ${id}:`, err);
241
+ });
242
+ return task;
243
+ }
244
+ async function executeTask(task, config, toolConfig, history) {
245
+ task.status = 'running';
246
+ task.startedAt = new Date();
247
+ const label = task.label ? ` "${task.label}"` : '';
248
+ console.log(`[subagent] Starting ${task.id}${label} (${task.type}, model: ${task.model})`);
249
+ try {
250
+ // Check cancellation before starting
251
+ if (task.abortController.signal.aborted) {
252
+ task.status = 'cancelled';
253
+ task.completedAt = new Date();
254
+ appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
255
+ return;
256
+ }
257
+ // Ensure agent dir + templates exist, register in config
258
+ ensureAgentSetup(task.type, config);
259
+ const response = await runAgentTurn(task.type, // agentId = preset.agentId = type name
260
+ task.prompt, config, task.model, toolConfig, history, {
261
+ channel: 'subagent',
262
+ sessionId: task.id,
263
+ metadata: {
264
+ type: task.type,
265
+ chatId: task.chatId,
266
+ label: task.label,
267
+ },
268
+ abortSignal: task.abortController.signal,
269
+ });
270
+ // Check cancellation after completion
271
+ if (task.abortController.signal.aborted) {
272
+ task.status = 'cancelled';
273
+ task.completedAt = new Date();
274
+ releaseAllLocks(task.id);
275
+ appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
276
+ return;
277
+ }
278
+ task.status = 'completed';
279
+ task.result = response;
280
+ task.completedAt = new Date();
281
+ // Release any file locks held by this task
282
+ releaseAllLocks(task.id);
283
+ const elapsed = Math.round((task.completedAt.getTime() - task.startedAt.getTime()) / 1000);
284
+ console.log(`[subagent] Completed ${task.id}${label} in ${elapsed}s`);
285
+ appendRegistryEvent({
286
+ type: 'task_completed',
287
+ taskId: task.id,
288
+ elapsed,
289
+ resultLength: response.length,
290
+ });
291
+ // Deliver result
292
+ if (deliverMessage) {
293
+ const labelStr = task.label ? ` (${task.label})` : '';
294
+ const header = `✅ Agent ${task.id}${labelStr} completed in ${elapsed}s:`;
295
+ await deliverMessage(task.chatId, `${header}\n\n${response}`);
296
+ }
297
+ }
298
+ catch (error) {
299
+ // Release any file locks on error
300
+ releaseAllLocks(task.id);
301
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
302
+ const retryCount = task.retryCount ?? 0;
303
+ const maxRetries = task.maxRetries ?? DEFAULT_MAX_RETRIES;
304
+ // Retry if under limit and not cancelled
305
+ if (retryCount < maxRetries && !task.abortController.signal.aborted) {
306
+ task.retryCount = retryCount + 1;
307
+ const elapsed = Math.round((Date.now() - (task.startedAt?.getTime() || task.createdAt.getTime())) / 1000);
308
+ console.log(`[subagent] Task ${task.id} failed after ${elapsed}s (attempt ${retryCount + 1}/${maxRetries + 1}), retrying: ${errorMsg}`);
309
+ appendRegistryEvent({
310
+ type: 'task_retry',
311
+ taskId: task.id,
312
+ attempt: task.retryCount,
313
+ error: errorMsg,
314
+ });
315
+ if (deliverMessage) {
316
+ await deliverMessage(task.chatId, `⚠️ Agent ${task.id} failed (attempt ${retryCount + 1}/${maxRetries + 1}), retrying...\n\nError: ${errorMsg}`);
317
+ }
318
+ // Modify prompt to include error context for retry (compressed to save tokens)
319
+ const taskSummary = task.prompt.length > 500 ? task.prompt.slice(0, 500) + '...' : task.prompt;
320
+ const retryPrompt = `Retry task (attempt ${(task.retryCount ?? 0) + 1}).\n\nTask: ${taskSummary}\n\nPrevious error: ${errorMsg}\n\nTry a different approach.`;
321
+ task.prompt = retryPrompt;
322
+ task.startedAt = new Date();
323
+ // Retry
324
+ return executeTask(task, config, {
325
+ ...PRESETS[task.type].toolConfig,
326
+ }, history);
327
+ }
328
+ task.status = 'failed';
329
+ task.error = errorMsg;
330
+ task.completedAt = new Date();
331
+ const elapsed = Math.round((task.completedAt.getTime() -
332
+ (task.startedAt?.getTime() || task.createdAt.getTime())) /
333
+ 1000);
334
+ console.error(`[subagent] Failed ${task.id} after ${elapsed}s (all retries exhausted):`, task.error);
335
+ appendRegistryEvent({
336
+ type: 'task_failed',
337
+ taskId: task.id,
338
+ elapsed,
339
+ error: task.error,
340
+ });
341
+ if (deliverMessage) {
342
+ await deliverMessage(task.chatId, `❌ Agent ${task.id} (${task.type}) failed after ${elapsed}s:\n\n${task.error}`);
343
+ }
344
+ }
345
+ }
346
+ export function cancelTask(id) {
347
+ const task = tasks.get(id);
348
+ if (!task)
349
+ return null;
350
+ if (task.status !== 'pending' && task.status !== 'running')
351
+ return task;
352
+ task.abortController.abort();
353
+ task.status = 'cancelled';
354
+ task.completedAt = new Date();
355
+ releaseAllLocks(task.id);
356
+ appendRegistryEvent({ type: 'task_cancelled', taskId: task.id });
357
+ console.log(`[subagent] Cancelled ${task.id}`);
358
+ return task;
359
+ }
360
+ export function getActiveTasks() {
361
+ return [...tasks.values()].filter((t) => t.status === 'pending' || t.status === 'running');
362
+ }
363
+ export function getRecentTasks(n = 10) {
364
+ return [...tasks.values()]
365
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
366
+ .slice(0, n);
367
+ }
368
+ export function getTask(id) {
369
+ return tasks.get(id) || null;
370
+ }
371
+ // For testing
372
+ export function resetForTesting() {
373
+ taskCounter = 0;
374
+ tasks.clear();
375
+ deliverMessage = null;
376
+ }
@@ -0,0 +1,2 @@
1
+ export { initTelegram, startTelegram, stopTelegram, getBot, setSilenceUntil, getSilenceUntil, isSilenced, sendProactiveMessage, sendProactiveVoice, getHistory, addToHistory, clearHistory, getRunContext, getDefaultTelegramToolConfig, buildHelpText, getTelegramDefaultChatId, startTypingIndicator, sendLongMessage, sendLongMessageHtml, escapeHtml, markdownToTelegramHtml, state, BOT_COMMANDS, LAUNCHD_LABEL, commandHandlers, subscribeToApprovalEvents, } from './channels/telegram/index.js';
2
+ export type { MemoryFileInfo, TelegramContext } from './channels/telegram/types.js';
@@ -0,0 +1,11 @@
1
+ // Telegram bot - Re-export from new location for backward compatibility
2
+ // @deprecated Import from './channels/telegram/index.js' instead
3
+ export {
4
+ // Core functions
5
+ initTelegram, startTelegram, stopTelegram, getBot, setSilenceUntil, getSilenceUntil, isSilenced, sendProactiveMessage, sendProactiveVoice,
6
+ // Utilities
7
+ getHistory, addToHistory, clearHistory, getRunContext, getDefaultTelegramToolConfig, buildHelpText, getTelegramDefaultChatId, startTypingIndicator, sendLongMessage, sendLongMessageHtml, escapeHtml, markdownToTelegramHtml,
8
+ // Constants
9
+ state, BOT_COMMANDS, LAUNCHD_LABEL,
10
+ // Handlers
11
+ commandHandlers, subscribeToApprovalEvents, } from './channels/telegram/index.js';
@@ -0,0 +1,3 @@
1
+ import type { ToolConfig } from '../types.js';
2
+ import type { ExecuteToolContext } from './execute-context.js';
3
+ export declare function executeBash(command: string, cwd: string | undefined, config: ToolConfig, context?: ExecuteToolContext): Promise<string>;
@@ -0,0 +1,59 @@
1
+ import { exec } from 'child_process';
2
+ import { isBashCommandSafe } from '../security.js';
3
+ import { classifyCommandRisk, requiresApproval, createApprovalRequest, waitForApproval, } from '../exec-approval.js';
4
+ import { isPathAllowed } from './path-utils.js';
5
+ export async function executeBash(command, cwd, config, context) {
6
+ // Hard block: existing safety filter (always enforced)
7
+ if (!isBashCommandSafe(command)) {
8
+ return Promise.resolve('Error: Command blocked by safety filter.');
9
+ }
10
+ if (cwd && !isPathAllowed(cwd, config.allowedPaths)) {
11
+ return Promise.resolve('Error: Working directory not in allowed paths.');
12
+ }
13
+ // Exec approval gate: classify risk and check if approval is needed
14
+ const approvalConfig = config.execApproval;
15
+ if (approvalConfig?.enabled !== false) {
16
+ const classification = classifyCommandRisk(command);
17
+ if (requiresApproval(classification, approvalConfig)) {
18
+ // Build channel metadata from context for notification routing
19
+ const channelMeta = context?.channel
20
+ ? {
21
+ channel: context.channel,
22
+ chatId: context.channelTargetId ?? context.chatId,
23
+ userId: context.approverUserId,
24
+ username: context.approverUsername,
25
+ }
26
+ : context?.chatId
27
+ ? {
28
+ channel: 'telegram',
29
+ chatId: context.chatId,
30
+ }
31
+ : undefined;
32
+ // Create a pending approval request and wait for resolution
33
+ const ttlMs = approvalConfig?.ttlMs ?? 5 * 60 * 1000;
34
+ const request = createApprovalRequest(command, cwd, classification, approvalConfig, channelMeta);
35
+ const resolved = await waitForApproval(request.id, ttlMs);
36
+ if (resolved.status !== 'approved') {
37
+ return `⛔ Command not executed — approval ${resolved.status} (tier ${classification.tier}: ${classification.reason}).`;
38
+ }
39
+ // Approved — fall through to execution below
40
+ }
41
+ }
42
+ const timeout = config.bashTimeout || 30_000;
43
+ return new Promise((res) => {
44
+ exec(command, {
45
+ cwd: cwd || undefined,
46
+ timeout,
47
+ env: { ...process.env },
48
+ maxBuffer: 5 * 1024 * 1024,
49
+ }, (error, stdout, stderr) => {
50
+ if (error) {
51
+ const parts = [stdout, stderr, `Exit code: ${error.code ?? 'unknown'}`].filter(Boolean);
52
+ res(parts.join('\n').slice(0, 50_000));
53
+ return;
54
+ }
55
+ const output = [stdout, stderr].filter(Boolean).join('\n');
56
+ res(output.slice(0, 50_000) || '(no output)');
57
+ });
58
+ });
59
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolConfig } from '../types.js';
2
+ export declare function executeBrowser(input: Record<string, any>, config: ToolConfig): Promise<string>;
3
+ export declare function cleanupBrowser(): Promise<void>;