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,589 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { executeTool, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, getToolDefinitions, fromClaudeCodeName, toClaudeCodeName, buildCodeAgentArgs, getActiveCodeAgents, getRecentCodeAgents, getCodeAgent, readTeamState, decomposeTask, synthesizeResults, computeWaves } from '../tools.js';
5
+ import { resolveModelAlias } from '../code-agents/utils.js';
6
+ const TEST_DIR = join(process.cwd(), '__test_sandbox__');
7
+ const OUTSIDE_DIR = join(process.cwd(), '__test_outside__');
8
+ const toolConfig = {
9
+ enabled: true,
10
+ allowedPaths: [TEST_DIR],
11
+ maxIterations: 5,
12
+ bashTimeout: 5000,
13
+ };
14
+ beforeEach(() => {
15
+ mkdirSync(TEST_DIR, { recursive: true });
16
+ mkdirSync(OUTSIDE_DIR, { recursive: true });
17
+ writeFileSync(join(TEST_DIR, 'hello.txt'), 'hello world');
18
+ writeFileSync(join(OUTSIDE_DIR, 'secret.txt'), 'top secret');
19
+ });
20
+ afterEach(() => {
21
+ rmSync(TEST_DIR, { recursive: true, force: true });
22
+ rmSync(OUTSIDE_DIR, { recursive: true, force: true });
23
+ });
24
+ describe('BUILTIN_TOOL_DEFINITIONS', () => {
25
+ it('exports 4 built-in tools', () => {
26
+ expect(BUILTIN_TOOL_DEFINITIONS).toHaveLength(4);
27
+ const names = BUILTIN_TOOL_DEFINITIONS.map(t => t.name);
28
+ expect(names).toContain('Read');
29
+ expect(names).toContain('Write');
30
+ expect(names).toContain('Glob');
31
+ expect(names).toContain('Bash');
32
+ });
33
+ it('exports Browser tool definition separately', () => {
34
+ expect(BROWSER_TOOL_DEFINITION.name).toBe('Browser');
35
+ expect(BROWSER_TOOL_DEFINITION.input_schema).toBeDefined();
36
+ });
37
+ });
38
+ describe('getToolDefinitions', () => {
39
+ it('returns at least the 4 built-in tools', async () => {
40
+ const tools = await getToolDefinitions();
41
+ expect(tools.length).toBeGreaterThanOrEqual(4);
42
+ const names = tools.map(t => t.name);
43
+ expect(names).toContain('Read');
44
+ expect(names).toContain('Write');
45
+ expect(names).toContain('Glob');
46
+ expect(names).toContain('Bash');
47
+ }, 15000);
48
+ it('includes Browser when browser.enabled is true', async () => {
49
+ const config = { ...toolConfig, browser: { enabled: true } };
50
+ const tools = await getToolDefinitions(config);
51
+ expect(tools.map(t => t.name)).toContain('Browser');
52
+ });
53
+ it('excludes Browser when browser.enabled is false', async () => {
54
+ const config = { ...toolConfig, browser: { enabled: false } };
55
+ const tools = await getToolDefinitions(config);
56
+ expect(tools.map(t => t.name)).not.toContain('Browser');
57
+ });
58
+ it('excludes Browser when no config provided', async () => {
59
+ const tools = await getToolDefinitions();
60
+ expect(tools.map(t => t.name)).not.toContain('Browser');
61
+ });
62
+ });
63
+ describe('tool name mapping', () => {
64
+ it('maps Claude Code names to internal names', () => {
65
+ expect(fromClaudeCodeName('Read')).toBe('read_file');
66
+ expect(fromClaudeCodeName('Write')).toBe('write_file');
67
+ expect(fromClaudeCodeName('Glob')).toBe('list_directory');
68
+ expect(fromClaudeCodeName('Bash')).toBe('bash');
69
+ });
70
+ it('maps internal names to Claude Code names', () => {
71
+ expect(toClaudeCodeName('read_file')).toBe('Read');
72
+ expect(toClaudeCodeName('write_file')).toBe('Write');
73
+ expect(toClaudeCodeName('list_directory')).toBe('Glob');
74
+ expect(toClaudeCodeName('bash')).toBe('Bash');
75
+ });
76
+ it('passes through unknown names unchanged', () => {
77
+ expect(fromClaudeCodeName('unknown')).toBe('unknown');
78
+ expect(toClaudeCodeName('unknown')).toBe('unknown');
79
+ });
80
+ });
81
+ describe('MCP tool name parsing', () => {
82
+ it('routes mcp__server__tool names to MCP executor', async () => {
83
+ // This will fail because the MCP server doesn't exist, but the routing should work
84
+ const result = await executeTool('mcp__fake_server__fake_tool', { arg: 'test' }, toolConfig);
85
+ // Should get an MCP error, NOT "Unknown tool"
86
+ expect(result).toContain('Error:');
87
+ expect(result).not.toContain('Unknown tool');
88
+ });
89
+ it('routes mcp__ names with dashes correctly', async () => {
90
+ const result = await executeTool('mcp__my-server__my-tool', { arg: 'test' }, toolConfig);
91
+ expect(result).toContain('Error:');
92
+ expect(result).not.toContain('Unknown tool');
93
+ });
94
+ it('routes mcp__ names with multiple segments correctly', async () => {
95
+ const result = await executeTool('mcp__server__category__subtool', { arg: 'test' }, toolConfig);
96
+ expect(result).toContain('Error:');
97
+ expect(result).not.toContain('Unknown tool');
98
+ });
99
+ });
100
+ describe('read_file', () => {
101
+ it('reads a file within allowed paths', async () => {
102
+ const result = await executeTool('Read', { path: join(TEST_DIR, 'hello.txt') }, toolConfig);
103
+ expect(result).toBe('hello world');
104
+ });
105
+ it('rejects paths outside allowed directories', async () => {
106
+ const result = await executeTool('Read', { path: join(OUTSIDE_DIR, 'secret.txt') }, toolConfig);
107
+ expect(result).toContain('Error: Path not allowed');
108
+ });
109
+ it('returns error for nonexistent file', async () => {
110
+ const result = await executeTool('Read', { path: join(TEST_DIR, 'nope.txt') }, toolConfig);
111
+ expect(result).toContain('Error: File not found');
112
+ });
113
+ it('rejects path traversal attempts', async () => {
114
+ const result = await executeTool('Read', { path: join(TEST_DIR, '..', '__test_outside__', 'secret.txt') }, toolConfig);
115
+ expect(result).toContain('Error: Path not allowed');
116
+ });
117
+ it('rejects sibling path prefix bypass attempts', async () => {
118
+ const siblingDir = `${TEST_DIR}-sibling`;
119
+ mkdirSync(siblingDir, { recursive: true });
120
+ const siblingFile = join(siblingDir, 'secret.txt');
121
+ writeFileSync(siblingFile, 'top secret');
122
+ const result = await executeTool('Read', { path: siblingFile }, toolConfig);
123
+ expect(result).toContain('Error: Path not allowed');
124
+ rmSync(siblingDir, { recursive: true, force: true });
125
+ });
126
+ });
127
+ describe('write_file', () => {
128
+ it('writes a new file', async () => {
129
+ const path = join(TEST_DIR, 'new.txt');
130
+ const result = await executeTool('Write', { path, content: 'new content' }, toolConfig);
131
+ expect(result).toContain('Written');
132
+ expect(readFileSync(path, 'utf-8')).toBe('new content');
133
+ });
134
+ it('creates parent directories', async () => {
135
+ const path = join(TEST_DIR, 'sub', 'deep', 'file.txt');
136
+ const result = await executeTool('Write', { path, content: 'deep' }, toolConfig);
137
+ expect(result).toContain('Written');
138
+ expect(readFileSync(path, 'utf-8')).toBe('deep');
139
+ });
140
+ it('rejects writes outside allowed paths', async () => {
141
+ const result = await executeTool('Write', { path: join(OUTSIDE_DIR, 'hack.txt'), content: 'bad' }, toolConfig);
142
+ expect(result).toContain('Error: Path not allowed');
143
+ expect(existsSync(join(OUTSIDE_DIR, 'hack.txt'))).toBe(false);
144
+ });
145
+ });
146
+ describe('list_directory', () => {
147
+ it('lists directory contents', async () => {
148
+ const result = await executeTool('Glob', { path: TEST_DIR }, toolConfig);
149
+ expect(result).toContain('hello.txt');
150
+ expect(result).toContain('file');
151
+ });
152
+ it('rejects listing outside allowed paths', async () => {
153
+ const result = await executeTool('Glob', { path: OUTSIDE_DIR }, toolConfig);
154
+ expect(result).toContain('Error: Path not allowed');
155
+ });
156
+ it('returns error for nonexistent directory', async () => {
157
+ const result = await executeTool('Glob', { path: join(TEST_DIR, 'nope') }, toolConfig);
158
+ expect(result).toContain('Error: Directory not found');
159
+ });
160
+ });
161
+ describe('bash', () => {
162
+ it('executes a simple command', async () => {
163
+ const result = await executeTool('Bash', { command: 'echo hello' }, toolConfig);
164
+ expect(result.trim()).toBe('hello');
165
+ });
166
+ it('blocks dangerous commands', async () => {
167
+ const result = await executeTool('Bash', { command: 'rm -rf /' }, toolConfig);
168
+ expect(result).toContain('Error: Command blocked');
169
+ });
170
+ it('blocks sudo', async () => {
171
+ const result = await executeTool('Bash', { command: 'sudo cat /etc/passwd' }, toolConfig);
172
+ expect(result).toContain('Error: Command blocked');
173
+ });
174
+ it('respects cwd when in allowed paths', async () => {
175
+ const result = await executeTool('Bash', { command: 'ls', cwd: TEST_DIR }, toolConfig);
176
+ expect(result).toContain('hello.txt');
177
+ });
178
+ it('rejects cwd outside allowed paths', async () => {
179
+ const result = await executeTool('Bash', { command: 'ls', cwd: OUTSIDE_DIR }, toolConfig);
180
+ expect(result).toContain('Error: Working directory not in allowed paths');
181
+ });
182
+ it('returns stderr on failure', async () => {
183
+ const result = await executeTool('Bash', { command: 'cat /nonexistent_file_xyz' }, toolConfig);
184
+ expect(result).toContain('No such file');
185
+ });
186
+ it('handles unknown tools', async () => {
187
+ const result = await executeTool('delete_everything', {}, toolConfig);
188
+ expect(result).toContain('Error: Unknown tool');
189
+ });
190
+ });
191
+ describe('browser', () => {
192
+ const browserDisabledConfig = {
193
+ ...toolConfig,
194
+ browser: { enabled: false },
195
+ };
196
+ const browserEnabledConfig = {
197
+ ...toolConfig,
198
+ browser: { enabled: true },
199
+ };
200
+ const browserWithFileConfig = {
201
+ ...toolConfig,
202
+ browser: { enabled: true, allowFile: true },
203
+ };
204
+ it('returns error when browser is disabled', async () => {
205
+ const result = await executeTool('Browser', { action: 'open', url: 'https://example.com' }, browserDisabledConfig);
206
+ expect(result).toContain('Error: Browser tool is disabled');
207
+ });
208
+ it('returns error when browser config is missing', async () => {
209
+ const result = await executeTool('Browser', { action: 'open', url: 'https://example.com' }, toolConfig);
210
+ expect(result).toContain('Error: Browser tool is disabled');
211
+ });
212
+ it('blocks file:// URLs when allowFile is false', async () => {
213
+ const result = await executeTool('Browser', { action: 'open', url: 'file:///etc/passwd' }, browserEnabledConfig);
214
+ expect(result).toContain('Error: file:// URLs are blocked');
215
+ });
216
+ it('validates file:// URLs with new URL() parsing', async () => {
217
+ // file://localhost/etc/passwd should parse to /etc/passwd, which is outside allowedPaths
218
+ const result = await executeTool('Browser', { action: 'open', url: 'file://localhost/etc/passwd' }, browserWithFileConfig);
219
+ expect(result).toContain('Error: file:// URLs are blocked');
220
+ });
221
+ it('returns error for unknown action', async () => {
222
+ const result = await executeTool('Browser', { action: 'destroy' }, browserEnabledConfig);
223
+ expect(result).toContain('Error: Unknown browser action "destroy"');
224
+ });
225
+ it('requires url for open action', async () => {
226
+ const result = await executeTool('Browser', { action: 'open' }, browserEnabledConfig);
227
+ expect(result).toContain('Error: url is required');
228
+ });
229
+ it('requires selector for click action', async () => {
230
+ const result = await executeTool('Browser', { action: 'click' }, browserEnabledConfig);
231
+ expect(result).toContain('Error: Browser not open');
232
+ });
233
+ it('requires selector and text for type action', async () => {
234
+ const result = await executeTool('Browser', { action: 'type', selector: '#input' }, browserEnabledConfig);
235
+ expect(result).toContain('Error: Browser not open');
236
+ });
237
+ it('requires script for evaluate action', async () => {
238
+ const result = await executeTool('Browser', { action: 'evaluate' }, browserEnabledConfig);
239
+ expect(result).toContain('Error: Browser not open');
240
+ });
241
+ it('requires selector for hover action', async () => {
242
+ const result = await executeTool('Browser', { action: 'hover' }, browserEnabledConfig);
243
+ expect(result).toContain('Error: Browser not open');
244
+ });
245
+ it('requires selector and text for select action', async () => {
246
+ const result = await executeTool('Browser', { action: 'select' }, browserEnabledConfig);
247
+ expect(result).toContain('Error: Browser not open');
248
+ });
249
+ it('returns "Browser not open" for actions before open', async () => {
250
+ for (const action of ['click', 'type', 'waitfor', 'screenshot', 'evaluate', 'gettext', 'scroll', 'select', 'hover']) {
251
+ const result = await executeTool('Browser', { action }, browserEnabledConfig);
252
+ expect(result).toContain('Error: Browser not open');
253
+ }
254
+ });
255
+ it('handles close when browser is not open', async () => {
256
+ const result = await executeTool('Browser', { action: 'close' }, browserEnabledConfig);
257
+ expect(result).toBe('Browser closed.');
258
+ });
259
+ it('handles case-insensitive actions', async () => {
260
+ const result = await executeTool('Browser', { action: 'OPEN' }, browserEnabledConfig);
261
+ expect(result).toContain('Error: url is required');
262
+ });
263
+ it('maps Browser name correctly', () => {
264
+ expect(fromClaudeCodeName('Browser')).toBe('browser');
265
+ expect(toClaudeCodeName('browser')).toBe('Browser');
266
+ });
267
+ });
268
+ describe('code_with_agent', () => {
269
+ describe('tool definition', () => {
270
+ it('has correct name and required fields', () => {
271
+ expect(CODE_WITH_AGENT_TOOL.name).toBe('code_with_agent');
272
+ expect(CODE_WITH_AGENT_TOOL.input_schema.required).toEqual(['task']);
273
+ });
274
+ it('has all expected properties in schema', () => {
275
+ const props = Object.keys(CODE_WITH_AGENT_TOOL.input_schema.properties);
276
+ expect(props).toContain('task');
277
+ expect(props).toContain('agent');
278
+ expect(props).toContain('workdir');
279
+ expect(props).toContain('model');
280
+ expect(props).toContain('max_turns');
281
+ expect(props).toContain('validate');
282
+ });
283
+ it('is included in getToolDefinitions when includeSpawnSubagent is true', async () => {
284
+ const tools = await getToolDefinitions(toolConfig, { includeSpawnSubagent: true });
285
+ expect(tools.map(t => t.name)).toContain('code_with_agent');
286
+ });
287
+ it('is excluded from getToolDefinitions when includeSpawnSubagent is false', async () => {
288
+ const tools = await getToolDefinitions(toolConfig);
289
+ expect(tools.map(t => t.name)).not.toContain('code_with_agent');
290
+ });
291
+ });
292
+ describe('buildCodeAgentArgs', () => {
293
+ it('builds claude args with defaults', () => {
294
+ const { cmd, args } = buildCodeAgentArgs({ task: 'fix the bug' });
295
+ expect(cmd).toContain('claude');
296
+ expect(args).toContain('-p');
297
+ expect(args).toContain('--output-format');
298
+ expect(args).toContain('stream-json');
299
+ expect(args).toContain('--dangerously-skip-permissions');
300
+ expect(args).toContain('--max-turns');
301
+ expect(args).toContain('50');
302
+ expect(args[args.length - 1]).toBe('fix the bug');
303
+ });
304
+ it('builds claude args with model override', () => {
305
+ const { cmd, args } = buildCodeAgentArgs({ task: 'fix it', model: 'opus' });
306
+ expect(cmd).toContain('claude');
307
+ expect(args).toContain('--model');
308
+ expect(args).toContain('opus');
309
+ });
310
+ it('builds claude args with custom max_turns', () => {
311
+ const { args } = buildCodeAgentArgs({ task: 'fix it', max_turns: 10 });
312
+ const idx = args.indexOf('--max-turns');
313
+ expect(args[idx + 1]).toBe('10');
314
+ });
315
+ it('builds codex args with defaults', () => {
316
+ const { cmd, args } = buildCodeAgentArgs({ task: 'fix the bug', agent: 'codex' });
317
+ expect(cmd).toContain('codex');
318
+ expect(args[0]).toBe('exec');
319
+ expect(args).toContain('--full-auto');
320
+ expect(args).toContain('--json');
321
+ expect(args).toContain('--color');
322
+ expect(args).toContain('never');
323
+ expect(args[args.length - 1]).toBe('fix the bug');
324
+ });
325
+ it('builds codex args with workdir', () => {
326
+ const { args } = buildCodeAgentArgs({ task: 'fix it', agent: 'codex', workdir: '/tmp/project' });
327
+ expect(args).toContain('-C');
328
+ expect(args).toContain('/tmp/project');
329
+ });
330
+ it('builds codex args with model override', () => {
331
+ const { args } = buildCodeAgentArgs({ task: 'fix it', agent: 'codex', model: 'gpt-5.3-codex' });
332
+ expect(args).toContain('-m');
333
+ expect(args).toContain('gpt-5.3-codex');
334
+ });
335
+ it('does not include --allowedTools for codex', () => {
336
+ const { args } = buildCodeAgentArgs({ task: 'fix it', agent: 'codex' });
337
+ expect(args).not.toContain('--allowedTools');
338
+ });
339
+ it('includes --append-system-prompt for claude', () => {
340
+ const { args } = buildCodeAgentArgs({ task: 'fix it' });
341
+ expect(args).toContain('--append-system-prompt');
342
+ });
343
+ });
344
+ describe('resolveModelAlias', () => {
345
+ it('returns undefined for undefined input', () => {
346
+ expect(resolveModelAlias(undefined, {})).toBeUndefined();
347
+ });
348
+ it('resolves aliases from config', () => {
349
+ expect(resolveModelAlias('fast', { fast: 'claude-haiku-4-5' })).toBe('claude-haiku-4-5');
350
+ });
351
+ it('strips provider prefix', () => {
352
+ expect(resolveModelAlias('anthropic/claude-sonnet-4-5', {})).toBe('claude-sonnet-4-5');
353
+ expect(resolveModelAlias('openai/gpt-4.1', {})).toBe('gpt-4.1');
354
+ });
355
+ it('migrates claude-3.5-sonnet to claude-sonnet-4-6', () => {
356
+ expect(resolveModelAlias('claude-3.5-sonnet', {})).toBe('claude-sonnet-4-6');
357
+ expect(resolveModelAlias('claude-3-5-sonnet', {})).toBe('claude-sonnet-4-6');
358
+ expect(resolveModelAlias('claude-3-5-sonnet-20241022', {})).toBe('claude-sonnet-4-6');
359
+ });
360
+ it('migrates claude-3.5-haiku to claude-haiku-4-5', () => {
361
+ expect(resolveModelAlias('claude-3.5-haiku', {})).toBe('claude-haiku-4-5');
362
+ expect(resolveModelAlias('claude-3-5-haiku', {})).toBe('claude-haiku-4-5');
363
+ });
364
+ it('migrates with provider prefix stripped', () => {
365
+ expect(resolveModelAlias('anthropic/claude-3.5-sonnet', {})).toBe('claude-sonnet-4-6');
366
+ expect(resolveModelAlias('anthropic/claude-3-5-sonnet-20241022', {})).toBe('claude-sonnet-4-6');
367
+ });
368
+ it('returns model as-is when no transformation needed', () => {
369
+ expect(resolveModelAlias('claude-sonnet-4-5', {})).toBe('claude-sonnet-4-5');
370
+ expect(resolveModelAlias('gpt-4.1', {})).toBe('gpt-4.1');
371
+ });
372
+ });
373
+ describe('executeTool routing', () => {
374
+ it('rejects workdir outside allowed paths', async () => {
375
+ const result = await executeTool('code_with_agent', {
376
+ task: 'fix it',
377
+ workdir: '/tmp/not-allowed',
378
+ }, toolConfig);
379
+ expect(result).toContain('Error: Working directory not allowed');
380
+ });
381
+ it('returns error when task is missing', async () => {
382
+ const result = await executeTool('code_with_agent', {}, toolConfig);
383
+ expect(result).toContain('Error: task is required');
384
+ });
385
+ it('returns error for invalid agent', async () => {
386
+ const result = await executeTool('code_with_agent', {
387
+ task: 'fix it',
388
+ agent: 'gpt',
389
+ }, toolConfig);
390
+ expect(result).toContain('Error: Invalid agent "gpt"');
391
+ });
392
+ });
393
+ describe('check_code_agent tool', () => {
394
+ it('has expected tool definition', () => {
395
+ expect(CHECK_CODE_AGENT_TOOL.name).toBe('check_code_agent');
396
+ expect(CHECK_CODE_AGENT_TOOL.input_schema.properties).toHaveProperty('id');
397
+ });
398
+ it('is included in getToolDefinitions when includeSpawnSubagent is true', async () => {
399
+ const tools = await getToolDefinitions(toolConfig, { includeSpawnSubagent: true });
400
+ expect(tools.map(t => t.name)).toContain('check_code_agent');
401
+ });
402
+ it('is excluded from getToolDefinitions when includeSpawnSubagent is false', async () => {
403
+ const tools = await getToolDefinitions(toolConfig);
404
+ expect(tools.map(t => t.name)).not.toContain('check_code_agent');
405
+ });
406
+ });
407
+ describe('multi-agent tracking', () => {
408
+ it('getActiveCodeAgents returns empty array initially', () => {
409
+ // Active agents are those currently running — initially none
410
+ const active = getActiveCodeAgents();
411
+ expect(Array.isArray(active)).toBe(true);
412
+ });
413
+ it('getRecentCodeAgents returns empty array initially', () => {
414
+ const recent = getRecentCodeAgents();
415
+ expect(Array.isArray(recent)).toBe(true);
416
+ });
417
+ it('getCodeAgent returns null for nonexistent ID', () => {
418
+ const result = getCodeAgent('ca-999');
419
+ expect(result).toBeNull();
420
+ });
421
+ it('check_code_agent with nonexistent ID returns not found message', async () => {
422
+ const result = await executeTool('check_code_agent', { id: 'ca-999' }, toolConfig);
423
+ expect(result).toContain('No coding agent found');
424
+ });
425
+ it('check_code_agent with no args returns message about no agents', async () => {
426
+ const result = await executeTool('check_code_agent', {}, toolConfig);
427
+ // May return "No coding agents have run yet" or a list depending on state
428
+ expect(typeof result).toBe('string');
429
+ });
430
+ });
431
+ });
432
+ describe('code_with_team', () => {
433
+ describe('tool definition', () => {
434
+ it('has correct name and required fields', () => {
435
+ expect(CODE_WITH_TEAM_TOOL.name).toBe('code_with_team');
436
+ expect(CODE_WITH_TEAM_TOOL.input_schema.required).toEqual(['task']);
437
+ });
438
+ it('has expected properties in schema (no max_turns)', () => {
439
+ const props = Object.keys(CODE_WITH_TEAM_TOOL.input_schema.properties);
440
+ expect(props).toContain('task');
441
+ expect(props).toContain('team_size');
442
+ expect(props).toContain('workdir');
443
+ expect(props).toContain('agent');
444
+ expect(props).toContain('model');
445
+ expect(props).toContain('timeout_minutes');
446
+ expect(props).toContain('validate');
447
+ // max_turns removed — not relevant for parallel agents
448
+ expect(props).not.toContain('max_turns');
449
+ });
450
+ it('is included in getToolDefinitions when includeSpawnSubagent is true', async () => {
451
+ const tools = await getToolDefinitions(toolConfig, { includeSpawnSubagent: true });
452
+ expect(tools.map(t => t.name)).toContain('code_with_team');
453
+ });
454
+ it('is excluded from getToolDefinitions when includeSpawnSubagent is false', async () => {
455
+ const tools = await getToolDefinitions(toolConfig);
456
+ expect(tools.map(t => t.name)).not.toContain('code_with_team');
457
+ });
458
+ });
459
+ describe('executeTool routing', () => {
460
+ it('rejects workdir outside allowed paths', async () => {
461
+ const result = await executeTool('code_with_team', {
462
+ task: 'refactor auth',
463
+ workdir: '/tmp/not-allowed',
464
+ }, toolConfig);
465
+ expect(result).toContain('Error: Working directory not allowed');
466
+ });
467
+ it('returns error when task is missing', async () => {
468
+ const result = await executeTool('code_with_team', {}, toolConfig);
469
+ expect(result).toContain('Error: task is required');
470
+ });
471
+ it('returns error when agent is invalid', async () => {
472
+ const result = await executeTool('code_with_team', {
473
+ task: 'refactor auth',
474
+ agent: 'not-a-real-agent',
475
+ }, toolConfig);
476
+ expect(result).toContain('Error: Invalid agent');
477
+ });
478
+ });
479
+ });
480
+ describe('decomposeTask', () => {
481
+ it('falls back to numbered subtasks with DecomposedSubtask format on error', async () => {
482
+ // decomposeTask with no valid config will fail the model call and use fallback
483
+ const mockConfig = {
484
+ agents: { list: {} },
485
+ };
486
+ const subtasks = await decomposeTask('fix everything', 3, mockConfig);
487
+ expect(subtasks).toHaveLength(3);
488
+ expect(subtasks[0].description).toContain('Part 1 of 3');
489
+ expect(subtasks[1].description).toContain('Part 2 of 3');
490
+ expect(subtasks[2].description).toContain('Part 3 of 3');
491
+ expect(subtasks[0].description).toContain('fix everything');
492
+ // Fallback produces all-independent subtasks
493
+ expect(subtasks[0].dependsOn).toEqual([]);
494
+ expect(subtasks[1].dependsOn).toEqual([]);
495
+ expect(subtasks[2].dependsOn).toEqual([]);
496
+ });
497
+ });
498
+ describe('computeWaves', () => {
499
+ it('puts all independent subtasks in a single wave', () => {
500
+ const subtasks = [
501
+ { description: 'task A', dependsOn: [] },
502
+ { description: 'task B', dependsOn: [] },
503
+ { description: 'task C', dependsOn: [] },
504
+ ];
505
+ const waves = computeWaves(subtasks);
506
+ expect(waves).toHaveLength(1);
507
+ expect(waves[0]).toEqual([0, 1, 2]);
508
+ });
509
+ it('creates sequential waves for a dependency chain', () => {
510
+ const subtasks = [
511
+ { description: 'schema', dependsOn: [] },
512
+ { description: 'queries', dependsOn: [0] },
513
+ { description: 'tests', dependsOn: [1] },
514
+ ];
515
+ const waves = computeWaves(subtasks);
516
+ expect(waves).toHaveLength(3);
517
+ expect(waves[0]).toEqual([0]);
518
+ expect(waves[1]).toEqual([1]);
519
+ expect(waves[2]).toEqual([2]);
520
+ });
521
+ it('groups tasks with shared dependency into the same wave', () => {
522
+ const subtasks = [
523
+ { description: 'schema', dependsOn: [] },
524
+ { description: 'API endpoints', dependsOn: [0] },
525
+ { description: 'admin endpoints', dependsOn: [0] },
526
+ { description: 'integration tests', dependsOn: [1, 2] },
527
+ ];
528
+ const waves = computeWaves(subtasks);
529
+ expect(waves).toHaveLength(3);
530
+ expect(waves[0]).toEqual([0]);
531
+ expect(waves[1].sort()).toEqual([1, 2]);
532
+ expect(waves[2]).toEqual([3]);
533
+ });
534
+ it('handles mixed independent and dependent tasks', () => {
535
+ const subtasks = [
536
+ { description: 'setup types', dependsOn: [] },
537
+ { description: 'write docs', dependsOn: [] },
538
+ { description: 'implement using types', dependsOn: [0] },
539
+ ];
540
+ const waves = computeWaves(subtasks);
541
+ expect(waves).toHaveLength(2);
542
+ expect(waves[0].sort()).toEqual([0, 1]);
543
+ expect(waves[1]).toEqual([2]);
544
+ });
545
+ it('handles dependency cycles by forcing remaining into current wave', () => {
546
+ const subtasks = [
547
+ { description: 'A depends on B', dependsOn: [1] },
548
+ { description: 'B depends on A', dependsOn: [0] },
549
+ ];
550
+ const waves = computeWaves(subtasks);
551
+ // Both should end up in a wave despite the cycle
552
+ const allIndices = waves.flat().sort();
553
+ expect(allIndices).toEqual([0, 1]);
554
+ });
555
+ it('handles single subtask', () => {
556
+ const subtasks = [
557
+ { description: 'only task', dependsOn: [] },
558
+ ];
559
+ const waves = computeWaves(subtasks);
560
+ expect(waves).toHaveLength(1);
561
+ expect(waves[0]).toEqual([0]);
562
+ });
563
+ it('handles empty subtask list', () => {
564
+ const waves = computeWaves([]);
565
+ expect(waves).toHaveLength(0);
566
+ });
567
+ });
568
+ describe('synthesizeResults', () => {
569
+ it('falls back to mechanical summary on error', async () => {
570
+ const mockConfig = {
571
+ agents: { list: {} },
572
+ };
573
+ const results = [
574
+ { subtask: 'fix auth', status: 'completed', output: 'done' },
575
+ { subtask: 'fix tests', status: 'failed', error: 'timeout' },
576
+ ];
577
+ const summary = await synthesizeResults('fix everything', results, mockConfig);
578
+ expect(summary).toContain('1/2 subtasks succeeded');
579
+ expect(summary).toContain('1 failed');
580
+ expect(summary).toContain('fix auth');
581
+ expect(summary).toContain('fix tests');
582
+ });
583
+ });
584
+ describe('readTeamState (deprecated)', () => {
585
+ it('always returns null', () => {
586
+ const state = readTeamState();
587
+ expect(state).toBeNull();
588
+ });
589
+ });
@@ -0,0 +1 @@
1
+ export {};