skimpyclaw 0.3.9 → 0.3.14
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/dist/__tests__/channels.test.js +1 -1
- package/dist/__tests__/context-manager.test.js +219 -76
- package/dist/__tests__/providers-utils.test.js +2 -0
- package/dist/__tests__/sandbox-manager.test.js +25 -0
- package/dist/__tests__/sandbox-mount-security.test.js +8 -0
- package/dist/__tests__/setup.test.js +1 -1
- package/dist/__tests__/skills.test.js +53 -26
- package/dist/__tests__/token-efficiency.test.js +37 -15
- package/dist/__tests__/tools.test.js +11 -9
- package/dist/agent.js +2 -2
- package/dist/api.js +5 -0
- package/dist/channels/discord/handlers.d.ts +7 -0
- package/dist/channels/discord/handlers.js +479 -0
- package/dist/channels/discord/index.d.ts +8 -0
- package/dist/channels/discord/index.js +149 -0
- package/dist/channels/discord/types.d.ts +6 -0
- package/dist/channels/discord/types.js +17 -0
- package/dist/channels/discord/utils.d.ts +14 -0
- package/dist/channels/discord/utils.js +161 -0
- package/dist/channels/telegram/utils.d.ts +1 -1
- package/dist/channels/telegram/utils.js +7 -9
- package/dist/channels.js +1 -1
- package/dist/cli.js +8 -43
- package/dist/code-agents/parser.js +5 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +13 -0
- package/dist/cron.js +6 -3
- package/dist/heartbeat.js +11 -15
- package/dist/providers/anthropic.js +7 -1
- package/dist/providers/codex.js +8 -2
- package/dist/providers/context-manager.d.ts +37 -6
- package/dist/providers/context-manager.js +303 -47
- package/dist/providers/openai.js +8 -2
- package/dist/providers/utils.d.ts +6 -2
- package/dist/providers/utils.js +36 -4
- package/dist/sandbox/manager.js +11 -0
- package/dist/sandbox/mount-security.js +5 -1
- package/dist/sandbox/runtime.d.ts +1 -0
- package/dist/sandbox/runtime.js +5 -0
- package/dist/sandbox-utils.d.ts +6 -0
- package/dist/sandbox-utils.js +36 -0
- package/dist/security.js +4 -3
- package/dist/service.js +25 -0
- package/dist/setup-templates.d.ts +14 -0
- package/dist/setup-templates.js +214 -0
- package/dist/setup.d.ts +1 -9
- package/dist/setup.js +3 -244
- package/dist/skills-types.d.ts +6 -0
- package/dist/skills.d.ts +5 -1
- package/dist/skills.js +25 -2
- package/dist/tools/bash-tool.js +11 -1
- package/dist/tools/definitions.d.ts +57 -0
- package/dist/tools/definitions.js +19 -1
- package/dist/tools/fetch-tool.d.ts +8 -0
- package/dist/tools/fetch-tool.js +80 -0
- package/dist/tools.d.ts +4 -2
- package/dist/tools.js +110 -62
- package/dist/types.d.ts +5 -0
- package/package.json +23 -29
package/dist/tools/bash-tool.js
CHANGED
|
@@ -9,14 +9,24 @@ const SENSITIVE_ENV_PATTERNS = [
|
|
|
9
9
|
/^ANTHROPIC_/i, /^OPENAI_/i, /^CLAUDE/i, /^CODEX_/i, /^MINIMAX_/i,
|
|
10
10
|
/^KIMI_/i, /^TOGETHER_/i, /^GROQ_/i, /^OPENROUTER_/i,
|
|
11
11
|
];
|
|
12
|
+
/** Env vars that match SENSITIVE_ENV_PATTERNS but should be kept (e.g. tool auth). */
|
|
13
|
+
const SENSITIVE_ENV_ALLOWLIST = new Set(['GH_TOKEN']);
|
|
14
|
+
/** Common tool paths that may be missing when launched as a service/daemon. */
|
|
15
|
+
const EXTRA_PATH_DIRS = ['/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin'];
|
|
12
16
|
/** Create a sanitized copy of process.env with secrets stripped. */
|
|
13
17
|
function sanitizeEnv() {
|
|
14
18
|
const env = { ...process.env };
|
|
15
19
|
for (const key of Object.keys(env)) {
|
|
16
|
-
if (SENSITIVE_ENV_PATTERNS.some(p => p.test(key))) {
|
|
20
|
+
if (!SENSITIVE_ENV_ALLOWLIST.has(key) && SENSITIVE_ENV_PATTERNS.some(p => p.test(key))) {
|
|
17
21
|
delete env[key];
|
|
18
22
|
}
|
|
19
23
|
}
|
|
24
|
+
// Ensure common tool directories are in PATH (daemon/service launches often have a minimal PATH)
|
|
25
|
+
const currentPath = env.PATH || '';
|
|
26
|
+
const missing = EXTRA_PATH_DIRS.filter(d => !currentPath.includes(d));
|
|
27
|
+
if (missing.length > 0) {
|
|
28
|
+
env.PATH = `${currentPath}:${missing.join(':')}`;
|
|
29
|
+
}
|
|
20
30
|
return env;
|
|
21
31
|
}
|
|
22
32
|
export async function executeBash(command, cwd, config, context) {
|
|
@@ -154,6 +154,35 @@ export declare const BROWSER_TOOL_DEFINITION: {
|
|
|
154
154
|
required: string[];
|
|
155
155
|
};
|
|
156
156
|
};
|
|
157
|
+
export declare const FETCH_TOOL_DEFINITION: {
|
|
158
|
+
name: string;
|
|
159
|
+
description: string;
|
|
160
|
+
input_schema: {
|
|
161
|
+
type: "object";
|
|
162
|
+
properties: {
|
|
163
|
+
url: {
|
|
164
|
+
type: string;
|
|
165
|
+
description: string;
|
|
166
|
+
};
|
|
167
|
+
method: {
|
|
168
|
+
type: string;
|
|
169
|
+
description: string;
|
|
170
|
+
};
|
|
171
|
+
headers: {
|
|
172
|
+
type: "object";
|
|
173
|
+
description: string;
|
|
174
|
+
additionalProperties: {
|
|
175
|
+
type: string;
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
body: {
|
|
179
|
+
type: string;
|
|
180
|
+
description: string;
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
required: string[];
|
|
184
|
+
};
|
|
185
|
+
};
|
|
157
186
|
export declare const TOOL_DEFINITIONS: ({
|
|
158
187
|
name: string;
|
|
159
188
|
description: string;
|
|
@@ -304,6 +333,34 @@ export declare const TOOL_DEFINITIONS: ({
|
|
|
304
333
|
};
|
|
305
334
|
required: string[];
|
|
306
335
|
};
|
|
336
|
+
} | {
|
|
337
|
+
name: string;
|
|
338
|
+
description: string;
|
|
339
|
+
input_schema: {
|
|
340
|
+
type: "object";
|
|
341
|
+
properties: {
|
|
342
|
+
url: {
|
|
343
|
+
type: string;
|
|
344
|
+
description: string;
|
|
345
|
+
};
|
|
346
|
+
method: {
|
|
347
|
+
type: string;
|
|
348
|
+
description: string;
|
|
349
|
+
};
|
|
350
|
+
headers: {
|
|
351
|
+
type: "object";
|
|
352
|
+
description: string;
|
|
353
|
+
additionalProperties: {
|
|
354
|
+
type: string;
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
body: {
|
|
358
|
+
type: string;
|
|
359
|
+
description: string;
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
required: string[];
|
|
363
|
+
};
|
|
307
364
|
})[];
|
|
308
365
|
export declare const CODE_WITH_AGENT_TOOL: {
|
|
309
366
|
name: string;
|
|
@@ -110,8 +110,26 @@ export const BROWSER_TOOL_DEFINITION = {
|
|
|
110
110
|
required: ['action'],
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
|
+
export const FETCH_TOOL_DEFINITION = {
|
|
114
|
+
name: 'Fetch',
|
|
115
|
+
description: 'Make an HTTP request and return the response. HTML is auto-converted to plain text. Use for APIs, web search (e.g. https://duckduckgo.com/html/?q=your+query), or fetching page content. Prefer over Browser for simple requests.',
|
|
116
|
+
input_schema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
url: { type: 'string', description: 'URL to fetch' },
|
|
120
|
+
method: { type: 'string', description: 'HTTP method (GET, POST, PUT, DELETE, PATCH). Default: GET' },
|
|
121
|
+
headers: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
description: 'Request headers (e.g. {"Authorization": "Bearer ..."})',
|
|
124
|
+
additionalProperties: { type: 'string' },
|
|
125
|
+
},
|
|
126
|
+
body: { type: 'string', description: 'Request body (for POST/PUT/PATCH)' },
|
|
127
|
+
},
|
|
128
|
+
required: ['url'],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
113
131
|
// Legacy export for backward compat — static list (built-ins + browser + no MCP)
|
|
114
|
-
export const TOOL_DEFINITIONS = [...BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION];
|
|
132
|
+
export const TOOL_DEFINITIONS = [...BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, FETCH_TOOL_DEFINITION];
|
|
115
133
|
export const CODE_WITH_AGENT_TOOL = {
|
|
116
134
|
name: 'code_with_agent',
|
|
117
135
|
description: 'Delegate a coding task to a coding agent CLI (Claude Code or Codex). The agent will edit files, run commands, and return results. Always use this for code changes instead of writing code directly.',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolConfig } from '../types.js';
|
|
2
|
+
export interface FetchInput {
|
|
3
|
+
url: string;
|
|
4
|
+
method?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
body?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function executeFetch(input: FetchInput, _config: ToolConfig): Promise<string>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Fetch tool — lightweight HTTP requests and web search without browser overhead
|
|
2
|
+
const MAX_RESPONSE_CHARS = 50_000;
|
|
3
|
+
const TIMEOUT_MS = 30_000;
|
|
4
|
+
/**
|
|
5
|
+
* Strip HTML tags and decode common entities. Returns plain text.
|
|
6
|
+
*/
|
|
7
|
+
function htmlToText(html) {
|
|
8
|
+
return html
|
|
9
|
+
// Remove script/style blocks
|
|
10
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
11
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
12
|
+
// Convert block elements to newlines
|
|
13
|
+
.replace(/<\/(p|div|h[1-6]|li|tr|br\s*\/?)>/gi, '\n')
|
|
14
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
15
|
+
// Remove remaining tags
|
|
16
|
+
.replace(/<[^>]+>/g, '')
|
|
17
|
+
// Decode common entities
|
|
18
|
+
.replace(/&/g, '&')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>')
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
|
+
.replace(/'/g, "'")
|
|
23
|
+
.replace(/ /g, ' ')
|
|
24
|
+
// Collapse whitespace
|
|
25
|
+
.replace(/[ \t]+/g, ' ')
|
|
26
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
export async function executeFetch(input, _config) {
|
|
30
|
+
const { url, method = 'GET', headers = {}, body } = input;
|
|
31
|
+
if (!url)
|
|
32
|
+
return 'Error: url is required';
|
|
33
|
+
try {
|
|
34
|
+
new URL(url);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return `Error: Invalid URL: ${url}`;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const defaultHeaders = {
|
|
41
|
+
'User-Agent': 'SkimpyClaw/1.0',
|
|
42
|
+
};
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method: method.toUpperCase(),
|
|
45
|
+
headers: { ...defaultHeaders, ...headers },
|
|
46
|
+
body: body || undefined,
|
|
47
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
48
|
+
redirect: 'follow',
|
|
49
|
+
});
|
|
50
|
+
const status = `${response.status} ${response.statusText}`;
|
|
51
|
+
const contentType = response.headers.get('content-type') || '';
|
|
52
|
+
let responseBody;
|
|
53
|
+
if (contentType.includes('json')) {
|
|
54
|
+
const text = await response.text();
|
|
55
|
+
try {
|
|
56
|
+
responseBody = JSON.stringify(JSON.parse(text), null, 2);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
responseBody = text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (contentType.includes('html')) {
|
|
63
|
+
const html = await response.text();
|
|
64
|
+
responseBody = htmlToText(html);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
responseBody = await response.text();
|
|
68
|
+
}
|
|
69
|
+
if (responseBody.length > MAX_RESPONSE_CHARS) {
|
|
70
|
+
responseBody = responseBody.slice(0, MAX_RESPONSE_CHARS) + `\n\n[Truncated: ${responseBody.length} chars total]`;
|
|
71
|
+
}
|
|
72
|
+
return `HTTP ${status}\n\n${responseBody}`;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
76
|
+
return `Error: Request timed out after ${TIMEOUT_MS / 1000}s`;
|
|
77
|
+
}
|
|
78
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { ToolConfig } from './types.js';
|
|
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';
|
|
2
|
+
import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, FETCH_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, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, cleanupBrowser, };
|
|
6
|
+
export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, FETCH_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
|
+
/** Force-reconnect the MCP runtime (clears cached runtime and tool discovery). */
|
|
12
|
+
export declare function reconnectMcp(): Promise<void>;
|
|
11
13
|
/**
|
|
12
14
|
* Get all available tool definitions: built-ins + browser (if enabled) + MCP (auto-discovered) + agent tools.
|
|
13
15
|
* This is the primary way to get tools — replaces the static TOOL_DEFINITIONS export.
|
package/dist/tools.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Tool definitions and executors for Anthropic API tool_use
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
3
|
import { join, resolve } from 'path';
|
|
3
4
|
import { homedir } from 'os';
|
|
4
5
|
import { TTLCache } from './cache.js';
|
|
5
6
|
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';
|
|
7
|
+
import { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, FETCH_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, } from './tools/definitions.js';
|
|
7
8
|
import { executeReadFile, executeWriteFileLocked, executeListDirectory } from './tools/file-tools.js';
|
|
8
9
|
import { executeBash } from './tools/bash-tool.js';
|
|
9
10
|
import { executeBrowser, cleanupBrowser } from './tools/browser-tool.js';
|
|
11
|
+
import { executeFetch } from './tools/fetch-tool.js';
|
|
10
12
|
import { ensureContainer, SANDBOX_DEFAULTS, translatePath, validateMountPaths } from './sandbox/index.js';
|
|
11
13
|
import { sandboxBash, sandboxReadFile, sandboxWriteFile, sandboxListDir, sandboxGlob } from './sandbox/index.js';
|
|
12
14
|
import { isBashCommandSafe } from './security.js';
|
|
@@ -26,7 +28,7 @@ buildCodeAgentArgs, readTeamState,
|
|
|
26
28
|
// Parsers
|
|
27
29
|
parseStreamJsonForLive, } from './code-agents/index.js';
|
|
28
30
|
// Re-export from 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, };
|
|
31
|
+
export { fromClaudeCodeName, toClaudeCodeName, BUILTIN_TOOL_DEFINITIONS, BROWSER_TOOL_DEFINITION, FETCH_TOOL_DEFINITION, TOOL_DEFINITIONS, CODE_WITH_AGENT_TOOL, CODE_WITH_TEAM_TOOL, CHECK_CODE_AGENT_TOOL, cleanupBrowser, };
|
|
30
32
|
// --- MCP (mcporter) ---
|
|
31
33
|
let mcpRuntime = null;
|
|
32
34
|
async function getMcpRuntime() {
|
|
@@ -46,6 +48,32 @@ function sanitizeToolName(name) {
|
|
|
46
48
|
}
|
|
47
49
|
// Maps sanitized tool name → { server (original), tool (original) } for routing
|
|
48
50
|
const mcpToolNameMap = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* Fallback tool definitions for MCP servers that don't support tools/list.
|
|
53
|
+
* Loaded from ~/.skimpyclaw/mcp-fallbacks.json if it exists.
|
|
54
|
+
*
|
|
55
|
+
* Format: { "<server-name>": [ { name, description, input_schema } ] }
|
|
56
|
+
*/
|
|
57
|
+
let mcpFallbackCache = null;
|
|
58
|
+
function getMcpFallbackTools(server) {
|
|
59
|
+
if (!mcpFallbackCache) {
|
|
60
|
+
const fallbackPath = join(homedir(), '.skimpyclaw', 'mcp-fallbacks.json');
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(fallbackPath)) {
|
|
63
|
+
mcpFallbackCache = JSON.parse(readFileSync(fallbackPath, 'utf-8'));
|
|
64
|
+
console.log(`[mcp] Loaded fallback tools from ${fallbackPath}`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
mcpFallbackCache = {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.warn('[mcp] Failed to load mcp-fallbacks.json:', err instanceof Error ? err.message : err);
|
|
72
|
+
mcpFallbackCache = {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return mcpFallbackCache[server] || [];
|
|
76
|
+
}
|
|
49
77
|
export async function discoverMcpTools() {
|
|
50
78
|
if (discoveredMcpTools !== null)
|
|
51
79
|
return discoveredMcpTools;
|
|
@@ -72,6 +100,18 @@ export async function discoverMcpTools() {
|
|
|
72
100
|
}
|
|
73
101
|
catch (err) {
|
|
74
102
|
console.warn(`[mcp] Failed to list tools for server "${server}":`, err instanceof Error ? err.message : err);
|
|
103
|
+
// Fallback: register well-known tools for servers that don't support tools/list
|
|
104
|
+
const fallbackTools = getMcpFallbackTools(server);
|
|
105
|
+
if (fallbackTools.length > 0) {
|
|
106
|
+
const sanitizedServer = sanitizeToolName(server);
|
|
107
|
+
for (const ft of fallbackTools) {
|
|
108
|
+
const sanitizedTool = sanitizeToolName(ft.name);
|
|
109
|
+
const name = `mcp__${sanitizedServer}__${sanitizedTool}`;
|
|
110
|
+
mcpToolNameMap.set(name, { server, tool: ft.name });
|
|
111
|
+
tools.push({ name, description: ft.description, input_schema: ft.input_schema });
|
|
112
|
+
}
|
|
113
|
+
console.log(`[mcp] Registered ${fallbackTools.length} fallback tools for "${server}"`);
|
|
114
|
+
}
|
|
75
115
|
}
|
|
76
116
|
}
|
|
77
117
|
}
|
|
@@ -85,6 +125,26 @@ export function clearMcpToolCache() {
|
|
|
85
125
|
discoveredMcpTools = null;
|
|
86
126
|
}
|
|
87
127
|
const toolDefsCache = new TTLCache(60_000);
|
|
128
|
+
/** Force-reconnect the MCP runtime (clears cached runtime and tool discovery). */
|
|
129
|
+
export async function reconnectMcp() {
|
|
130
|
+
console.log('[mcp] Reconnecting...');
|
|
131
|
+
if (mcpRuntime) {
|
|
132
|
+
await mcpRuntime.close().catch(() => { });
|
|
133
|
+
mcpRuntime = null;
|
|
134
|
+
}
|
|
135
|
+
discoveredMcpTools = null;
|
|
136
|
+
mcpToolNameMap.clear();
|
|
137
|
+
mcpFallbackCache = null;
|
|
138
|
+
toolDefsCache.clear();
|
|
139
|
+
try {
|
|
140
|
+
await getMcpRuntime();
|
|
141
|
+
const tools = await discoverMcpTools();
|
|
142
|
+
console.log(`[mcp] Reconnected — ${tools.length} tools discovered`);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.error('[mcp] Reconnect failed:', err instanceof Error ? err.message : err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
88
148
|
/** Inject project names into a tool's workdir description, or return the tool unchanged. */
|
|
89
149
|
function injectProjects(tool, projects) {
|
|
90
150
|
if (!projects || Object.keys(projects).length === 0)
|
|
@@ -126,7 +186,9 @@ export async function getToolDefinitions(config, options) {
|
|
|
126
186
|
if (cached)
|
|
127
187
|
return cached;
|
|
128
188
|
const tools = [...BUILTIN_TOOL_DEFINITIONS];
|
|
129
|
-
//
|
|
189
|
+
// Fetch is always available (lightweight HTTP, no dependencies)
|
|
190
|
+
tools.push(FETCH_TOOL_DEFINITION);
|
|
191
|
+
// Minimal profile: built-in tools + fetch.
|
|
130
192
|
// Used by orchestrator decompose/synthesize calls.
|
|
131
193
|
if (profile === 'minimal') {
|
|
132
194
|
toolDefsCache.set(cacheKey, tools);
|
|
@@ -166,75 +228,49 @@ export function clearToolDefsCache() {
|
|
|
166
228
|
toolDefsCache.clear();
|
|
167
229
|
}
|
|
168
230
|
// --- MCP Tool Execution (generic) ---
|
|
169
|
-
async function
|
|
170
|
-
// Look up original server/tool names from the sanitized name map
|
|
171
|
-
const mapping = mcpToolNameMap.get(fullName);
|
|
172
|
-
if (!mapping) {
|
|
173
|
-
// Fallback: parse from the name directly (works when names don't need sanitizing)
|
|
174
|
-
const parts = fullName.split('__');
|
|
175
|
-
if (parts.length < 3)
|
|
176
|
-
return `Error: Invalid MCP tool name "${fullName}"`;
|
|
177
|
-
const server = parts[1];
|
|
178
|
-
const toolName = parts.slice(2).join('__');
|
|
179
|
-
const runtime = await getMcpRuntime();
|
|
180
|
-
const result = await runtime.callTool(server, toolName, { args });
|
|
181
|
-
const content = result?.content;
|
|
182
|
-
if (Array.isArray(content)) {
|
|
183
|
-
return content.map((c) => c.text || JSON.stringify(c)).join('\n');
|
|
184
|
-
}
|
|
185
|
-
return JSON.stringify(result);
|
|
186
|
-
}
|
|
231
|
+
async function callMcpTool(server, tool, args) {
|
|
187
232
|
const runtime = await getMcpRuntime();
|
|
188
|
-
const result = await runtime.callTool(
|
|
233
|
+
const result = await runtime.callTool(server, tool, { args });
|
|
189
234
|
const content = result?.content;
|
|
190
235
|
if (Array.isArray(content)) {
|
|
191
236
|
return content.map((c) => c.text || JSON.stringify(c)).join('\n');
|
|
192
237
|
}
|
|
193
238
|
return JSON.stringify(result);
|
|
194
239
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const q = query.trim();
|
|
203
|
-
if (!q)
|
|
204
|
-
return 'Error: $web_search requires a non-empty query';
|
|
205
|
-
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(q)}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
|
|
206
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
|
207
|
-
if (!res.ok) {
|
|
208
|
-
return `Error: web search failed (${res.status} ${res.statusText})`;
|
|
240
|
+
async function executeMcpToolGeneric(fullName, args) {
|
|
241
|
+
const mapping = mcpToolNameMap.get(fullName);
|
|
242
|
+
let server;
|
|
243
|
+
let toolName;
|
|
244
|
+
if (mapping) {
|
|
245
|
+
server = mapping.server;
|
|
246
|
+
toolName = mapping.tool;
|
|
209
247
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
248
|
+
else {
|
|
249
|
+
const parts = fullName.split('__');
|
|
250
|
+
if (parts.length < 3)
|
|
251
|
+
return `Error: Invalid MCP tool name "${fullName}"`;
|
|
252
|
+
server = parts[1];
|
|
253
|
+
toolName = parts.slice(2).join('__');
|
|
217
254
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if ('Topics' in item && Array.isArray(item.Topics)) {
|
|
221
|
-
related.push(...item.Topics);
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
related.push(item);
|
|
225
|
-
}
|
|
255
|
+
try {
|
|
256
|
+
return await callMcpTool(server, toolName, args);
|
|
226
257
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
catch (err) {
|
|
259
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
260
|
+
// Retry once on connection/session errors
|
|
261
|
+
if (msg.includes('session') || msg.includes('Session') || msg.includes('ECONNR') || msg.includes('EPIPE') || msg.includes('closed') || msg.includes('disconnected')) {
|
|
262
|
+
console.warn(`[mcp] Tool call failed (${msg}), reconnecting and retrying...`);
|
|
263
|
+
await reconnectMcp();
|
|
264
|
+
return await callMcpTool(server, toolName, args);
|
|
232
265
|
}
|
|
266
|
+
throw err;
|
|
233
267
|
}
|
|
234
|
-
|
|
235
|
-
|
|
268
|
+
}
|
|
269
|
+
export async function cleanupMcp() {
|
|
270
|
+
if (mcpRuntime) {
|
|
271
|
+
await mcpRuntime.close().catch(() => { });
|
|
272
|
+
mcpRuntime = null;
|
|
236
273
|
}
|
|
237
|
-
return lines.join('\n');
|
|
238
274
|
}
|
|
239
275
|
// --- Tool Executor ---
|
|
240
276
|
export async function executeTool(name, input, config, context) {
|
|
@@ -324,12 +360,21 @@ export async function executeTool(name, input, config, context) {
|
|
|
324
360
|
switch (normalized) {
|
|
325
361
|
case 'bash': {
|
|
326
362
|
const translatedCmd = translateBashPaths(input.command);
|
|
327
|
-
// Apply hard safety blocks
|
|
328
|
-
//
|
|
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.).
|
|
329
367
|
if (!isBashCommandSafe(translatedCmd)) {
|
|
330
368
|
return 'Error: Command blocked by safety filter.';
|
|
331
369
|
}
|
|
332
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
|
+
}
|
|
333
378
|
const approvalConfig = config.execApproval;
|
|
334
379
|
if (requiresApproval(classification, approvalConfig)) {
|
|
335
380
|
const isUnattended = context?.channel === 'subagent' ||
|
|
@@ -372,7 +417,8 @@ export async function executeTool(name, input, config, context) {
|
|
|
372
417
|
case '$web_search':
|
|
373
418
|
case 'web_search':
|
|
374
419
|
case 'websearch':
|
|
375
|
-
|
|
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);
|
|
376
422
|
case 'read_file':
|
|
377
423
|
return executeReadFile(input.file_path || input.path, config);
|
|
378
424
|
case 'write_file':
|
|
@@ -383,6 +429,8 @@ export async function executeTool(name, input, config, context) {
|
|
|
383
429
|
return await executeBash(input.command, input.cwd, config, context);
|
|
384
430
|
case 'browser':
|
|
385
431
|
return await executeBrowser(input, config);
|
|
432
|
+
case 'fetch':
|
|
433
|
+
return await executeFetch(input, config);
|
|
386
434
|
default:
|
|
387
435
|
return `Error: Unknown tool "${name}"`;
|
|
388
436
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export interface VoiceConfig {
|
|
|
25
25
|
channels?: Record<string, VoiceChannelConfig>;
|
|
26
26
|
}
|
|
27
27
|
export interface Config {
|
|
28
|
+
/** Global allowed paths. Channels, cron, and heartbeat inherit these
|
|
29
|
+
* unless they specify their own tools.allowedPaths override. */
|
|
30
|
+
allowedPaths?: string[];
|
|
28
31
|
gateway: {
|
|
29
32
|
port: number;
|
|
30
33
|
host?: string;
|
|
@@ -149,6 +152,7 @@ export interface SandboxConfig {
|
|
|
149
152
|
memory?: string;
|
|
150
153
|
network?: string;
|
|
151
154
|
idleTimeoutMs?: number;
|
|
155
|
+
env?: Record<string, string>;
|
|
152
156
|
}
|
|
153
157
|
export interface ToolConfig {
|
|
154
158
|
enabled: boolean;
|
|
@@ -160,6 +164,7 @@ export interface ToolConfig {
|
|
|
160
164
|
contextManagement?: {
|
|
161
165
|
enabled?: boolean;
|
|
162
166
|
maxContextTokens?: number;
|
|
167
|
+
compactionModel?: string;
|
|
163
168
|
};
|
|
164
169
|
browser?: {
|
|
165
170
|
enabled?: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skimpyclaw",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.14",
|
|
4
|
+
"description": "A lobster in a bikini",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -16,31 +16,11 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"cli": "tsx src/cli.ts",
|
|
21
|
-
"start": "tsx src/index.ts",
|
|
22
|
-
"dev": "tsx watch src/index.ts",
|
|
23
|
-
"dashboard:dev": "pnpm --dir web/dashboard dev",
|
|
24
|
-
"dashboard:build": "pnpm --dir web/dashboard install --frozen-lockfile && pnpm --dir web/dashboard build",
|
|
25
|
-
"docs:dev": "pnpm --dir docs dev",
|
|
26
|
-
"docs:build": "pnpm --dir docs install --frozen-lockfile && pnpm --dir docs build",
|
|
27
|
-
"docs:preview": "pnpm --dir docs preview",
|
|
28
|
-
"setup": "tsx src/setup.ts",
|
|
29
|
-
"onboard": "tsx src/cli.ts onboard",
|
|
30
|
-
"build": "tsc && pnpm dashboard:build",
|
|
31
|
-
"release:check": "pnpm build && pnpm test",
|
|
32
|
-
"release:local": "bash ./scripts/release.sh",
|
|
33
|
-
"lint": "eslint \"src/**/*.ts\"",
|
|
34
|
-
"typecheck": "tsc --noEmit",
|
|
35
|
-
"test": "vitest run",
|
|
36
|
-
"ci": "pnpm run lint && pnpm run typecheck && pnpm run test"
|
|
37
|
-
},
|
|
38
19
|
"dependencies": {
|
|
39
20
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
40
21
|
"@grammyjs/runner": "^2.0.3",
|
|
41
22
|
"@langfuse/otel": "^4.5.1",
|
|
42
23
|
"@langfuse/tracing": "^4.5.1",
|
|
43
|
-
"@mariozechner/pi-ai": "^0.51.6",
|
|
44
24
|
"@opentelemetry/api": "^1.9.0",
|
|
45
25
|
"@opentelemetry/core": "^2.0.1",
|
|
46
26
|
"@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
|
|
@@ -49,18 +29,13 @@
|
|
|
49
29
|
"croner": "^8.0.0",
|
|
50
30
|
"discord.js": "^14.25.1",
|
|
51
31
|
"dotenv": "^16.4.0",
|
|
52
|
-
"fastify": "^
|
|
32
|
+
"fastify": "^5.7.2",
|
|
53
33
|
"grammy": "^1.21.1",
|
|
54
34
|
"gray-matter": "^4.0.3",
|
|
55
35
|
"mcporter": "^0.7.3",
|
|
56
36
|
"openai": "^4.47.0",
|
|
57
37
|
"playwright": "^1.49.0"
|
|
58
38
|
},
|
|
59
|
-
"pnpm": {
|
|
60
|
-
"onlyBuiltDependencies": [
|
|
61
|
-
"esbuild"
|
|
62
|
-
]
|
|
63
|
-
},
|
|
64
39
|
"devDependencies": {
|
|
65
40
|
"@eslint/js": "^9.39.2",
|
|
66
41
|
"@types/node": "^20.11.0",
|
|
@@ -70,5 +45,24 @@
|
|
|
70
45
|
"typescript": "^5.4.0",
|
|
71
46
|
"typescript-eslint": "^8.54.0",
|
|
72
47
|
"vitest": "^4.0.18"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"cli": "tsx src/cli.ts",
|
|
51
|
+
"start": "tsx src/index.ts",
|
|
52
|
+
"dev": "tsx watch src/index.ts",
|
|
53
|
+
"dashboard:dev": "pnpm --dir web/dashboard dev",
|
|
54
|
+
"dashboard:build": "pnpm --dir web/dashboard install --frozen-lockfile && pnpm --dir web/dashboard build",
|
|
55
|
+
"docs:dev": "pnpm --dir docs dev",
|
|
56
|
+
"docs:build": "pnpm --dir docs install --frozen-lockfile && pnpm --dir docs build",
|
|
57
|
+
"docs:preview": "pnpm --dir docs preview",
|
|
58
|
+
"setup": "tsx src/setup.ts",
|
|
59
|
+
"onboard": "tsx src/cli.ts onboard",
|
|
60
|
+
"build": "tsc && pnpm dashboard:build",
|
|
61
|
+
"release:check": "pnpm build && pnpm test",
|
|
62
|
+
"release:local": "bash ./scripts/release.sh",
|
|
63
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"ci": "pnpm run lint && pnpm run typecheck && pnpm run test"
|
|
73
67
|
}
|
|
74
|
-
}
|
|
68
|
+
}
|