skimpyclaw 0.3.14 → 0.4.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 (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/dist/tools.js CHANGED
@@ -4,39 +4,85 @@ import { join, resolve } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { TTLCache } from './cache.js';
6
6
  import { toErrorMessage } from './utils.js';
7
- import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, FETCH_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, } from './tools/definitions.js';
7
+ import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, FETCH_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CHECK_CODE_AGENT_TOOL, DELEGATE_TO_AGENT_TOOL, } from './tools/definitions.js';
8
8
  import { executeReadFile, executeWriteFileLocked, executeListDirectory } from './tools/file-tools.js';
9
9
  import { executeBash } from './tools/bash-tool.js';
10
- import { executeBrowser, cleanupBrowser } from './tools/browser-tool.js';
11
10
  import { executeFetch } from './tools/fetch-tool.js';
12
- import { ensureContainer, SANDBOX_DEFAULTS, translatePath, validateMountPaths } from './sandbox/index.js';
13
- import { sandboxBash, sandboxReadFile, sandboxWriteFile, sandboxListDir, sandboxGlob } from './sandbox/index.js';
14
- import { isBashCommandSafe } from './security.js';
15
- import { classifyCommandRisk, requiresApproval } from './exec-approval.js';
16
11
  // Re-export from code-agents module for backward compatibility
17
12
  export {
18
13
  // Registry functions
19
14
  getActiveCodeAgents, getRecentCodeAgents, getAllCodeAgents, getCodeAgent, cancelCodeAgent, restoreCodeAgentTasks,
20
15
  // Config
21
16
  setCodeAgentConfig,
22
- // Orchestrator functions
23
- computeWaves, decomposeTask, synthesizeResults,
24
17
  // Executor functions
25
18
  runValidation, runCodeAgentBackground,
26
19
  // Utilities
27
- buildCodeAgentArgs, readTeamState,
20
+ buildCodeAgentArgs,
28
21
  // Parsers
29
22
  parseStreamJsonForLive, } from './code-agents/index.js';
30
23
  // Re-export from definitions
31
- export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, FETCH_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, cleanupBrowser, };
24
+ export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, FETCH_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CHECK_CODE_AGENT_TOOL, DELEGATE_TO_AGENT_TOOL, };
32
25
  // --- MCP (mcporter) ---
33
26
  let mcpRuntime = null;
27
+ let mcpHealthInterval = null;
28
+ /** Last time MCP tools were successfully discovered (epoch ms) */
29
+ let mcpLastDiscoveredAt = 0;
30
+ /** How often to re-validate MCP tools (5 minutes) */
31
+ const MCP_REDISCOVERY_INTERVAL_MS = 5 * 60 * 1000;
32
+ /**
33
+ * MCP health check — periodically re-discovers tools to detect daemon restarts.
34
+ * Runs every 5 minutes. If the runtime is stale (daemon died), clears caches
35
+ * so the next getToolDefinitions() call triggers fresh discovery.
36
+ */
37
+ function startMcpHealthCheck() {
38
+ if (mcpHealthInterval)
39
+ return;
40
+ mcpHealthInterval = setInterval(async () => {
41
+ try {
42
+ const runtime = await getMcpRuntime();
43
+ const servers = runtime.listServers();
44
+ if (servers.length === 0 && discoveredMcpTools && discoveredMcpTools.length > 0) {
45
+ // Runtime lost its servers — daemon likely died
46
+ console.warn('[mcp] Health check: runtime has no servers, triggering reconnect');
47
+ await reconnectMcp();
48
+ return;
49
+ }
50
+ // Verify we can actually list tools from at least one server
51
+ let healthy = false;
52
+ for (const server of servers) {
53
+ try {
54
+ await runtime.listTools(server, { includeSchema: false });
55
+ healthy = true;
56
+ break;
57
+ }
58
+ catch {
59
+ // This server is unhealthy
60
+ }
61
+ }
62
+ if (!healthy && servers.length > 0) {
63
+ console.warn('[mcp] Health check: all servers unhealthy, triggering reconnect');
64
+ await reconnectMcp();
65
+ }
66
+ }
67
+ catch {
68
+ // Runtime creation failed — trigger reconnect on next use
69
+ if (mcpRuntime) {
70
+ await mcpRuntime.close().catch(() => { });
71
+ mcpRuntime = null;
72
+ }
73
+ discoveredMcpTools = null;
74
+ mcpToolNameMap.clear();
75
+ toolDefsCache.clear();
76
+ }
77
+ }, MCP_REDISCOVERY_INTERVAL_MS);
78
+ }
34
79
  async function getMcpRuntime() {
35
80
  if (!mcpRuntime) {
36
81
  const { createRuntime } = await import('mcporter');
37
82
  mcpRuntime = await createRuntime({
38
83
  configPath: join(homedir(), '.mcporter', 'mcporter.json'),
39
84
  });
85
+ startMcpHealthCheck();
40
86
  }
41
87
  return mcpRuntime;
42
88
  }
@@ -75,16 +121,20 @@ function getMcpFallbackTools(server) {
75
121
  return mcpFallbackCache[server] || [];
76
122
  }
77
123
  export async function discoverMcpTools() {
78
- if (discoveredMcpTools !== null)
124
+ // Re-discover if cache is older than the rediscovery interval
125
+ const stale = mcpLastDiscoveredAt > 0 && (Date.now() - mcpLastDiscoveredAt) > MCP_REDISCOVERY_INTERVAL_MS;
126
+ if (discoveredMcpTools !== null && !stale)
79
127
  return discoveredMcpTools;
80
128
  const tools = [];
81
129
  mcpToolNameMap.clear();
82
130
  try {
83
131
  const runtime = await getMcpRuntime();
84
132
  const servers = runtime.listServers();
133
+ console.log(`[mcp] Servers found: ${servers.join(', ')}`);
85
134
  for (const server of servers) {
86
135
  try {
87
136
  const serverTools = await runtime.listTools(server, { includeSchema: true });
137
+ console.log(`[mcp] Server "${server}" returned ${serverTools.length} tools: ${serverTools.map((t) => t.name).join(', ')}`);
88
138
  const sanitizedServer = sanitizeToolName(server);
89
139
  for (const tool of serverTools) {
90
140
  const sanitizedTool = sanitizeToolName(tool.name);
@@ -119,10 +169,13 @@ export async function discoverMcpTools() {
119
169
  console.warn('[mcp] Failed to create runtime for tool discovery:', err instanceof Error ? err.message : err);
120
170
  }
121
171
  discoveredMcpTools = tools;
172
+ mcpLastDiscoveredAt = Date.now();
173
+ console.log(`[mcp] Discovered ${tools.length} tools: ${tools.map((t) => t.name).join(', ')}`);
122
174
  return tools;
123
175
  }
124
176
  export function clearMcpToolCache() {
125
177
  discoveredMcpTools = null;
178
+ mcpLastDiscoveredAt = 0;
126
179
  }
127
180
  const toolDefsCache = new TTLCache(60_000);
128
181
  /** Force-reconnect the MCP runtime (clears cached runtime and tool discovery). */
@@ -167,16 +220,15 @@ function injectProjects(tool, projects) {
167
220
  };
168
221
  }
169
222
  /**
170
- * Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) + agent tools.
223
+ * Get all available tool definitions: built-ins + fetch + MCP (auto-discovered) + agent tools.
171
224
  * This is the primary way to get tools — replaces the static TOOL_DEFINITIONS export.
172
- * Pass includeAgentTools: true to include code_with_agent, code_with_team, check_code_agent.
225
+ * Pass includeAgentTools: true to include code_with_agent and check_code_agent.
173
226
  * Results are cached for 60s to avoid rebuilding the array on every agent turn.
174
227
  */
175
228
  export async function getToolDefinitions(config, options) {
176
229
  const includeMcp = options?.includeMcp !== false; // default true for backwards compat
177
230
  const profile = config?.toolProfile ?? 'full';
178
231
  const cacheKey = JSON.stringify({
179
- browser: config?.browser?.enabled,
180
232
  agentTools: options?.includeAgentTools,
181
233
  mcp: includeMcp,
182
234
  projects: options?.projects,
@@ -189,37 +241,33 @@ export async function getToolDefinitions(config, options) {
189
241
  // Fetch is always available (lightweight HTTP, no dependencies)
190
242
  tools.push(FETCH_TOOL_DEFINITION);
191
243
  // Minimal profile: built-in tools + fetch.
192
- // Used by orchestrator decompose/synthesize calls.
193
244
  if (profile === 'minimal') {
194
245
  toolDefsCache.set(cacheKey, tools);
195
246
  return tools;
196
247
  }
197
- // Include browser tool only when explicitly enabled
198
- if (config?.browser?.enabled) {
199
- tools.push(BROWSER_TOOL_DEFINITION);
200
- }
201
- // Coding profile: built-ins + browser + code_with_agent + check_code_agent.
202
- // Skips MCP discovery and code_with_team.
248
+ // Coding profile: built-ins + fetch + code_with_agent + check_code_agent.
249
+ // Skips MCP discovery.
203
250
  if (profile === 'coding') {
204
251
  if (options?.includeAgentTools) {
205
252
  tools.push(injectProjects(CODE_WITH_AGENT_TOOL, options.projects));
206
253
  tools.push(CHECK_CODE_AGENT_TOOL);
254
+ tools.push(DELEGATE_TO_AGENT_TOOL);
207
255
  }
208
256
  toolDefsCache.set(cacheKey, tools);
209
257
  return tools;
210
258
  }
211
- // Full profile (default): everything including MCP and code_with_team.
259
+ // Full profile (default): everything including MCP.
212
260
  // Auto-discover MCP tools from mcporter config (only for Anthropic models)
213
261
  if (includeMcp) {
214
262
  const mcpTools = await discoverMcpTools();
215
263
  tools.push(...mcpTools);
216
264
  }
217
- // Include code_with_agent, code_with_team, and check_code_agent when requested
265
+ // Include code_with_agent and check_code_agent when requested
218
266
  if (options?.includeAgentTools) {
219
267
  const projects = options.projects;
220
268
  tools.push(injectProjects(CODE_WITH_AGENT_TOOL, projects));
221
- tools.push(injectProjects(CODE_WITH_TEAM_TOOL, projects));
222
269
  tools.push(CHECK_CODE_AGENT_TOOL);
270
+ tools.push(DELEGATE_TO_AGENT_TOOL);
223
271
  }
224
272
  toolDefsCache.set(cacheKey, tools);
225
273
  return tools;
@@ -237,6 +285,32 @@ async function callMcpTool(server, tool, args) {
237
285
  }
238
286
  return JSON.stringify(result);
239
287
  }
288
+ function isPlainObject(value) {
289
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
290
+ }
291
+ export function normalizeMcpToolArgsForExecution(server, tool, args) {
292
+ if (server !== 'context-a8c' || tool !== 'context-a8c-execute-tool' || isPlainObject(args.params)) {
293
+ return args;
294
+ }
295
+ const aliasedParams = ['parameters', 'input', 'arguments']
296
+ .map(key => args[key])
297
+ .find(isPlainObject);
298
+ if (aliasedParams) {
299
+ const { parameters: _parameters, input: _input, arguments: _arguments, ...rest } = args;
300
+ return { ...rest, params: aliasedParams };
301
+ }
302
+ if (typeof args.provider === 'string' && typeof args.tool === 'string') {
303
+ const params = Object.fromEntries(Object.entries(args).filter(([key]) => !['provider', 'tool', 'params'].includes(key)));
304
+ if (Object.keys(params).length > 0) {
305
+ return {
306
+ provider: args.provider,
307
+ tool: args.tool,
308
+ params,
309
+ };
310
+ }
311
+ }
312
+ return args;
313
+ }
240
314
  async function executeMcpToolGeneric(fullName, args) {
241
315
  const mapping = mcpToolNameMap.get(fullName);
242
316
  let server;
@@ -252,21 +326,38 @@ async function executeMcpToolGeneric(fullName, args) {
252
326
  server = parts[1];
253
327
  toolName = parts.slice(2).join('__');
254
328
  }
255
- try {
256
- return await callMcpTool(server, toolName, args);
257
- }
258
- catch (err) {
259
- const msg = err instanceof Error ? err.message : String(err);
260
- // Retry once on connection/session errors
261
- if (msg.includes('session') || msg.includes('Session') || msg.includes('ECONNR') || msg.includes('EPIPE') || msg.includes('closed') || msg.includes('disconnected')) {
262
- console.warn(`[mcp] Tool call failed (${msg}), reconnecting and retrying...`);
263
- await reconnectMcp();
264
- return await callMcpTool(server, toolName, args);
329
+ const normalizedArgs = normalizeMcpToolArgsForExecution(server, toolName, args);
330
+ const isRetryableError = (msg) => msg.includes('session') || msg.includes('Session') || msg.includes('ECONNR') ||
331
+ msg.includes('EPIPE') || msg.includes('closed') || msg.includes('close') ||
332
+ msg.includes('disconnected') || msg.includes('fetch failed') ||
333
+ msg.includes('timed out') || msg.includes('Premature') ||
334
+ msg.includes('-32603') || msg.includes('-32001') || msg.includes('-32000');
335
+ const MAX_RETRIES = 2;
336
+ let lastErr;
337
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
338
+ try {
339
+ return await callMcpTool(server, toolName, normalizedArgs);
340
+ }
341
+ catch (err) {
342
+ lastErr = err;
343
+ const msg = err instanceof Error ? err.message : String(err);
344
+ if (attempt < MAX_RETRIES && isRetryableError(msg)) {
345
+ const delay = (attempt + 1) * 2000; // 2s, 4s
346
+ console.warn(`[mcp] Tool call failed (${msg}), reconnecting (attempt ${attempt + 1}/${MAX_RETRIES})...`);
347
+ await new Promise(r => setTimeout(r, delay));
348
+ await reconnectMcp();
349
+ continue;
350
+ }
351
+ throw err;
265
352
  }
266
- throw err;
267
353
  }
354
+ throw lastErr;
268
355
  }
269
356
  export async function cleanupMcp() {
357
+ if (mcpHealthInterval) {
358
+ clearInterval(mcpHealthInterval);
359
+ mcpHealthInterval = null;
360
+ }
270
361
  if (mcpRuntime) {
271
362
  await mcpRuntime.close().catch(() => { });
272
363
  mcpRuntime = null;
@@ -298,127 +389,20 @@ export async function executeTool(name, input, config, context) {
298
389
  const { executeCodeWithAgent } = await import('./code-agents/index.js');
299
390
  return await executeCodeWithAgent(input, config, context);
300
391
  }
301
- // Route code_with_team - delegate to code-agents module
302
- if (name === 'code_with_team') {
303
- const { executeCodeWithTeam } = await import('./code-agents/index.js');
304
- return await executeCodeWithTeam(input, config, context);
305
- }
306
392
  // Route check_code_agent - delegate to code-agents module
307
393
  if (name === 'check_code_agent') {
308
394
  const { executeCheckCodeAgent } = await import('./code-agents/index.js');
309
395
  return executeCheckCodeAgent(input);
310
396
  }
397
+ if (name === 'delegate_to_agent') {
398
+ const { executeDelegateToAgent } = await import('./tools/agent-delegation.js');
399
+ if (!context?.fullConfig)
400
+ return 'Error: delegate_to_agent requires runtime config.';
401
+ return executeDelegateToAgent(input, context.fullConfig, context);
402
+ }
311
403
  // Map Claude Code names to internal names for built-in tools
312
404
  const normalized = fromClaudeCodeName(name).toLowerCase().replace(/-/g, '_');
313
- // --- Sandbox routing ---
314
- const sandboxCfg = context?.sandboxConfig;
315
- if (sandboxCfg?.enabled) {
316
- const SANDBOXED_TOOLS = new Set(['bash', 'read_file', 'write_file', 'list_directory', 'glob']);
317
- // macOS-only commands that must run on the host (not available in Linux containers)
318
- const MACOS_HOST_COMMANDS = new Set([
319
- 'osascript', 'open', 'say', 'pbcopy', 'pbpaste', 'defaults',
320
- 'icalBuddy', 'shortcuts', 'caffeinate', 'networksetup', 'launchctl',
321
- 'security', 'xattr', 'ditto', 'hdiutil', 'diskutil', 'sw_vers',
322
- ]);
323
- const needsHost = normalized === 'bash' && input.command &&
324
- MACOS_HOST_COMMANDS.has(input.command.trim().split(/[\s;|&]/)[0]);
325
- if (SANDBOXED_TOOLS.has(normalized) && !needsHost) {
326
- const sessionId = context?.sessionId || context?.chatId?.toString() || 'default';
327
- const merged = { ...SANDBOX_DEFAULTS, ...sandboxCfg };
328
- const containerName = await ensureContainer(sessionId, merged, config.allowedPaths);
329
- const mounts = validateMountPaths(config.allowedPaths);
330
- const tp = (p) => translatePath(p, mounts);
331
- // Translate host paths in bash commands so they resolve inside the container
332
- const translateBashPaths = (cmd) => {
333
- let translated = cmd;
334
- // Sort mounts by host path length descending to match most specific first
335
- const sorted = [...mounts].sort((a, b) => b.host.length - a.host.length);
336
- for (const mount of sorted) {
337
- translated = translated.replaceAll(mount.host, mount.container);
338
- }
339
- // Also translate ~ and $HOME references to /workspace/config
340
- const home = homedir();
341
- const homeMounts = sorted.filter(m => m.host.startsWith(home));
342
- for (const mount of homeMounts) {
343
- const tildeForm = '~' + mount.host.slice(home.length);
344
- translated = translated.replaceAll(tildeForm, mount.container);
345
- const envForm = '$HOME' + mount.host.slice(home.length);
346
- translated = translated.replaceAll(envForm, mount.container);
347
- }
348
- return translated;
349
- };
350
- // Reverse-translate container paths back to host paths in file content.
351
- // Prevents the agent from writing /workspace/... paths into config files.
352
- const reverseTranslatePaths = (content) => {
353
- let reversed = content;
354
- const sorted = [...mounts].sort((a, b) => b.container.length - a.container.length);
355
- for (const mount of sorted) {
356
- reversed = reversed.replaceAll(mount.container, mount.host);
357
- }
358
- return reversed;
359
- };
360
- switch (normalized) {
361
- case 'bash': {
362
- const translatedCmd = translateBashPaths(input.command);
363
- // Apply hard safety blocks inside sandbox.
364
- // Sandbox provides filesystem isolation, so opaque script execution
365
- // (heredocs, -c, -e) is safe — only require approval for truly
366
- // destructive patterns (rm -rf, dd, mkfs, etc.).
367
- if (!isBashCommandSafe(translatedCmd)) {
368
- return 'Error: Command blocked by safety filter.';
369
- }
370
- const classification = classifyCommandRisk(translatedCmd);
371
- // Downgrade opaque-script classifications in sandbox — the isolation
372
- // already handles the risk that inline code poses on the host.
373
- if (classification.tier >= 2 && (classification.reason === 'Inline heredoc script execution' ||
374
- classification.reason === 'Inline interpreter code execution')) {
375
- classification.tier = 0;
376
- classification.reason = `${classification.reason} (sandboxed — auto-approved)`;
377
- }
378
- const approvalConfig = config.execApproval;
379
- if (requiresApproval(classification, approvalConfig)) {
380
- const isUnattended = context?.channel === 'subagent' ||
381
- context?.isCronJob === true ||
382
- (!context?.approverUserId && !context?.channelTargetId && !context?.chatId);
383
- if (isUnattended) {
384
- return `⛔ Command blocked — tier ${classification.tier} commands require approval but no approver is available in this context (${classification.reason}). Use safer alternatives or request approval via an interactive channel.`;
385
- }
386
- // Attended context — run full approval flow before executing in sandbox
387
- const { createApprovalRequest: sbxCreate, waitForApproval: sbxWait } = await import('./exec-approval.js');
388
- const ttlMs = approvalConfig?.ttlMs ?? 5 * 60 * 1000;
389
- const channelMeta = context?.channel ? {
390
- channel: context.channel,
391
- chatId: context.channelTargetId ?? context.chatId,
392
- userId: context.approverUserId,
393
- username: context.approverUsername,
394
- } : context?.chatId ? { channel: 'telegram', chatId: context.chatId } : undefined;
395
- const sbxReq = sbxCreate(translatedCmd, input.cwd, classification, approvalConfig, channelMeta);
396
- const sbxResolved = await sbxWait(sbxReq.id, ttlMs);
397
- if (sbxResolved.status !== 'approved') {
398
- return `⛔ Command not executed — approval ${sbxResolved.status} (tier ${classification.tier}: ${classification.reason}).`;
399
- }
400
- }
401
- return await sandboxBash(containerName, translatedCmd, input.cwd ? tp(input.cwd) : undefined, config.bashTimeout);
402
- }
403
- case 'read_file':
404
- return await sandboxReadFile(containerName, tp(input.file_path || input.path));
405
- case 'write_file':
406
- return await sandboxWriteFile(containerName, tp(input.file_path || input.path), reverseTranslatePaths(input.content));
407
- case 'list_directory':
408
- return await sandboxListDir(containerName, tp(input.path));
409
- case 'glob':
410
- return await sandboxGlob(containerName, tp(input.base || input.path || '/workspace'), input.pattern || '*');
411
- default:
412
- break; // fall through
413
- }
414
- }
415
- }
416
405
  switch (normalized) {
417
- case '$web_search':
418
- case 'web_search':
419
- case 'websearch':
420
- // Legacy: redirect to Fetch with DuckDuckGo HTML search
421
- return await executeFetch({ url: `https://duckduckgo.com/html/?q=${encodeURIComponent(input.query || input.q || input.text || '')}` }, config);
422
406
  case 'read_file':
423
407
  return executeReadFile(input.file_path || input.path, config);
424
408
  case 'write_file':
@@ -426,9 +410,7 @@ export async function executeTool(name, input, config, context) {
426
410
  case 'list_directory':
427
411
  return executeListDirectory(input.path, config);
428
412
  case 'bash':
429
- return await executeBash(input.command, input.cwd, config, context);
430
- case 'browser':
431
- return await executeBrowser(input, config);
413
+ return await executeBash(input.command || input.cmd, input.cwd, config, context);
432
414
  case 'fetch':
433
415
  return await executeFetch(input, config);
434
416
  default:
package/dist/types.d.ts CHANGED
@@ -67,9 +67,13 @@ export interface Config {
67
67
  maxConcurrent?: number;
68
68
  defaultAgent?: string;
69
69
  timeoutMinutes?: number;
70
- teamTimeoutMinutes?: number;
71
70
  maxTurns?: number;
72
- skipPlaywright?: boolean;
71
+ worktrees?: {
72
+ enabled?: boolean;
73
+ mode?: 'off' | 'auto' | 'always';
74
+ root?: string;
75
+ cleanup?: boolean;
76
+ };
73
77
  /** Per-project validation commands. Keys match project names from `projects` config.
74
78
  * Values are shell commands run in the project dir. Overrides auto-detected build+test. */
75
79
  validationCommands?: Record<string, string>;
@@ -88,7 +92,6 @@ export interface Config {
88
92
  /** Named project paths. Keys are short names (e.g. "skimpyclaw"), values are absolute paths.
89
93
  * Project paths are automatically added to tool allowedPaths and available to code_with_agent by name. */
90
94
  projects?: Record<string, string>;
91
- sandbox?: SandboxConfig;
92
95
  }
93
96
  export interface AgentConfig {
94
97
  identity: {
@@ -96,8 +99,9 @@ export interface AgentConfig {
96
99
  emoji: string;
97
100
  };
98
101
  model: string;
99
- thinking?: 'none' | 'low' | 'medium' | 'high';
102
+ thinking?: ThinkingLevel;
100
103
  }
104
+ export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'xhigh';
101
105
  export type AllowlistEntry = string | number;
102
106
  export type ChannelId = 'telegram' | 'discord';
103
107
  export interface TelegramChannelConfig {
@@ -115,6 +119,8 @@ export interface DiscordChannelConfig {
115
119
  tools?: ToolConfig;
116
120
  defaultAllowedPaths?: string[];
117
121
  defaultChannelId?: string;
122
+ /** Route coding agent status updates to a thread on the triggering message (default: true) */
123
+ threadedReplies?: boolean;
118
124
  }
119
125
  export interface ChannelsConfig {
120
126
  active?: ChannelId;
@@ -143,16 +149,8 @@ export interface CronPayload {
143
149
  timeoutMs?: number;
144
150
  tools?: ToolConfig;
145
151
  sendAsVoice?: boolean;
146
- }
147
- export interface SandboxConfig {
148
- enabled: boolean;
149
- runtime?: 'container' | 'docker';
150
- image?: string;
151
- cpus?: number;
152
- memory?: string;
153
- network?: string;
154
- idleTimeoutMs?: number;
155
- env?: Record<string, string>;
152
+ /** Discord thread ID for routing notifications. Invalid/unavailable thread targets fall back to default channel delivery. */
153
+ discordThreadId?: string;
156
154
  }
157
155
  export interface ToolConfig {
158
156
  enabled: boolean;
@@ -166,20 +164,6 @@ export interface ToolConfig {
166
164
  maxContextTokens?: number;
167
165
  compactionModel?: string;
168
166
  };
169
- browser?: {
170
- enabled?: boolean;
171
- type?: 'chromium' | 'firefox' | 'webkit';
172
- headless?: boolean;
173
- allowFile?: boolean;
174
- slowMoMs?: number;
175
- userAgent?: string;
176
- viewport?: {
177
- width: number;
178
- height: number;
179
- };
180
- profileDir?: string;
181
- executablePath?: string;
182
- };
183
167
  execApproval?: {
184
168
  enabled?: boolean;
185
169
  ttlMs?: number;
@@ -199,11 +183,22 @@ export interface Session {
199
183
  createdAt: Date;
200
184
  updatedAt: Date;
201
185
  }
186
+ export type InteractiveSessionStatus = 'active' | 'errored' | 'archived';
187
+ export interface InteractiveSession {
188
+ discordThreadId: string;
189
+ cliSessionId: string;
190
+ cliAgent: 'claude' | 'codex';
191
+ status: InteractiveSessionStatus;
192
+ createdAt: string;
193
+ lastActivityAt: string;
194
+ initialTask: string;
195
+ }
202
196
  export interface GatewayStatus {
203
197
  status: 'ok' | 'error';
204
198
  uptime: number;
205
199
  agent: string;
206
200
  model: string;
201
+ thinking?: ThinkingLevel;
207
202
  lastMessage?: Date;
208
203
  activeChannel?: ChannelId | null;
209
204
  cronJobs: {
@@ -233,11 +228,23 @@ export interface ChatMessage {
233
228
  role: 'system' | 'user' | 'assistant';
234
229
  content: string | ContentBlock[];
235
230
  }
231
+ export interface FeedbackSignal {
232
+ type: 'correction' | 'acceptance';
233
+ reward: number;
234
+ confidence: number;
235
+ reason: string;
236
+ dimensions: Partial<Record<string, number>>;
237
+ }
236
238
  export interface ChatOptions {
237
239
  model: string;
238
240
  maxTokens?: number;
239
241
  temperature?: number;
240
- thinking?: 'none' | 'low' | 'medium' | 'high';
242
+ thinking?: ThinkingLevel;
243
+ }
244
+ export interface AbortSignalLike {
245
+ readonly aborted: boolean;
246
+ addEventListener?: (...args: any[]) => void;
247
+ removeEventListener?: (...args: any[]) => void;
241
248
  }
242
249
  export interface AgentRunContext {
243
250
  userId?: string;
@@ -245,7 +252,7 @@ export interface AgentRunContext {
245
252
  channel?: string;
246
253
  tags?: string[];
247
254
  metadata?: Record<string, unknown>;
248
- abortSignal?: AbortSignal;
255
+ abortSignal?: AbortSignalLike;
249
256
  /** Audit trigger label (e.g. "telegram", "cron", "discord", "system") */
250
257
  trigger?: AuditTrace['trigger'];
251
258
  }
package/dist/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Shared utilities used across modules
2
2
  import { readFileSync, readdirSync, existsSync } from 'fs';
3
3
  import { join } from 'path';
4
- import { timingSafeEqual } from 'crypto';
4
+ import { createHash, timingSafeEqual } from 'crypto';
5
5
  /**
6
6
  * Format a Date as YYYY-MM-DD string.
7
7
  */
@@ -63,9 +63,7 @@ export function validateBearerToken(expected, authHeader) {
63
63
  if (!authHeader || !authHeader.startsWith('Bearer '))
64
64
  return false;
65
65
  const provided = authHeader.slice(7);
66
- const expectedBuf = Buffer.from(expected, 'utf8');
67
- const providedBuf = Buffer.from(provided, 'utf8');
68
- if (expectedBuf.length !== providedBuf.length)
69
- return false;
70
- return timingSafeEqual(expectedBuf, providedBuf);
66
+ const expectedDigest = createHash('sha256').update(expected, 'utf8').digest();
67
+ const providedDigest = createHash('sha256').update(provided, 'utf8').digest();
68
+ return timingSafeEqual(expectedDigest, providedDigest);
71
69
  }
package/dist/voice.d.ts CHANGED
@@ -10,7 +10,7 @@ export interface TranscriptionResult {
10
10
  */
11
11
  export declare function transcribeAudio(audioPath: string, config: VoiceConfig): Promise<TranscriptionResult>;
12
12
  export interface SpeechResult {
13
- buffer: Buffer;
13
+ buffer: Uint8Array;
14
14
  format: 'ogg' | 'mp3';
15
15
  provider: string;
16
16
  }
package/dist/voice.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Voice transcription — local Whisper CLI (free) with API fallback
2
2
  import { existsSync, readFileSync, unlinkSync, readdirSync } from 'fs';
3
- import { execSync } from 'child_process';
3
+ import { execSync, spawnSync } from 'child_process';
4
4
  import { basename, dirname, join } from 'path';
5
5
  import { tmpdir } from 'os';
6
6
  import { toErrorMessage } from './utils.js';
@@ -341,9 +341,22 @@ function synthesizeWithMacOS(text, voice = 'Zoe') {
341
341
  const aiffPath = join(tmpdir(), `skimpyclaw-tts-${id}.aiff`);
342
342
  const oggPath = join(tmpdir(), `skimpyclaw-tts-${id}.ogg`);
343
343
  try {
344
- const safeText = text.replace(/'/g, "'\\''");
345
- execSync(`say -v '${voice}' -o '${aiffPath}' '${safeText}'`, { stdio: ['ignore', 'pipe', 'pipe'] });
346
- execSync(`ffmpeg -i '${aiffPath}' -c:a libopus '${oggPath}' -y`, { stdio: ['ignore', 'pipe', 'pipe'] });
344
+ const sayResult = spawnSync('say', ['-v', voice, '-o', aiffPath, text], {
345
+ encoding: 'utf-8',
346
+ stdio: ['ignore', 'pipe', 'pipe'],
347
+ });
348
+ if (sayResult.status !== 0) {
349
+ const msg = (sayResult.stderr || sayResult.stdout || '').trim() || 'unknown error';
350
+ throw new Error(`macOS say failed: ${msg}`);
351
+ }
352
+ const ffmpegResult = spawnSync('ffmpeg', ['-i', aiffPath, '-c:a', 'libopus', oggPath, '-y'], {
353
+ encoding: 'utf-8',
354
+ stdio: ['ignore', 'pipe', 'pipe'],
355
+ });
356
+ if (ffmpegResult.status !== 0) {
357
+ const msg = (ffmpegResult.stderr || ffmpegResult.stdout || '').trim() || 'unknown error';
358
+ throw new Error(`ffmpeg conversion failed: ${msg}`);
359
+ }
347
360
  const buffer = readFileSync(oggPath);
348
361
  return { buffer, format: 'ogg', provider: `macos (${voice})` };
349
362
  }