skimpyclaw 0.3.6 → 0.3.8
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 +14 -6
- package/dist/__tests__/api.test.js +1 -0
- package/dist/__tests__/channels.test.js +1 -1
- package/dist/__tests__/code-agents-orchestrator.test.js +74 -7
- package/dist/__tests__/code-agents-sandbox.test.d.ts +1 -0
- package/dist/__tests__/code-agents-sandbox.test.js +163 -0
- package/dist/__tests__/context-manager.test.d.ts +1 -0
- package/dist/__tests__/context-manager.test.js +236 -0
- package/dist/__tests__/package-manager-detection.test.js +5 -5
- package/dist/__tests__/setup.test.js +7 -5
- package/dist/__tests__/skills.test.js +2 -2
- package/dist/__tests__/structured-context.test.d.ts +1 -0
- package/dist/__tests__/structured-context.test.js +100 -0
- package/dist/__tests__/tools.test.js +65 -3
- package/dist/agent.js +4 -5
- package/dist/api.js +10 -58
- package/dist/audit.js +5 -51
- package/dist/channels/telegram/handlers.js +2 -60
- package/dist/channels/telegram/index.js +0 -7
- package/dist/channels.js +1 -1
- package/dist/cli.js +151 -16
- package/dist/code-agents/executor.d.ts +9 -4
- package/dist/code-agents/executor.js +187 -13
- package/dist/code-agents/index.d.ts +1 -1
- package/dist/code-agents/index.js +23 -21
- package/dist/code-agents/orchestrator.d.ts +8 -2
- package/dist/code-agents/orchestrator.js +297 -27
- package/dist/code-agents/structured-context.d.ts +7 -0
- package/dist/code-agents/structured-context.js +54 -0
- package/dist/code-agents/types.d.ts +2 -0
- package/dist/code-agents/utils.js +12 -2
- package/dist/code-agents/worktree.d.ts +40 -0
- package/dist/code-agents/worktree.js +215 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -3
- package/dist/cron.js +18 -4
- package/dist/dashboard/assets/{index-CkonC7Cd.js → index-BoTHPby4.js} +20 -20
- package/dist/dashboard/assets/{index-EAg6lqF5.css → index-D4mufvBg.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/discord.js +4 -40
- package/dist/exec-approval.js +1 -1
- package/dist/file-lock.js +1 -1
- package/dist/gateway.js +3 -10
- package/dist/providers/anthropic.js +9 -5
- package/dist/providers/codex.js +10 -6
- package/dist/providers/context-manager.d.ts +22 -0
- package/dist/providers/context-manager.js +100 -0
- package/dist/providers/openai.js +9 -5
- package/dist/providers/types.d.ts +1 -0
- package/dist/security.js +9 -0
- package/dist/setup.js +112 -14
- package/dist/skills.js +9 -2
- package/dist/subagent.js +33 -2
- package/dist/tools/bash-tool.js +8 -0
- package/dist/tools/browser-tool.js +2 -1
- package/dist/tools/definitions.d.ts +0 -27
- package/dist/tools/definitions.js +0 -18
- package/dist/tools/execute-context.d.ts +4 -4
- package/dist/tools/file-tools.d.ts +1 -1
- package/dist/tools/file-tools.js +1 -1
- package/dist/tools.d.ts +5 -5
- package/dist/tools.js +87 -98
- package/dist/types.d.ts +14 -22
- package/dist/usage.d.ts +1 -0
- package/dist/usage.js +30 -46
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +71 -0
- package/dist/voice.js +9 -7
- package/package.json +1 -1
package/dist/tools.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import type { ToolConfig } from './types.js';
|
|
2
|
-
import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS,
|
|
2
|
+
import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL } from './tools/definitions.js';
|
|
3
3
|
import type { ExecuteToolContext } from './tools/execute-context.js';
|
|
4
4
|
import { cleanupBrowser } from './tools/browser-tool.js';
|
|
5
5
|
export { getActiveCodeAgents, getRecentCodeAgents, getAllCodeAgents, getCodeAgent, cancelCodeAgent, restoreCodeAgentTasks, setCodeAgentConfig, computeWaves, decomposeTask, synthesizeResults, runValidation, runCodeAgentBackground, buildCodeAgentArgs, readTeamState, parseStreamJsonForLive, } from './code-agents/index.js';
|
|
6
|
-
export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS,
|
|
6
|
+
export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, cleanupBrowser, };
|
|
7
7
|
export type { ExecuteToolContext };
|
|
8
8
|
export type { CodeAgentTask, DecomposedSubtask } from './code-agents/index.js';
|
|
9
9
|
export declare function discoverMcpTools(): Promise<any[]>;
|
|
10
10
|
export declare function clearMcpToolCache(): void;
|
|
11
11
|
/**
|
|
12
|
-
* Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) +
|
|
12
|
+
* Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) + agent tools.
|
|
13
13
|
* This is the primary way to get tools — replaces the static TOOL_DEFINITIONS export.
|
|
14
|
-
* Pass
|
|
14
|
+
* Pass includeAgentTools: true to include code_with_agent, code_with_team, check_code_agent.
|
|
15
15
|
* Results are cached for 60s to avoid rebuilding the array on every agent turn.
|
|
16
16
|
*/
|
|
17
17
|
export declare function getToolDefinitions(config?: ToolConfig, options?: {
|
|
18
|
-
|
|
18
|
+
includeAgentTools?: boolean;
|
|
19
19
|
includeMcp?: boolean;
|
|
20
20
|
projects?: Record<string, string>;
|
|
21
21
|
}): Promise<any[]>;
|
package/dist/tools.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Tool definitions and executors for Anthropic API tool_use
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { TTLCache } from './cache.js';
|
|
5
|
-
import {
|
|
5
|
+
import { toErrorMessage } from './utils.js';
|
|
6
|
+
import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, } from './tools/definitions.js';
|
|
6
7
|
import { executeReadFile, executeWriteFileLocked, executeListDirectory } from './tools/file-tools.js';
|
|
7
8
|
import { executeBash } from './tools/bash-tool.js';
|
|
8
9
|
import { executeBrowser, cleanupBrowser } from './tools/browser-tool.js';
|
|
9
10
|
import { ensureContainer, SANDBOX_DEFAULTS, translatePath, validateMountPaths } from './sandbox/index.js';
|
|
10
11
|
import { sandboxBash, sandboxReadFile, sandboxWriteFile, sandboxListDir, sandboxGlob } from './sandbox/index.js';
|
|
12
|
+
import { isBashCommandSafe } from './security.js';
|
|
13
|
+
import { classifyCommandRisk, requiresApproval } from './exec-approval.js';
|
|
11
14
|
// Re-export from code-agents module for backward compatibility
|
|
12
15
|
export {
|
|
13
16
|
// Registry functions
|
|
@@ -23,7 +26,7 @@ buildCodeAgentArgs, readTeamState,
|
|
|
23
26
|
// Parsers
|
|
24
27
|
parseStreamJsonForLive, } from './code-agents/index.js';
|
|
25
28
|
// Re-export from definitions
|
|
26
|
-
export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS,
|
|
29
|
+
export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, cleanupBrowser, };
|
|
27
30
|
// --- MCP (mcporter) ---
|
|
28
31
|
let mcpRuntime = null;
|
|
29
32
|
async function getMcpRuntime() {
|
|
@@ -82,83 +85,78 @@ export function clearMcpToolCache() {
|
|
|
82
85
|
discoveredMcpTools = null;
|
|
83
86
|
}
|
|
84
87
|
const toolDefsCache = new TTLCache(60_000);
|
|
88
|
+
/** Inject project names into a tool's workdir description, or return the tool unchanged. */
|
|
89
|
+
function injectProjects(tool, projects) {
|
|
90
|
+
if (!projects || Object.keys(projects).length === 0)
|
|
91
|
+
return tool;
|
|
92
|
+
const projectList = Object.entries(projects)
|
|
93
|
+
.map(([name, path]) => `"${name}" → ${path}`)
|
|
94
|
+
.join(', ');
|
|
95
|
+
return {
|
|
96
|
+
...tool,
|
|
97
|
+
input_schema: {
|
|
98
|
+
...tool.input_schema,
|
|
99
|
+
properties: {
|
|
100
|
+
...tool.input_schema.properties,
|
|
101
|
+
workdir: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: `Working directory or project name. Named projects: ${projectList}. Default: SkimpyClaw repo root.`,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
85
109
|
/**
|
|
86
|
-
* Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) +
|
|
110
|
+
* Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) + agent tools.
|
|
87
111
|
* This is the primary way to get tools — replaces the static TOOL_DEFINITIONS export.
|
|
88
|
-
* Pass
|
|
112
|
+
* Pass includeAgentTools: true to include code_with_agent, code_with_team, check_code_agent.
|
|
89
113
|
* Results are cached for 60s to avoid rebuilding the array on every agent turn.
|
|
90
114
|
*/
|
|
91
115
|
export async function getToolDefinitions(config, options) {
|
|
92
116
|
const includeMcp = options?.includeMcp !== false; // default true for backwards compat
|
|
117
|
+
const profile = config?.toolProfile ?? 'full';
|
|
93
118
|
const cacheKey = JSON.stringify({
|
|
94
119
|
browser: config?.browser?.enabled,
|
|
95
|
-
|
|
120
|
+
agentTools: options?.includeAgentTools,
|
|
96
121
|
mcp: includeMcp,
|
|
97
122
|
projects: options?.projects,
|
|
123
|
+
profile,
|
|
98
124
|
});
|
|
99
125
|
const cached = toolDefsCache.get(cacheKey);
|
|
100
126
|
if (cached)
|
|
101
127
|
return cached;
|
|
102
128
|
const tools = [...BUILTIN_TOOL_DEFINITIONS];
|
|
129
|
+
// Minimal profile: only the 4 built-in tools (Read, Write, Glob, Bash).
|
|
130
|
+
// Used by orchestrator decompose/synthesize calls.
|
|
131
|
+
if (profile === 'minimal') {
|
|
132
|
+
toolDefsCache.set(cacheKey, tools);
|
|
133
|
+
return tools;
|
|
134
|
+
}
|
|
103
135
|
// Include browser tool only when explicitly enabled
|
|
104
136
|
if (config?.browser?.enabled) {
|
|
105
137
|
tools.push(BROWSER_TOOL_DEFINITION);
|
|
106
138
|
}
|
|
139
|
+
// Coding profile: built-ins + browser + code_with_agent + check_code_agent.
|
|
140
|
+
// Skips MCP discovery and code_with_team.
|
|
141
|
+
if (profile === 'coding') {
|
|
142
|
+
if (options?.includeAgentTools) {
|
|
143
|
+
tools.push(injectProjects(CODE_WITH_AGENT_TOOL, options.projects));
|
|
144
|
+
tools.push(CHECK_CODE_AGENT_TOOL);
|
|
145
|
+
}
|
|
146
|
+
toolDefsCache.set(cacheKey, tools);
|
|
147
|
+
return tools;
|
|
148
|
+
}
|
|
149
|
+
// Full profile (default): everything including MCP and code_with_team.
|
|
107
150
|
// Auto-discover MCP tools from mcporter config (only for Anthropic models)
|
|
108
151
|
if (includeMcp) {
|
|
109
152
|
const mcpTools = await discoverMcpTools();
|
|
110
153
|
tools.push(...mcpTools);
|
|
111
154
|
}
|
|
112
|
-
// Include
|
|
113
|
-
if (options?.
|
|
114
|
-
tools.push(SPAWN_SUBAGENT_TOOL);
|
|
115
|
-
// Inject project names into code_with_agent description so the model knows what to use
|
|
155
|
+
// Include code_with_agent, code_with_team, and check_code_agent when requested
|
|
156
|
+
if (options?.includeAgentTools) {
|
|
116
157
|
const projects = options.projects;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.map(([name, path]) => `"${name}" → ${path}`)
|
|
120
|
-
.join(', ');
|
|
121
|
-
const codeAgentWithProjects = {
|
|
122
|
-
...CODE_WITH_AGENT_TOOL,
|
|
123
|
-
input_schema: {
|
|
124
|
-
...CODE_WITH_AGENT_TOOL.input_schema,
|
|
125
|
-
properties: {
|
|
126
|
-
...CODE_WITH_AGENT_TOOL.input_schema.properties,
|
|
127
|
-
workdir: {
|
|
128
|
-
type: 'string',
|
|
129
|
-
description: `Working directory or project name. Named projects: ${projectList}. Default: SkimpyClaw repo root.`,
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
tools.push(codeAgentWithProjects);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
tools.push(CODE_WITH_AGENT_TOOL);
|
|
138
|
-
}
|
|
139
|
-
// Inject project names into code_with_team description too
|
|
140
|
-
if (projects && Object.keys(projects).length > 0) {
|
|
141
|
-
const projectList = Object.entries(projects)
|
|
142
|
-
.map(([name, path]) => `"${name}" → ${path}`)
|
|
143
|
-
.join(', ');
|
|
144
|
-
const codeTeamWithProjects = {
|
|
145
|
-
...CODE_WITH_TEAM_TOOL,
|
|
146
|
-
input_schema: {
|
|
147
|
-
...CODE_WITH_TEAM_TOOL.input_schema,
|
|
148
|
-
properties: {
|
|
149
|
-
...CODE_WITH_TEAM_TOOL.input_schema.properties,
|
|
150
|
-
workdir: {
|
|
151
|
-
type: 'string',
|
|
152
|
-
description: `Working directory or project name. Named projects: ${projectList}. Default: SkimpyClaw repo root.`,
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
tools.push(codeTeamWithProjects);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
tools.push(CODE_WITH_TEAM_TOOL);
|
|
161
|
-
}
|
|
158
|
+
tools.push(injectProjects(CODE_WITH_AGENT_TOOL, projects));
|
|
159
|
+
tools.push(injectProjects(CODE_WITH_TEAM_TOOL, projects));
|
|
162
160
|
tools.push(CHECK_CODE_AGENT_TOOL);
|
|
163
161
|
}
|
|
164
162
|
toolDefsCache.set(cacheKey, tools);
|
|
@@ -250,8 +248,6 @@ export async function executeTool(name, input, config, context) {
|
|
|
250
248
|
const { isPathAllowed } = await import('./tools/path-utils.js');
|
|
251
249
|
for (const [key, value] of Object.entries(input)) {
|
|
252
250
|
if (typeof value === 'string' && (value.startsWith('/') || value.startsWith('~/') || value.startsWith('./'))) {
|
|
253
|
-
const { resolve } = await import('path');
|
|
254
|
-
const { homedir } = await import('os');
|
|
255
251
|
const resolved = value.startsWith('~/') ? resolve(homedir(), value.slice(2)) : resolve(value);
|
|
256
252
|
if (!isPathAllowed(resolved, config.allowedPaths)) {
|
|
257
253
|
return `Error: MCP tool argument "${key}" references path outside allowed directories: ${value}`;
|
|
@@ -261,10 +257,6 @@ export async function executeTool(name, input, config, context) {
|
|
|
261
257
|
}
|
|
262
258
|
return await executeMcpToolGeneric(name, input);
|
|
263
259
|
}
|
|
264
|
-
// Route spawn_subagent
|
|
265
|
-
if (name === 'spawn_subagent') {
|
|
266
|
-
return await executeSpawnSubagent(input, context);
|
|
267
|
-
}
|
|
268
260
|
// Route code_with_agent - delegate to code-agents module
|
|
269
261
|
if (name === 'code_with_agent') {
|
|
270
262
|
const { executeCodeWithAgent } = await import('./code-agents/index.js');
|
|
@@ -330,8 +322,39 @@ export async function executeTool(name, input, config, context) {
|
|
|
330
322
|
return reversed;
|
|
331
323
|
};
|
|
332
324
|
switch (normalized) {
|
|
333
|
-
case 'bash':
|
|
334
|
-
|
|
325
|
+
case 'bash': {
|
|
326
|
+
const translatedCmd = translateBashPaths(input.command);
|
|
327
|
+
// Apply hard safety blocks and exec-approval inside sandbox.
|
|
328
|
+
// The sandbox provides filesystem isolation but not command-level policy.
|
|
329
|
+
if (!isBashCommandSafe(translatedCmd)) {
|
|
330
|
+
return 'Error: Command blocked by safety filter.';
|
|
331
|
+
}
|
|
332
|
+
const classification = classifyCommandRisk(translatedCmd);
|
|
333
|
+
const approvalConfig = config.execApproval;
|
|
334
|
+
if (requiresApproval(classification, approvalConfig)) {
|
|
335
|
+
const isUnattended = context?.channel === 'subagent' ||
|
|
336
|
+
context?.isCronJob === true ||
|
|
337
|
+
(!context?.approverUserId && !context?.channelTargetId && !context?.chatId);
|
|
338
|
+
if (isUnattended) {
|
|
339
|
+
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.`;
|
|
340
|
+
}
|
|
341
|
+
// Attended context — run full approval flow before executing in sandbox
|
|
342
|
+
const { createApprovalRequest: sbxCreate, waitForApproval: sbxWait } = await import('./exec-approval.js');
|
|
343
|
+
const ttlMs = approvalConfig?.ttlMs ?? 5 * 60 * 1000;
|
|
344
|
+
const channelMeta = context?.channel ? {
|
|
345
|
+
channel: context.channel,
|
|
346
|
+
chatId: context.channelTargetId ?? context.chatId,
|
|
347
|
+
userId: context.approverUserId,
|
|
348
|
+
username: context.approverUsername,
|
|
349
|
+
} : context?.chatId ? { channel: 'telegram', chatId: context.chatId } : undefined;
|
|
350
|
+
const sbxReq = sbxCreate(translatedCmd, input.cwd, classification, approvalConfig, channelMeta);
|
|
351
|
+
const sbxResolved = await sbxWait(sbxReq.id, ttlMs);
|
|
352
|
+
if (sbxResolved.status !== 'approved') {
|
|
353
|
+
return `⛔ Command not executed — approval ${sbxResolved.status} (tier ${classification.tier}: ${classification.reason}).`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return await sandboxBash(containerName, translatedCmd, input.cwd ? tp(input.cwd) : undefined, config.bashTimeout);
|
|
357
|
+
}
|
|
335
358
|
case 'read_file':
|
|
336
359
|
return await sandboxReadFile(containerName, tp(input.file_path || input.path));
|
|
337
360
|
case 'write_file':
|
|
@@ -365,41 +388,7 @@ export async function executeTool(name, input, config, context) {
|
|
|
365
388
|
}
|
|
366
389
|
}
|
|
367
390
|
catch (err) {
|
|
368
|
-
return `Error: ${
|
|
391
|
+
return `Error: ${toErrorMessage(err)}`;
|
|
369
392
|
}
|
|
370
393
|
}
|
|
371
394
|
// --- Individual Tool Implementations ---
|
|
372
|
-
/**
|
|
373
|
-
* Execute spawn_subagent tool — dispatches a background subagent.
|
|
374
|
-
*/
|
|
375
|
-
async function executeSpawnSubagent(input, context) {
|
|
376
|
-
if (!context?.fullConfig || !context?.chatId) {
|
|
377
|
-
return 'Error: spawn_subagent requires a chat context (not available in this mode)';
|
|
378
|
-
}
|
|
379
|
-
const { dispatchSubagent } = await import('./subagent.js');
|
|
380
|
-
const task = input.task;
|
|
381
|
-
const type = input.type;
|
|
382
|
-
const model = input.model;
|
|
383
|
-
const label = input.label;
|
|
384
|
-
// NOTE: allowedPaths deliberately NOT accepted from model input (security — prevents path escalation).
|
|
385
|
-
// Subagents inherit their preset's allowedPaths from config.
|
|
386
|
-
if (!task || !type) {
|
|
387
|
-
return 'Error: task and type are required';
|
|
388
|
-
}
|
|
389
|
-
if (!['coding', 'research'].includes(type)) {
|
|
390
|
-
return `Error: Invalid type "${type}". Must be coding or research.`;
|
|
391
|
-
}
|
|
392
|
-
try {
|
|
393
|
-
const subagentTask = dispatchSubagent(type, task, context.chatId, context.fullConfig, model, context.history, { label });
|
|
394
|
-
const labelStr = label ? ` "${label}"` : '';
|
|
395
|
-
return JSON.stringify({
|
|
396
|
-
status: 'accepted',
|
|
397
|
-
runId: subagentTask.id,
|
|
398
|
-
label: label || subagentTask.type,
|
|
399
|
-
message: `Subagent ${subagentTask.id}${labelStr} dispatched (${subagentTask.type}, model: ${subagentTask.model}). Results will be announced when done.`,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
catch (err) {
|
|
403
|
-
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
404
|
-
}
|
|
405
|
-
}
|
package/dist/types.d.ts
CHANGED
|
@@ -60,10 +60,16 @@ export interface Config {
|
|
|
60
60
|
token?: string;
|
|
61
61
|
frontend?: 'framework';
|
|
62
62
|
};
|
|
63
|
-
|
|
63
|
+
codeAgents?: {
|
|
64
64
|
maxConcurrent?: number;
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
defaultAgent?: string;
|
|
66
|
+
timeoutMinutes?: number;
|
|
67
|
+
teamTimeoutMinutes?: number;
|
|
68
|
+
maxTurns?: number;
|
|
69
|
+
skipPlaywright?: boolean;
|
|
70
|
+
/** Per-project validation commands. Keys match project names from `projects` config.
|
|
71
|
+
* Values are shell commands run in the project dir. Overrides auto-detected build+test. */
|
|
72
|
+
validationCommands?: Record<string, string>;
|
|
67
73
|
};
|
|
68
74
|
langfuse?: {
|
|
69
75
|
enabled?: boolean;
|
|
@@ -150,6 +156,11 @@ export interface ToolConfig {
|
|
|
150
156
|
maxIterations?: number;
|
|
151
157
|
bashTimeout?: number;
|
|
152
158
|
maxTurnTokens?: number;
|
|
159
|
+
toolProfile?: 'minimal' | 'coding' | 'full';
|
|
160
|
+
contextManagement?: {
|
|
161
|
+
enabled?: boolean;
|
|
162
|
+
maxContextTokens?: number;
|
|
163
|
+
};
|
|
153
164
|
browser?: {
|
|
154
165
|
enabled?: boolean;
|
|
155
166
|
type?: 'chromium' | 'firefox' | 'webkit';
|
|
@@ -200,25 +211,6 @@ export interface ModelProvider {
|
|
|
200
211
|
name: string;
|
|
201
212
|
chat(messages: ChatMessage[], options: ChatOptions): Promise<string>;
|
|
202
213
|
}
|
|
203
|
-
export type SubagentType = 'coding' | 'research';
|
|
204
|
-
export type SubagentStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
205
|
-
export interface SubagentTask {
|
|
206
|
-
id: string;
|
|
207
|
-
type: SubagentType;
|
|
208
|
-
prompt: string;
|
|
209
|
-
status: SubagentStatus;
|
|
210
|
-
chatId: number;
|
|
211
|
-
model: string;
|
|
212
|
-
label?: string;
|
|
213
|
-
createdAt: Date;
|
|
214
|
-
startedAt?: Date;
|
|
215
|
-
completedAt?: Date;
|
|
216
|
-
result?: string;
|
|
217
|
-
error?: string;
|
|
218
|
-
retryCount?: number;
|
|
219
|
-
maxRetries?: number;
|
|
220
|
-
abortController: AbortController;
|
|
221
|
-
}
|
|
222
214
|
export type ImageContentBlock = {
|
|
223
215
|
type: 'image';
|
|
224
216
|
source: {
|
package/dist/usage.d.ts
CHANGED
|
@@ -72,5 +72,6 @@ export declare function readUsageRecords(options?: ReadUsageOptions): {
|
|
|
72
72
|
export declare function aggregateUsage(startDate: string, endDate: string): UsageAggregation;
|
|
73
73
|
/**
|
|
74
74
|
* Get usage summary for today, last 7 days, and last 30 days.
|
|
75
|
+
* Reads files once for the full 30-day window, then partitions in memory.
|
|
75
76
|
*/
|
|
76
77
|
export declare function getUsageSummary(): UsageSummary;
|
package/dist/usage.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
// Usage tracking — JSONL append-only storage at ~/.skimpyclaw/logs/usage/YYYY-MM-DD.jsonl
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
+
import { formatDate, readJsonlDir } from './utils.js';
|
|
6
7
|
const USAGE_DIR = join(homedir(), '.skimpyclaw', 'logs', 'usage');
|
|
7
|
-
function formatDate(date) {
|
|
8
|
-
const y = date.getFullYear();
|
|
9
|
-
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
10
|
-
const d = String(date.getDate()).padStart(2, '0');
|
|
11
|
-
return `${y}-${m}-${d}`;
|
|
12
|
-
}
|
|
13
|
-
function getUsageFilePath(dateStr) {
|
|
14
|
-
return join(USAGE_DIR, `${dateStr}.jsonl`);
|
|
15
|
-
}
|
|
16
8
|
/** For testing: override the usage directory */
|
|
17
9
|
let usageDirOverride = null;
|
|
18
10
|
export function setUsageDirForTesting(dir) {
|
|
@@ -67,45 +59,15 @@ export function buildUsageRecord(opts) {
|
|
|
67
59
|
*/
|
|
68
60
|
export function readUsageRecords(options = {}) {
|
|
69
61
|
const dir = getUsageDir();
|
|
70
|
-
if (!existsSync(dir)) {
|
|
71
|
-
return { records: [], total: 0 };
|
|
72
|
-
}
|
|
73
62
|
const limit = options.limit ?? 50;
|
|
74
63
|
const offset = options.offset ?? 0;
|
|
75
64
|
const now = new Date();
|
|
76
65
|
const endDate = options.endDate ?? formatDate(now);
|
|
77
66
|
const startDate = options.startDate ?? formatDate(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000));
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
.sort()
|
|
81
|
-
.reverse(); // newest first
|
|
82
|
-
const allRecords = [];
|
|
83
|
-
for (const file of files) {
|
|
84
|
-
const dateStr = file.replace('.jsonl', '');
|
|
85
|
-
if (dateStr < startDate || dateStr > endDate)
|
|
86
|
-
continue;
|
|
87
|
-
const filePath = join(dir, file);
|
|
88
|
-
try {
|
|
89
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
90
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
91
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
92
|
-
try {
|
|
93
|
-
const record = JSON.parse(lines[i]);
|
|
94
|
-
if (options.model && record.model !== options.model)
|
|
95
|
-
continue;
|
|
96
|
-
allRecords.push(record);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// Skip malformed lines
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// Skip unreadable files
|
|
105
|
-
}
|
|
106
|
-
}
|
|
67
|
+
const modelFilter = options.model;
|
|
68
|
+
const allRecords = readJsonlDir(dir, startDate, endDate, modelFilter ? (r) => r.model === modelFilter : undefined);
|
|
107
69
|
// Sort newest first
|
|
108
|
-
allRecords.sort((a, b) =>
|
|
70
|
+
allRecords.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
109
71
|
const total = allRecords.length;
|
|
110
72
|
const paged = allRecords.slice(offset, offset + limit);
|
|
111
73
|
return { records: paged, total };
|
|
@@ -136,15 +98,37 @@ export function aggregateUsage(startDate, endDate) {
|
|
|
136
98
|
}
|
|
137
99
|
/**
|
|
138
100
|
* Get usage summary for today, last 7 days, and last 30 days.
|
|
101
|
+
* Reads files once for the full 30-day window, then partitions in memory.
|
|
139
102
|
*/
|
|
140
103
|
export function getUsageSummary() {
|
|
141
104
|
const now = new Date();
|
|
142
105
|
const todayStr = formatDate(now);
|
|
143
106
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
144
107
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
108
|
+
const weekAgoStr = formatDate(weekAgo);
|
|
109
|
+
const monthAgoStr = formatDate(monthAgo);
|
|
110
|
+
// Read all records once for the full 30-day range
|
|
111
|
+
const { records: allRecords } = readUsageRecords({ startDate: monthAgoStr, endDate: todayStr, limit: 100000 });
|
|
112
|
+
const aggregate = (records) => {
|
|
113
|
+
const agg = emptyAggregation();
|
|
114
|
+
for (const r of records) {
|
|
115
|
+
agg.totalCost += r.totalCost;
|
|
116
|
+
agg.totalInputTokens += r.inputTokens;
|
|
117
|
+
agg.totalOutputTokens += r.outputTokens;
|
|
118
|
+
agg.totalCalls++;
|
|
119
|
+
if (!agg.byModel[r.model]) {
|
|
120
|
+
agg.byModel[r.model] = { calls: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
|
121
|
+
}
|
|
122
|
+
agg.byModel[r.model].calls++;
|
|
123
|
+
agg.byModel[r.model].inputTokens += r.inputTokens;
|
|
124
|
+
agg.byModel[r.model].outputTokens += r.outputTokens;
|
|
125
|
+
agg.byModel[r.model].cost += r.totalCost;
|
|
126
|
+
}
|
|
127
|
+
return agg;
|
|
128
|
+
};
|
|
145
129
|
return {
|
|
146
|
-
today:
|
|
147
|
-
week:
|
|
148
|
-
month:
|
|
130
|
+
today: aggregate(allRecords.filter(r => r.timestamp.slice(0, 10) === todayStr)),
|
|
131
|
+
week: aggregate(allRecords.filter(r => r.timestamp.slice(0, 10) >= weekAgoStr)),
|
|
132
|
+
month: aggregate(allRecords),
|
|
149
133
|
};
|
|
150
134
|
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a Date as YYYY-MM-DD string.
|
|
3
|
+
*/
|
|
4
|
+
export declare function formatDate(date: Date): string;
|
|
5
|
+
/**
|
|
6
|
+
* Extract an error message from an unknown caught value.
|
|
7
|
+
*/
|
|
8
|
+
export declare function toErrorMessage(err: unknown): string;
|
|
9
|
+
/**
|
|
10
|
+
* Read JSONL files from a dated directory (YYYY-MM-DD.jsonl), filtered by date range.
|
|
11
|
+
* Returns records in reverse chronological order (newest first).
|
|
12
|
+
*/
|
|
13
|
+
export declare function readJsonlDir<T>(dir: string, startDate: string, endDate: string, filter?: (record: T) => boolean): T[];
|
|
14
|
+
/**
|
|
15
|
+
* Timing-safe bearer token validation.
|
|
16
|
+
* Returns true if the provided token matches the expected token.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateBearerToken(expected: string, authHeader: string | undefined): boolean;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Shared utilities used across modules
|
|
2
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { timingSafeEqual } from 'crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Format a Date as YYYY-MM-DD string.
|
|
7
|
+
*/
|
|
8
|
+
export function formatDate(date) {
|
|
9
|
+
const y = date.getFullYear();
|
|
10
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
11
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
12
|
+
return `${y}-${m}-${d}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Extract an error message from an unknown caught value.
|
|
16
|
+
*/
|
|
17
|
+
export function toErrorMessage(err) {
|
|
18
|
+
return err instanceof Error ? err.message : String(err);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read JSONL files from a dated directory (YYYY-MM-DD.jsonl), filtered by date range.
|
|
22
|
+
* Returns records in reverse chronological order (newest first).
|
|
23
|
+
*/
|
|
24
|
+
export function readJsonlDir(dir, startDate, endDate, filter) {
|
|
25
|
+
if (!existsSync(dir))
|
|
26
|
+
return [];
|
|
27
|
+
const files = readdirSync(dir)
|
|
28
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
29
|
+
.sort()
|
|
30
|
+
.reverse(); // newest first
|
|
31
|
+
const allRecords = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const dateStr = file.replace('.jsonl', '');
|
|
34
|
+
if (dateStr < startDate || dateStr > endDate)
|
|
35
|
+
continue;
|
|
36
|
+
const filePath = join(dir, file);
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
39
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
40
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
41
|
+
try {
|
|
42
|
+
const record = JSON.parse(lines[i]);
|
|
43
|
+
if (filter && !filter(record))
|
|
44
|
+
continue;
|
|
45
|
+
allRecords.push(record);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Skip malformed lines
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Skip unreadable files
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return allRecords;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Timing-safe bearer token validation.
|
|
60
|
+
* Returns true if the provided token matches the expected token.
|
|
61
|
+
*/
|
|
62
|
+
export function validateBearerToken(expected, authHeader) {
|
|
63
|
+
if (!authHeader || !authHeader.startsWith('Bearer '))
|
|
64
|
+
return false;
|
|
65
|
+
const provided = authHeader.slice(7);
|
|
66
|
+
const expectedBuf = Buffer.from(expected, 'utf8');
|
|
67
|
+
const providedBuf = Buffer.from(provided, 'utf8');
|
|
68
|
+
if (expectedBuf.length !== providedBuf.length)
|
|
69
|
+
return false;
|
|
70
|
+
return timingSafeEqual(expectedBuf, providedBuf);
|
|
71
|
+
}
|
package/dist/voice.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, unlinkSync, readdirSync } from 'fs';
|
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import { basename, dirname, join } from 'path';
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
|
+
import { toErrorMessage } from './utils.js';
|
|
6
7
|
function detectLocalWhisper() {
|
|
7
8
|
// Prefer whisper.cpp (much faster)
|
|
8
9
|
try {
|
|
@@ -62,7 +63,7 @@ function convertToWav(audioPath) {
|
|
|
62
63
|
execSync(`ffmpeg -i "${audioPath}" -ar 16000 -ac 1 -c:a pcm_s16le "${wavPath}" -y`, { encoding: 'utf-8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
63
64
|
}
|
|
64
65
|
catch (err) {
|
|
65
|
-
const msg =
|
|
66
|
+
const msg = toErrorMessage(err);
|
|
66
67
|
throw new Error(`ffmpeg conversion failed: ${msg}`);
|
|
67
68
|
}
|
|
68
69
|
if (!existsSync(wavPath)) {
|
|
@@ -98,7 +99,7 @@ async function transcribeWithWhisperCpp(audioPath, cliPath) {
|
|
|
98
99
|
execSync(`"${cliPath}" -m "${WHISPER_CPP_MODEL}" -otxt -of "${outputBase}" -np -nt "${wavPath}"`, { encoding: 'utf-8', timeout: 60_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
99
100
|
}
|
|
100
101
|
catch (err) {
|
|
101
|
-
const msg =
|
|
102
|
+
const msg = toErrorMessage(err);
|
|
102
103
|
console.error(`[voice] whisper-cli failed: ${msg}`);
|
|
103
104
|
throw new Error(`whisper-cli failed: ${msg}`);
|
|
104
105
|
}
|
|
@@ -150,7 +151,7 @@ async function transcribeWithPythonWhisper(audioPath, cliPath) {
|
|
|
150
151
|
execSync(`"${cliPath}" "${audioPath}" --model turbo --output_format txt --output_dir "${outputDir}"`, { encoding: 'utf-8', timeout: 120_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
151
152
|
}
|
|
152
153
|
catch (err) {
|
|
153
|
-
const msg =
|
|
154
|
+
const msg = toErrorMessage(err);
|
|
154
155
|
throw new Error(`Python whisper failed: ${msg}`);
|
|
155
156
|
}
|
|
156
157
|
const txtPath = join(outputDir, `${baseName}.txt`);
|
|
@@ -374,6 +375,7 @@ export async function synthesizeSpeech(text, config) {
|
|
|
374
375
|
throw new Error('No TTS provider configured. Add a provider with a tts config to your voice config.');
|
|
375
376
|
}
|
|
376
377
|
const { name, provider } = ttsProvider;
|
|
378
|
+
const macosVoice = config.providers?.macos?.tts?.voice || 'Zoe';
|
|
377
379
|
// macOS `say` command
|
|
378
380
|
if (name === 'macos') {
|
|
379
381
|
const voice = provider.tts?.voice || 'Zoe';
|
|
@@ -385,7 +387,7 @@ export async function synthesizeSpeech(text, config) {
|
|
|
385
387
|
if (!apiKey) {
|
|
386
388
|
if (canFallbackToMacOS(config)) {
|
|
387
389
|
console.warn('[voice] No ElevenLabs API key — falling back to macOS TTS');
|
|
388
|
-
return synthesizeWithMacOS(text,
|
|
390
|
+
return synthesizeWithMacOS(text, macosVoice);
|
|
389
391
|
}
|
|
390
392
|
throw new Error('No API key configured for ElevenLabs TTS provider.');
|
|
391
393
|
}
|
|
@@ -411,7 +413,7 @@ export async function synthesizeSpeech(text, config) {
|
|
|
411
413
|
catch (err) {
|
|
412
414
|
if (canFallbackToMacOS(config)) {
|
|
413
415
|
console.warn(`[voice] ElevenLabs failed, falling back to macOS TTS: ${err.message}`);
|
|
414
|
-
return synthesizeWithMacOS(text,
|
|
416
|
+
return synthesizeWithMacOS(text, macosVoice);
|
|
415
417
|
}
|
|
416
418
|
throw err;
|
|
417
419
|
}
|
|
@@ -421,7 +423,7 @@ export async function synthesizeSpeech(text, config) {
|
|
|
421
423
|
if (!apiKey) {
|
|
422
424
|
if (canFallbackToMacOS(config)) {
|
|
423
425
|
console.warn(`[voice] No API key for "${name}" — falling back to macOS TTS`);
|
|
424
|
-
return synthesizeWithMacOS(text,
|
|
426
|
+
return synthesizeWithMacOS(text, macosVoice);
|
|
425
427
|
}
|
|
426
428
|
throw new Error(`No API key configured for TTS provider "${name}".`);
|
|
427
429
|
}
|
|
@@ -445,7 +447,7 @@ export async function synthesizeSpeech(text, config) {
|
|
|
445
447
|
catch (err) {
|
|
446
448
|
if (canFallbackToMacOS(config)) {
|
|
447
449
|
console.warn(`[voice] ${name} TTS failed, falling back to macOS TTS: ${err.message}`);
|
|
448
|
-
return synthesizeWithMacOS(text,
|
|
450
|
+
return synthesizeWithMacOS(text, macosVoice);
|
|
449
451
|
}
|
|
450
452
|
throw err;
|
|
451
453
|
}
|