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.
- package/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { executeTool, BUILTIN_TOOL_DEFINITIONS,
|
|
4
|
+
import { executeTool, BUILTIN_TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CHECK_CODE_AGENT_TOOL, DELEGATE_TO_AGENT_TOOL, getToolDefinitions, fromClaudeCodeName, toClaudeCodeName, buildCodeAgentArgs, getActiveCodeAgents, getRecentCodeAgents, getCodeAgent, normalizeMcpToolArgsForExecution } from '../tools.js';
|
|
5
|
+
import { registerDelegateToAgentHandler } from '../tools/agent-delegation.js';
|
|
5
6
|
import { resolveModelAlias } from '../code-agents/utils.js';
|
|
6
7
|
const TEST_DIR = join(process.cwd(), '__test_sandbox__');
|
|
7
8
|
const OUTSIDE_DIR = join(process.cwd(), '__test_outside__');
|
|
@@ -30,14 +31,10 @@ describe('BUILTIN_TOOL_DEFINITIONS', () => {
|
|
|
30
31
|
expect(names).toContain('Glob');
|
|
31
32
|
expect(names).toContain('Bash');
|
|
32
33
|
});
|
|
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
34
|
});
|
|
38
35
|
describe('getToolDefinitions', () => {
|
|
39
36
|
it('returns at least the 4 built-in tools', async () => {
|
|
40
|
-
const tools = await getToolDefinitions();
|
|
37
|
+
const tools = await getToolDefinitions(undefined, { includeMcp: false });
|
|
41
38
|
expect(tools.length).toBeGreaterThanOrEqual(4);
|
|
42
39
|
const names = tools.map(t => t.name);
|
|
43
40
|
expect(names).toContain('Read');
|
|
@@ -45,20 +42,6 @@ describe('getToolDefinitions', () => {
|
|
|
45
42
|
expect(names).toContain('Glob');
|
|
46
43
|
expect(names).toContain('Bash');
|
|
47
44
|
}, 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
45
|
describe('tool profiles', () => {
|
|
63
46
|
it('minimal returns built-in tools plus Fetch', async () => {
|
|
64
47
|
const config = { ...toolConfig, toolProfile: 'minimal' };
|
|
@@ -66,23 +49,17 @@ describe('getToolDefinitions', () => {
|
|
|
66
49
|
expect(tools).toHaveLength(5);
|
|
67
50
|
expect(tools.map(t => t.name)).toEqual(['Read', 'Write', 'Glob', 'Bash', 'Fetch']);
|
|
68
51
|
});
|
|
69
|
-
it('minimal excludes Browser even when browser.enabled is true', async () => {
|
|
70
|
-
const config = { ...toolConfig, toolProfile: 'minimal', browser: { enabled: true } };
|
|
71
|
-
const tools = await getToolDefinitions(config);
|
|
72
|
-
expect(tools.map(t => t.name)).not.toContain('Browser');
|
|
73
|
-
});
|
|
74
52
|
it('minimal excludes MCP tools', async () => {
|
|
75
53
|
const config = { ...toolConfig, toolProfile: 'minimal' };
|
|
76
54
|
const tools = await getToolDefinitions(config, { includeMcp: true });
|
|
77
55
|
expect(tools.every(t => !t.name.startsWith('mcp__'))).toBe(true);
|
|
78
56
|
});
|
|
79
|
-
it('coding includes code_with_agent and check_code_agent
|
|
57
|
+
it('coding includes code_with_agent and check_code_agent', async () => {
|
|
80
58
|
const config = { ...toolConfig, toolProfile: 'coding' };
|
|
81
59
|
const tools = await getToolDefinitions(config, { includeAgentTools: true });
|
|
82
60
|
const names = tools.map(t => t.name);
|
|
83
61
|
expect(names).toContain('code_with_agent');
|
|
84
62
|
expect(names).toContain('check_code_agent');
|
|
85
|
-
expect(names).not.toContain('code_with_team');
|
|
86
63
|
});
|
|
87
64
|
it('coding excludes MCP tools', async () => {
|
|
88
65
|
const config = { ...toolConfig, toolProfile: 'coding' };
|
|
@@ -90,8 +67,8 @@ describe('getToolDefinitions', () => {
|
|
|
90
67
|
expect(tools.every(t => !t.name.startsWith('mcp__'))).toBe(true);
|
|
91
68
|
});
|
|
92
69
|
it('full profile behaves like default (no profile set)', async () => {
|
|
93
|
-
const defaultTools = await getToolDefinitions(toolConfig);
|
|
94
|
-
const fullTools = await getToolDefinitions({ ...toolConfig, toolProfile: 'full' });
|
|
70
|
+
const defaultTools = await getToolDefinitions(toolConfig, { includeMcp: false });
|
|
71
|
+
const fullTools = await getToolDefinitions({ ...toolConfig, toolProfile: 'full' }, { includeMcp: false });
|
|
95
72
|
expect(fullTools.map(t => t.name)).toEqual(defaultTools.map(t => t.name));
|
|
96
73
|
}, 15000);
|
|
97
74
|
});
|
|
@@ -132,6 +109,27 @@ describe('MCP tool name parsing', () => {
|
|
|
132
109
|
expect(result).toContain('Error:');
|
|
133
110
|
expect(result).not.toContain('Unknown tool');
|
|
134
111
|
});
|
|
112
|
+
it('normalizes context-a8c execute-tool parameter aliases', () => {
|
|
113
|
+
expect(normalizeMcpToolArgsForExecution('context-a8c', 'context-a8c-execute-tool', {
|
|
114
|
+
provider: 'zendesk',
|
|
115
|
+
tool: 'search',
|
|
116
|
+
parameters: { query: 'jetpack search' },
|
|
117
|
+
})).toEqual({
|
|
118
|
+
provider: 'zendesk',
|
|
119
|
+
tool: 'search',
|
|
120
|
+
params: { query: 'jetpack search' },
|
|
121
|
+
});
|
|
122
|
+
expect(normalizeMcpToolArgsForExecution('context-a8c', 'context-a8c-execute-tool', {
|
|
123
|
+
provider: 'slack',
|
|
124
|
+
tool: 'search',
|
|
125
|
+
query: 'jetpack search',
|
|
126
|
+
limit: 10,
|
|
127
|
+
})).toEqual({
|
|
128
|
+
provider: 'slack',
|
|
129
|
+
tool: 'search',
|
|
130
|
+
params: { query: 'jetpack search', limit: 10 },
|
|
131
|
+
});
|
|
132
|
+
});
|
|
135
133
|
});
|
|
136
134
|
describe('read_file', () => {
|
|
137
135
|
it('reads a file within allowed paths', async () => {
|
|
@@ -164,13 +162,13 @@ describe('write_file', () => {
|
|
|
164
162
|
it('writes a new file', async () => {
|
|
165
163
|
const path = join(TEST_DIR, 'new.txt');
|
|
166
164
|
const result = await executeTool('Write', { path, content: 'new content' }, toolConfig);
|
|
167
|
-
expect(result).
|
|
165
|
+
expect(result).toBe('OK');
|
|
168
166
|
expect(readFileSync(path, 'utf-8')).toBe('new content');
|
|
169
167
|
});
|
|
170
168
|
it('creates parent directories', async () => {
|
|
171
169
|
const path = join(TEST_DIR, 'sub', 'deep', 'file.txt');
|
|
172
170
|
const result = await executeTool('Write', { path, content: 'deep' }, toolConfig);
|
|
173
|
-
expect(result).
|
|
171
|
+
expect(result).toBe('OK');
|
|
174
172
|
expect(readFileSync(path, 'utf-8')).toBe('deep');
|
|
175
173
|
});
|
|
176
174
|
it('rejects writes outside allowed paths', async () => {
|
|
@@ -199,6 +197,14 @@ describe('bash', () => {
|
|
|
199
197
|
const result = await executeTool('Bash', { command: 'echo hello' }, toolConfig);
|
|
200
198
|
expect(result.trim()).toBe('hello');
|
|
201
199
|
});
|
|
200
|
+
it('blocks shell command chaining to prevent injection patterns', async () => {
|
|
201
|
+
const result = await executeTool('Bash', { command: 'echo hello; uname -a' }, toolConfig);
|
|
202
|
+
expect(result).toContain('Shell control operators are blocked');
|
|
203
|
+
});
|
|
204
|
+
it('blocks shell pipe operators to avoid implicit shell mode', async () => {
|
|
205
|
+
const result = await executeTool('Bash', { command: 'echo hello | cat' }, toolConfig);
|
|
206
|
+
expect(result).toContain('Shell control operators are blocked');
|
|
207
|
+
});
|
|
202
208
|
it('blocks dangerous commands via exec approval', async () => {
|
|
203
209
|
const result = await executeTool('Bash', { command: `rm -rf ${TEST_DIR}` }, toolConfig);
|
|
204
210
|
expect(result).toContain('⛔');
|
|
@@ -257,83 +263,6 @@ describe('bash', () => {
|
|
|
257
263
|
});
|
|
258
264
|
});
|
|
259
265
|
});
|
|
260
|
-
describe('browser', () => {
|
|
261
|
-
const browserDisabledConfig = {
|
|
262
|
-
...toolConfig,
|
|
263
|
-
browser: { enabled: false },
|
|
264
|
-
};
|
|
265
|
-
const browserEnabledConfig = {
|
|
266
|
-
...toolConfig,
|
|
267
|
-
browser: { enabled: true },
|
|
268
|
-
};
|
|
269
|
-
const browserWithFileConfig = {
|
|
270
|
-
...toolConfig,
|
|
271
|
-
browser: { enabled: true, allowFile: true },
|
|
272
|
-
};
|
|
273
|
-
it('returns error when browser is disabled', async () => {
|
|
274
|
-
const result = await executeTool('Browser', { action: 'open', url: 'https://example.com' }, browserDisabledConfig);
|
|
275
|
-
expect(result).toContain('Error: Browser tool is disabled');
|
|
276
|
-
});
|
|
277
|
-
it('returns error when browser config is missing', async () => {
|
|
278
|
-
const result = await executeTool('Browser', { action: 'open', url: 'https://example.com' }, toolConfig);
|
|
279
|
-
expect(result).toContain('Error: Browser tool is disabled');
|
|
280
|
-
});
|
|
281
|
-
it('blocks file:// URLs when allowFile is false', async () => {
|
|
282
|
-
const result = await executeTool('Browser', { action: 'open', url: 'file:///etc/passwd' }, browserEnabledConfig);
|
|
283
|
-
expect(result).toContain('Error: file:// URLs are blocked');
|
|
284
|
-
});
|
|
285
|
-
it('validates file:// URLs with new URL() parsing', async () => {
|
|
286
|
-
// file://localhost/etc/passwd should parse to /etc/passwd, which is outside allowedPaths
|
|
287
|
-
const result = await executeTool('Browser', { action: 'open', url: 'file://localhost/etc/passwd' }, browserWithFileConfig);
|
|
288
|
-
expect(result).toContain('Error: file:// URLs are blocked');
|
|
289
|
-
});
|
|
290
|
-
it('returns error for unknown action', async () => {
|
|
291
|
-
const result = await executeTool('Browser', { action: 'destroy' }, browserEnabledConfig);
|
|
292
|
-
expect(result).toContain('Error: Unknown browser action "destroy"');
|
|
293
|
-
});
|
|
294
|
-
it('requires url for open action', async () => {
|
|
295
|
-
const result = await executeTool('Browser', { action: 'open' }, browserEnabledConfig);
|
|
296
|
-
expect(result).toContain('Error: url is required');
|
|
297
|
-
});
|
|
298
|
-
it('requires selector for click action', async () => {
|
|
299
|
-
const result = await executeTool('Browser', { action: 'click' }, browserEnabledConfig);
|
|
300
|
-
expect(result).toContain('Error: Browser not open');
|
|
301
|
-
});
|
|
302
|
-
it('requires selector and text for type action', async () => {
|
|
303
|
-
const result = await executeTool('Browser', { action: 'type', selector: '#input' }, browserEnabledConfig);
|
|
304
|
-
expect(result).toContain('Error: Browser not open');
|
|
305
|
-
});
|
|
306
|
-
it('requires script for evaluate action', async () => {
|
|
307
|
-
const result = await executeTool('Browser', { action: 'evaluate' }, browserEnabledConfig);
|
|
308
|
-
expect(result).toContain('Error: Browser not open');
|
|
309
|
-
});
|
|
310
|
-
it('requires selector for hover action', async () => {
|
|
311
|
-
const result = await executeTool('Browser', { action: 'hover' }, browserEnabledConfig);
|
|
312
|
-
expect(result).toContain('Error: Browser not open');
|
|
313
|
-
});
|
|
314
|
-
it('requires selector and text for select action', async () => {
|
|
315
|
-
const result = await executeTool('Browser', { action: 'select' }, browserEnabledConfig);
|
|
316
|
-
expect(result).toContain('Error: Browser not open');
|
|
317
|
-
});
|
|
318
|
-
it('returns "Browser not open" for actions before open', async () => {
|
|
319
|
-
for (const action of ['click', 'type', 'waitfor', 'screenshot', 'evaluate', 'gettext', 'scroll', 'select', 'hover']) {
|
|
320
|
-
const result = await executeTool('Browser', { action }, browserEnabledConfig);
|
|
321
|
-
expect(result).toContain('Error: Browser not open');
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
it('handles close when browser is not open', async () => {
|
|
325
|
-
const result = await executeTool('Browser', { action: 'close' }, browserEnabledConfig);
|
|
326
|
-
expect(result).toBe('Browser closed.');
|
|
327
|
-
});
|
|
328
|
-
it('handles case-insensitive actions', async () => {
|
|
329
|
-
const result = await executeTool('Browser', { action: 'OPEN' }, browserEnabledConfig);
|
|
330
|
-
expect(result).toContain('Error: url is required');
|
|
331
|
-
});
|
|
332
|
-
it('maps Browser name correctly', () => {
|
|
333
|
-
expect(fromClaudeCodeName('Browser')).toBe('browser');
|
|
334
|
-
expect(toClaudeCodeName('browser')).toBe('Browser');
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
266
|
describe('code_with_agent', () => {
|
|
338
267
|
describe('tool definition', () => {
|
|
339
268
|
it('has correct name and required fields', () => {
|
|
@@ -344,17 +273,15 @@ describe('code_with_agent', () => {
|
|
|
344
273
|
const props = Object.keys(CODE_WITH_AGENT_TOOL.input_schema.properties);
|
|
345
274
|
expect(props).toContain('task');
|
|
346
275
|
expect(props).toContain('agent');
|
|
347
|
-
expect(props).toContain('
|
|
348
|
-
|
|
349
|
-
expect(props).toContain('max_turns');
|
|
350
|
-
expect(props).toContain('validate');
|
|
276
|
+
expect(props).toContain('worktree');
|
|
277
|
+
// workdir, model, max_turns, validate — omitted from schema to save tokens (executor still accepts them)
|
|
351
278
|
});
|
|
352
279
|
it('is included in getToolDefinitions when includeSpawnSubagent is true', async () => {
|
|
353
|
-
const tools = await getToolDefinitions(toolConfig, { includeAgentTools: true });
|
|
280
|
+
const tools = await getToolDefinitions(toolConfig, { includeAgentTools: true, includeMcp: false });
|
|
354
281
|
expect(tools.map(t => t.name)).toContain('code_with_agent');
|
|
355
282
|
});
|
|
356
283
|
it('is excluded from getToolDefinitions when includeSpawnSubagent is false', async () => {
|
|
357
|
-
const tools = await getToolDefinitions(toolConfig);
|
|
284
|
+
const tools = await getToolDefinitions(toolConfig, { includeMcp: false });
|
|
358
285
|
expect(tools.map(t => t.name)).not.toContain('code_with_agent');
|
|
359
286
|
});
|
|
360
287
|
});
|
|
@@ -371,10 +298,10 @@ describe('code_with_agent', () => {
|
|
|
371
298
|
expect(args[args.length - 1]).toBe('fix the bug');
|
|
372
299
|
});
|
|
373
300
|
it('builds claude args with model override', () => {
|
|
374
|
-
const { cmd, args } = buildCodeAgentArgs({ task: 'fix it', model: 'opus' });
|
|
301
|
+
const { cmd, args } = buildCodeAgentArgs({ task: 'fix it', model: 'claude-opus-4-7' });
|
|
375
302
|
expect(cmd).toContain('claude');
|
|
376
303
|
expect(args).toContain('--model');
|
|
377
|
-
expect(args).toContain('opus');
|
|
304
|
+
expect(args).toContain('claude-opus-4-7');
|
|
378
305
|
});
|
|
379
306
|
it('builds claude args with custom max_turns', () => {
|
|
380
307
|
const { args } = buildCodeAgentArgs({ task: 'fix it', max_turns: 10 });
|
|
@@ -386,6 +313,7 @@ describe('code_with_agent', () => {
|
|
|
386
313
|
expect(cmd).toContain('codex');
|
|
387
314
|
expect(args[0]).toBe('exec');
|
|
388
315
|
expect(args).toContain('--full-auto');
|
|
316
|
+
expect(args).toContain('--skip-git-repo-check');
|
|
389
317
|
expect(args).toContain('--json');
|
|
390
318
|
expect(args).toContain('--color');
|
|
391
319
|
expect(args).toContain('never');
|
|
@@ -401,6 +329,11 @@ describe('code_with_agent', () => {
|
|
|
401
329
|
expect(args).toContain('-m');
|
|
402
330
|
expect(args).toContain('gpt-5.3-codex');
|
|
403
331
|
});
|
|
332
|
+
it('builds codex args with effort override', () => {
|
|
333
|
+
const { args } = buildCodeAgentArgs({ task: 'fix it', agent: 'codex', effort: 'xhigh' });
|
|
334
|
+
expect(args).toContain('-c');
|
|
335
|
+
expect(args).toContain('model_reasoning_effort=xhigh');
|
|
336
|
+
});
|
|
404
337
|
it('does not include --allowedTools for codex', () => {
|
|
405
338
|
const { args } = buildCodeAgentArgs({ task: 'fix it', agent: 'codex' });
|
|
406
339
|
expect(args).not.toContain('--allowedTools');
|
|
@@ -438,6 +371,9 @@ describe('code_with_agent', () => {
|
|
|
438
371
|
expect(resolveModelAlias('claude-sonnet-4-5', {})).toBe('claude-sonnet-4-5');
|
|
439
372
|
expect(resolveModelAlias('gpt-4.1', {})).toBe('gpt-4.1');
|
|
440
373
|
});
|
|
374
|
+
it('normalizes dotted claude opus 4.6 model id', () => {
|
|
375
|
+
expect(resolveModelAlias('anthropic/claude-opus-4.6', {})).toBe('claude-opus-4-6');
|
|
376
|
+
});
|
|
441
377
|
});
|
|
442
378
|
describe('executeTool routing', () => {
|
|
443
379
|
it('rejects workdir outside allowed paths', async () => {
|
|
@@ -465,14 +401,61 @@ describe('code_with_agent', () => {
|
|
|
465
401
|
expect(CHECK_CODE_AGENT_TOOL.input_schema.properties).toHaveProperty('id');
|
|
466
402
|
});
|
|
467
403
|
it('is included in getToolDefinitions when includeSpawnSubagent is true', async () => {
|
|
468
|
-
const tools = await getToolDefinitions(toolConfig, { includeAgentTools: true });
|
|
404
|
+
const tools = await getToolDefinitions(toolConfig, { includeAgentTools: true, includeMcp: false });
|
|
469
405
|
expect(tools.map(t => t.name)).toContain('check_code_agent');
|
|
470
406
|
});
|
|
471
407
|
it('is excluded from getToolDefinitions when includeSpawnSubagent is false', async () => {
|
|
472
|
-
const tools = await getToolDefinitions(toolConfig);
|
|
408
|
+
const tools = await getToolDefinitions(toolConfig, { includeMcp: false });
|
|
473
409
|
expect(tools.map(t => t.name)).not.toContain('check_code_agent');
|
|
474
410
|
});
|
|
475
411
|
});
|
|
412
|
+
describe('delegate_to_agent tool', () => {
|
|
413
|
+
afterEach(() => {
|
|
414
|
+
registerDelegateToAgentHandler(null);
|
|
415
|
+
});
|
|
416
|
+
it('has expected tool definition', () => {
|
|
417
|
+
expect(DELEGATE_TO_AGENT_TOOL.name).toBe('delegate_to_agent');
|
|
418
|
+
expect(DELEGATE_TO_AGENT_TOOL.input_schema.required).toEqual(['alias', 'task']);
|
|
419
|
+
});
|
|
420
|
+
it('is included in getToolDefinitions when agent tools are enabled', async () => {
|
|
421
|
+
const tools = await getToolDefinitions(toolConfig, { includeAgentTools: true, includeMcp: false });
|
|
422
|
+
expect(tools.map(t => t.name)).toContain('delegate_to_agent');
|
|
423
|
+
});
|
|
424
|
+
it('rejects non-Discord contexts', async () => {
|
|
425
|
+
const result = await executeTool('delegate_to_agent', {
|
|
426
|
+
alias: 'reviewer',
|
|
427
|
+
task: 'review this',
|
|
428
|
+
}, toolConfig, {
|
|
429
|
+
fullConfig: { agents: { default: 'main', list: {} } },
|
|
430
|
+
channel: 'telegram',
|
|
431
|
+
});
|
|
432
|
+
expect(result).toContain('Discord-only');
|
|
433
|
+
});
|
|
434
|
+
it('calls the registered delegation handler for Discord', async () => {
|
|
435
|
+
registerDelegateToAgentHandler(async (input) => `delegated ${input.alias}: ${input.task}`);
|
|
436
|
+
const result = await executeTool('delegate_to_agent', {
|
|
437
|
+
alias: '@Reviewer',
|
|
438
|
+
task: 'review this',
|
|
439
|
+
}, toolConfig, {
|
|
440
|
+
fullConfig: { agents: { default: 'main', list: {} } },
|
|
441
|
+
channel: 'discord',
|
|
442
|
+
channelTargetId: '123',
|
|
443
|
+
});
|
|
444
|
+
expect(result).toBe('delegated reviewer: review this');
|
|
445
|
+
});
|
|
446
|
+
it('blocks self delegation', async () => {
|
|
447
|
+
registerDelegateToAgentHandler(async () => 'should not run');
|
|
448
|
+
const result = await executeTool('delegate_to_agent', {
|
|
449
|
+
alias: 'reviewer',
|
|
450
|
+
task: 'review this',
|
|
451
|
+
}, toolConfig, {
|
|
452
|
+
fullConfig: { agents: { default: 'main', list: {} } },
|
|
453
|
+
channel: 'discord',
|
|
454
|
+
threadAgentAlias: 'reviewer',
|
|
455
|
+
});
|
|
456
|
+
expect(result).toContain('cannot delegate to itself');
|
|
457
|
+
});
|
|
458
|
+
});
|
|
476
459
|
describe('multi-agent tracking', () => {
|
|
477
460
|
it('getActiveCodeAgents returns empty array initially', () => {
|
|
478
461
|
// Active agents are those currently running — initially none
|
|
@@ -498,161 +481,3 @@ describe('code_with_agent', () => {
|
|
|
498
481
|
});
|
|
499
482
|
});
|
|
500
483
|
});
|
|
501
|
-
describe('code_with_team', () => {
|
|
502
|
-
describe('tool definition', () => {
|
|
503
|
-
it('has correct name and required fields', () => {
|
|
504
|
-
expect(CODE_WITH_TEAM_TOOL.name).toBe('code_with_team');
|
|
505
|
-
expect(CODE_WITH_TEAM_TOOL.input_schema.required).toEqual(['task']);
|
|
506
|
-
});
|
|
507
|
-
it('has expected properties in schema (no max_turns)', () => {
|
|
508
|
-
const props = Object.keys(CODE_WITH_TEAM_TOOL.input_schema.properties);
|
|
509
|
-
expect(props).toContain('task');
|
|
510
|
-
expect(props).toContain('team_size');
|
|
511
|
-
expect(props).toContain('workdir');
|
|
512
|
-
expect(props).toContain('agent');
|
|
513
|
-
expect(props).toContain('model');
|
|
514
|
-
expect(props).toContain('timeout_minutes');
|
|
515
|
-
expect(props).toContain('validate');
|
|
516
|
-
// max_turns removed — not relevant for parallel agents
|
|
517
|
-
expect(props).not.toContain('max_turns');
|
|
518
|
-
});
|
|
519
|
-
it('is included in getToolDefinitions when includeSpawnSubagent is true', async () => {
|
|
520
|
-
const tools = await getToolDefinitions(toolConfig, { includeAgentTools: true });
|
|
521
|
-
expect(tools.map(t => t.name)).toContain('code_with_team');
|
|
522
|
-
});
|
|
523
|
-
it('is excluded from getToolDefinitions when includeSpawnSubagent is false', async () => {
|
|
524
|
-
const tools = await getToolDefinitions(toolConfig);
|
|
525
|
-
expect(tools.map(t => t.name)).not.toContain('code_with_team');
|
|
526
|
-
});
|
|
527
|
-
});
|
|
528
|
-
describe('executeTool routing', () => {
|
|
529
|
-
it('rejects workdir outside allowed paths', async () => {
|
|
530
|
-
const result = await executeTool('code_with_team', {
|
|
531
|
-
task: 'refactor auth',
|
|
532
|
-
workdir: '/tmp/not-allowed',
|
|
533
|
-
}, toolConfig);
|
|
534
|
-
expect(result).toContain('Error: Working directory not allowed');
|
|
535
|
-
});
|
|
536
|
-
it('returns error when task is missing', async () => {
|
|
537
|
-
const result = await executeTool('code_with_team', {}, toolConfig);
|
|
538
|
-
expect(result).toContain('Error: task is required');
|
|
539
|
-
});
|
|
540
|
-
it('returns error when agent is invalid', async () => {
|
|
541
|
-
const result = await executeTool('code_with_team', {
|
|
542
|
-
task: 'refactor auth',
|
|
543
|
-
agent: 'not-a-real-agent',
|
|
544
|
-
}, toolConfig);
|
|
545
|
-
expect(result).toContain('Error: Invalid agent');
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
});
|
|
549
|
-
describe('decomposeTask', () => {
|
|
550
|
-
it('falls back to numbered subtasks with DecomposedSubtask format on error', async () => {
|
|
551
|
-
// decomposeTask with no valid config will fail the model call and use fallback
|
|
552
|
-
const mockConfig = {
|
|
553
|
-
agents: { list: {} },
|
|
554
|
-
};
|
|
555
|
-
const subtasks = await decomposeTask('fix everything', 3, mockConfig);
|
|
556
|
-
expect(subtasks).toHaveLength(3);
|
|
557
|
-
expect(subtasks[0].description).toContain('Part 1 of 3');
|
|
558
|
-
expect(subtasks[1].description).toContain('Part 2 of 3');
|
|
559
|
-
expect(subtasks[2].description).toContain('Part 3 of 3');
|
|
560
|
-
expect(subtasks[0].description).toContain('fix everything');
|
|
561
|
-
// Fallback produces all-independent subtasks
|
|
562
|
-
expect(subtasks[0].dependsOn).toEqual([]);
|
|
563
|
-
expect(subtasks[1].dependsOn).toEqual([]);
|
|
564
|
-
expect(subtasks[2].dependsOn).toEqual([]);
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
describe('computeWaves', () => {
|
|
568
|
-
it('puts all independent subtasks in a single wave', () => {
|
|
569
|
-
const subtasks = [
|
|
570
|
-
{ description: 'task A', dependsOn: [] },
|
|
571
|
-
{ description: 'task B', dependsOn: [] },
|
|
572
|
-
{ description: 'task C', dependsOn: [] },
|
|
573
|
-
];
|
|
574
|
-
const waves = computeWaves(subtasks);
|
|
575
|
-
expect(waves).toHaveLength(1);
|
|
576
|
-
expect(waves[0]).toEqual([0, 1, 2]);
|
|
577
|
-
});
|
|
578
|
-
it('creates sequential waves for a dependency chain', () => {
|
|
579
|
-
const subtasks = [
|
|
580
|
-
{ description: 'schema', dependsOn: [] },
|
|
581
|
-
{ description: 'queries', dependsOn: [0] },
|
|
582
|
-
{ description: 'tests', dependsOn: [1] },
|
|
583
|
-
];
|
|
584
|
-
const waves = computeWaves(subtasks);
|
|
585
|
-
expect(waves).toHaveLength(3);
|
|
586
|
-
expect(waves[0]).toEqual([0]);
|
|
587
|
-
expect(waves[1]).toEqual([1]);
|
|
588
|
-
expect(waves[2]).toEqual([2]);
|
|
589
|
-
});
|
|
590
|
-
it('groups tasks with shared dependency into the same wave', () => {
|
|
591
|
-
const subtasks = [
|
|
592
|
-
{ description: 'schema', dependsOn: [] },
|
|
593
|
-
{ description: 'API endpoints', dependsOn: [0] },
|
|
594
|
-
{ description: 'admin endpoints', dependsOn: [0] },
|
|
595
|
-
{ description: 'integration tests', dependsOn: [1, 2] },
|
|
596
|
-
];
|
|
597
|
-
const waves = computeWaves(subtasks);
|
|
598
|
-
expect(waves).toHaveLength(3);
|
|
599
|
-
expect(waves[0]).toEqual([0]);
|
|
600
|
-
expect(waves[1].sort()).toEqual([1, 2]);
|
|
601
|
-
expect(waves[2]).toEqual([3]);
|
|
602
|
-
});
|
|
603
|
-
it('handles mixed independent and dependent tasks', () => {
|
|
604
|
-
const subtasks = [
|
|
605
|
-
{ description: 'setup types', dependsOn: [] },
|
|
606
|
-
{ description: 'write docs', dependsOn: [] },
|
|
607
|
-
{ description: 'implement using types', dependsOn: [0] },
|
|
608
|
-
];
|
|
609
|
-
const waves = computeWaves(subtasks);
|
|
610
|
-
expect(waves).toHaveLength(2);
|
|
611
|
-
expect(waves[0].sort()).toEqual([0, 1]);
|
|
612
|
-
expect(waves[1]).toEqual([2]);
|
|
613
|
-
});
|
|
614
|
-
it('handles dependency cycles by forcing remaining into current wave', () => {
|
|
615
|
-
const subtasks = [
|
|
616
|
-
{ description: 'A depends on B', dependsOn: [1] },
|
|
617
|
-
{ description: 'B depends on A', dependsOn: [0] },
|
|
618
|
-
];
|
|
619
|
-
const waves = computeWaves(subtasks);
|
|
620
|
-
// Both should end up in a wave despite the cycle
|
|
621
|
-
const allIndices = waves.flat().sort();
|
|
622
|
-
expect(allIndices).toEqual([0, 1]);
|
|
623
|
-
});
|
|
624
|
-
it('handles single subtask', () => {
|
|
625
|
-
const subtasks = [
|
|
626
|
-
{ description: 'only task', dependsOn: [] },
|
|
627
|
-
];
|
|
628
|
-
const waves = computeWaves(subtasks);
|
|
629
|
-
expect(waves).toHaveLength(1);
|
|
630
|
-
expect(waves[0]).toEqual([0]);
|
|
631
|
-
});
|
|
632
|
-
it('handles empty subtask list', () => {
|
|
633
|
-
const waves = computeWaves([]);
|
|
634
|
-
expect(waves).toHaveLength(0);
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
describe('synthesizeResults', () => {
|
|
638
|
-
it('falls back to mechanical summary on error', async () => {
|
|
639
|
-
const mockConfig = {
|
|
640
|
-
agents: { list: {} },
|
|
641
|
-
};
|
|
642
|
-
const results = [
|
|
643
|
-
{ subtask: 'fix auth', status: 'completed', output: 'done' },
|
|
644
|
-
{ subtask: 'fix tests', status: 'failed', error: 'timeout' },
|
|
645
|
-
];
|
|
646
|
-
const summary = await synthesizeResults('fix everything', results, mockConfig);
|
|
647
|
-
expect(summary).toContain('1/2 subtasks succeeded');
|
|
648
|
-
expect(summary).toContain('1 failed');
|
|
649
|
-
expect(summary).toContain('fix auth');
|
|
650
|
-
expect(summary).toContain('fix tests');
|
|
651
|
-
});
|
|
652
|
-
});
|
|
653
|
-
describe('readTeamState (deprecated)', () => {
|
|
654
|
-
it('always returns null', () => {
|
|
655
|
-
const state = readTeamState();
|
|
656
|
-
expect(state).toBeNull();
|
|
657
|
-
});
|
|
658
|
-
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateBearerToken } from '../utils.js';
|
|
3
|
+
describe('validateBearerToken', () => {
|
|
4
|
+
it('accepts valid bearer token', () => {
|
|
5
|
+
expect(validateBearerToken('abc123', 'Bearer abc123')).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
it('rejects invalid bearer token', () => {
|
|
8
|
+
expect(validateBearerToken('abc123', 'Bearer nope')).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('rejects missing or malformed auth headers', () => {
|
|
11
|
+
expect(validateBearerToken('abc123', undefined)).toBe(false);
|
|
12
|
+
expect(validateBearerToken('abc123', 'Basic abc123')).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -3,6 +3,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
3
3
|
const { mockAudioSpeechCreate } = vi.hoisted(() => ({
|
|
4
4
|
mockAudioSpeechCreate: vi.fn(),
|
|
5
5
|
}));
|
|
6
|
+
const { mockSpawnSync } = vi.hoisted(() => ({
|
|
7
|
+
mockSpawnSync: vi.fn(() => ({ status: 0, stdout: '', stderr: '' })),
|
|
8
|
+
}));
|
|
6
9
|
// Mock openai — use a class so `new OpenAI()` works correctly in ESM mocking context
|
|
7
10
|
vi.mock('openai', () => ({
|
|
8
11
|
default: class MockOpenAI {
|
|
@@ -17,6 +20,7 @@ vi.mock('openai', () => ({
|
|
|
17
20
|
// Mock child_process — execSync used by macOS say provider
|
|
18
21
|
vi.mock('child_process', () => ({
|
|
19
22
|
execSync: vi.fn(() => Buffer.from('')),
|
|
23
|
+
spawnSync: (...args) => mockSpawnSync(...args),
|
|
20
24
|
}));
|
|
21
25
|
// Mock fs to avoid actual disk I/O
|
|
22
26
|
vi.mock('fs', async (importOriginal) => {
|
|
@@ -183,6 +187,23 @@ describe('synthesizeSpeech', () => {
|
|
|
183
187
|
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
184
188
|
}
|
|
185
189
|
});
|
|
190
|
+
it('uses non-shell args for macOS say + ffmpeg', async () => {
|
|
191
|
+
const originalPlatform = process.platform;
|
|
192
|
+
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
|
193
|
+
try {
|
|
194
|
+
const config = {
|
|
195
|
+
...baseVoiceConfig,
|
|
196
|
+
providers: { macos: { tts: { voice: "Bad'Voice; rm -rf /" } } },
|
|
197
|
+
};
|
|
198
|
+
const result = await synthesizeSpeech("hello'; say hacked", config);
|
|
199
|
+
expect(result.format).toBe('ogg');
|
|
200
|
+
expect(mockSpawnSync).toHaveBeenNthCalledWith(1, 'say', ['-v', "Bad'Voice; rm -rf /", '-o', expect.stringContaining('.aiff'), "hello'; say hacked"], expect.any(Object));
|
|
201
|
+
expect(mockSpawnSync).toHaveBeenNthCalledWith(2, 'ffmpeg', ['-i', expect.stringContaining('.aiff'), '-c:a', 'libopus', expect.stringContaining('.ogg'), '-y'], expect.any(Object));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
186
207
|
it('resolves API key from environment variable syntax', async () => {
|
|
187
208
|
const fakeBuffer = Buffer.from('fake-audio');
|
|
188
209
|
mockAudioSpeechCreate.mockResolvedValueOnce({
|
package/dist/agent.js
CHANGED
|
@@ -80,6 +80,12 @@ export function appendToMemory(agentId, entry) {
|
|
|
80
80
|
// Langfuse app tagging
|
|
81
81
|
const LANGFUSE_APP_NAME = 'skimpyclaw';
|
|
82
82
|
const LANGFUSE_APP_TAG = 'app:skimpyclaw';
|
|
83
|
+
const THINKING_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh']);
|
|
84
|
+
function metadataThinking(value) {
|
|
85
|
+
return typeof value === 'string' && THINKING_LEVELS.has(value)
|
|
86
|
+
? value
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
83
89
|
export async function runAgentTurn(agentId, userMessage, config, modelOverride, toolConfig, history, context) {
|
|
84
90
|
const agentConfig = config.agents.list[agentId];
|
|
85
91
|
if (!agentConfig) {
|
|
@@ -94,11 +100,27 @@ export async function runAgentTurn(agentId, userMessage, config, modelOverride,
|
|
|
94
100
|
// Inject channel-specific formatting context
|
|
95
101
|
if (context?.channel) {
|
|
96
102
|
const channelHints = {
|
|
97
|
-
telegram: `\n\n##
|
|
98
|
-
discord: `\n\n##
|
|
103
|
+
telegram: `\n\n## Channel: Telegram\nPlain text only. No markdown. Use CAPS for emphasis, plain URLs.`,
|
|
104
|
+
discord: `\n\n## Channel: Discord\nUse markdown: **bold**, *italic*, \`code\`, \`\`\`blocks\`\`\`, [links](url).`,
|
|
99
105
|
};
|
|
100
106
|
systemPrompt += channelHints[context.channel] || '';
|
|
101
107
|
}
|
|
108
|
+
const metadata = context?.metadata;
|
|
109
|
+
const threadAgentAlias = typeof metadata?.threadAgentAlias === 'string'
|
|
110
|
+
? metadata.threadAgentAlias.trim()
|
|
111
|
+
: '';
|
|
112
|
+
const threadAgentPrompt = typeof metadata?.threadAgentPromptOverlay === 'string'
|
|
113
|
+
? metadata.threadAgentPromptOverlay.trim()
|
|
114
|
+
: '';
|
|
115
|
+
if (threadAgentAlias || threadAgentPrompt) {
|
|
116
|
+
systemPrompt += `\n\n## Discord Thread Agent`;
|
|
117
|
+
if (threadAgentAlias) {
|
|
118
|
+
systemPrompt += `\nAlias: ${threadAgentAlias}`;
|
|
119
|
+
}
|
|
120
|
+
if (threadAgentPrompt) {
|
|
121
|
+
systemPrompt += `\nFollow this additional thread-specific prompt:\n${threadAgentPrompt}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
102
124
|
// Build user content — support both string and content arrays (for images)
|
|
103
125
|
let userContent;
|
|
104
126
|
let sanitizedMessage;
|
|
@@ -123,7 +145,10 @@ export async function runAgentTurn(agentId, userMessage, config, modelOverride,
|
|
|
123
145
|
{ role: 'user', content: userContent },
|
|
124
146
|
];
|
|
125
147
|
const model = modelOverride || agentConfig.model;
|
|
126
|
-
const
|
|
148
|
+
const thinking = metadataThinking(metadata?.threadAgentThinking)
|
|
149
|
+
?? metadataThinking(metadata?.thinkingOverride)
|
|
150
|
+
?? agentConfig.thinking;
|
|
151
|
+
const chatOptions = { model, thinking };
|
|
127
152
|
const route = resolveProviderRoute(model, config);
|
|
128
153
|
const { resolvedModel, provider, modelId } = route;
|
|
129
154
|
let response = '';
|
|
@@ -152,9 +177,15 @@ export async function runAgentTurn(agentId, userMessage, config, modelOverride,
|
|
|
152
177
|
channelTargetId,
|
|
153
178
|
approverUserId: context?.userId,
|
|
154
179
|
approverUsername: context?.metadata?.username,
|
|
155
|
-
sandboxConfig: config.sandbox,
|
|
156
180
|
sessionId: context?.sessionId || String(chatIdNum ?? 'default'),
|
|
157
181
|
isCronJob: context?.metadata?.isCronJob === true,
|
|
182
|
+
discordThreadId: context?.metadata?.discordThreadId,
|
|
183
|
+
discordChannelId: context?.metadata?.discordChannelId,
|
|
184
|
+
isDm: context?.metadata?.isDm === true,
|
|
185
|
+
threadAgentAlias,
|
|
186
|
+
delegationDepth: typeof context?.metadata?.delegationDepth === 'number'
|
|
187
|
+
? (context?.metadata).delegationDepth
|
|
188
|
+
: 0,
|
|
158
189
|
};
|
|
159
190
|
const runTurn = async () => {
|
|
160
191
|
if (toolConfig?.enabled) {
|