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
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,
|
|
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,
|
|
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,
|
|
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
|
|
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 +
|
|
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
|
|
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
|
-
//
|
|
198
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
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?:
|
|
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?:
|
|
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
|
|
67
|
-
const
|
|
68
|
-
|
|
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:
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
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
|
}
|