skimpyclaw 0.3.6 → 0.3.9
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-preflight.test.d.ts +1 -0
- package/dist/__tests__/code-agents-preflight.test.js +88 -0
- package/dist/__tests__/code-agents-sandbox.test.d.ts +1 -0
- package/dist/__tests__/code-agents-sandbox.test.js +163 -0
- package/dist/__tests__/code-agents-utils.test.js +12 -1
- 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 +30 -22
- package/dist/code-agents/orchestrator.d.ts +8 -2
- package/dist/code-agents/orchestrator.js +318 -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.d.ts +4 -0
- package/dist/code-agents/utils.js +38 -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 +122 -27
- 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 +26 -21
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export interface ExecuteToolContext {
|
|
2
|
-
/** Task ID for file lock acquisition (
|
|
2
|
+
/** Task ID for file lock acquisition (concurrent writes) */
|
|
3
3
|
lockTaskId?: string;
|
|
4
4
|
/** Abort signal for cancelling long-running tool loops */
|
|
5
5
|
abortSignal?: AbortSignal;
|
|
6
|
-
/** Chat ID for
|
|
6
|
+
/** Chat ID for channel routing */
|
|
7
7
|
chatId?: number;
|
|
8
|
-
/** Full config for
|
|
8
|
+
/** Full config for agent tools */
|
|
9
9
|
fullConfig?: import('../types.js').Config;
|
|
10
|
-
/** Conversation history
|
|
10
|
+
/** Conversation history */
|
|
11
11
|
history?: import('../types.js').ChatMessage[];
|
|
12
12
|
/** Audit trace ID for recording tool events */
|
|
13
13
|
auditTraceId?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ToolConfig } from '../types.js';
|
|
2
2
|
export declare function executeReadFile(path: string, config: ToolConfig): string;
|
|
3
3
|
/**
|
|
4
|
-
* Write with file locking when a lockTaskId is provided (
|
|
4
|
+
* Write with file locking when a lockTaskId is provided (concurrent context).
|
|
5
5
|
* Falls back to unlocked write when no lockTaskId.
|
|
6
6
|
*/
|
|
7
7
|
export declare function executeWriteFileLocked(path: string, content: string, config: ToolConfig, lockTaskId?: string): Promise<string>;
|
package/dist/tools/file-tools.js
CHANGED
|
@@ -26,7 +26,7 @@ function executeWriteFile(path, content, config) {
|
|
|
26
26
|
return `Written: ${path} (${content.length} bytes)`;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Write with file locking when a lockTaskId is provided (
|
|
29
|
+
* Write with file locking when a lockTaskId is provided (concurrent context).
|
|
30
30
|
* Falls back to unlocked write when no lockTaskId.
|
|
31
31
|
*/
|
|
32
32
|
export async function executeWriteFileLocked(path, content, config, lockTaskId) {
|
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
|
+
}
|