@dotsetlabs/dotclaw 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +54 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/config-examples/groups/global/CLAUDE.md +21 -0
- package/config-examples/groups/main/CLAUDE.md +47 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/config-examples/plugin-http.json +18 -0
- package/config-examples/runtime.json +30 -0
- package/config-examples/tool-budgets.json +24 -0
- package/config-examples/tool-policy.json +51 -0
- package/container/.dockerignore +6 -0
- package/container/Dockerfile +74 -0
- package/container/agent-runner/package-lock.json +92 -0
- package/container/agent-runner/package.json +20 -0
- package/container/agent-runner/src/agent-config.ts +295 -0
- package/container/agent-runner/src/container-protocol.ts +73 -0
- package/container/agent-runner/src/daemon.ts +91 -0
- package/container/agent-runner/src/index.ts +1428 -0
- package/container/agent-runner/src/ipc.ts +321 -0
- package/container/agent-runner/src/memory.ts +336 -0
- package/container/agent-runner/src/prompt-packs.ts +341 -0
- package/container/agent-runner/src/tools.ts +1720 -0
- package/container/agent-runner/tsconfig.json +19 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser.md +159 -0
- package/dist/admin-commands.d.ts +7 -0
- package/dist/admin-commands.d.ts.map +1 -0
- package/dist/admin-commands.js +87 -0
- package/dist/admin-commands.js.map +1 -0
- package/dist/agent-context.d.ts +42 -0
- package/dist/agent-context.d.ts.map +1 -0
- package/dist/agent-context.js +92 -0
- package/dist/agent-context.js.map +1 -0
- package/dist/agent-execution.d.ts +68 -0
- package/dist/agent-execution.d.ts.map +1 -0
- package/dist/agent-execution.js +169 -0
- package/dist/agent-execution.js.map +1 -0
- package/dist/agent-semaphore.d.ts +2 -0
- package/dist/agent-semaphore.d.ts.map +1 -0
- package/dist/agent-semaphore.js +52 -0
- package/dist/agent-semaphore.js.map +1 -0
- package/dist/behavior-config.d.ts +14 -0
- package/dist/behavior-config.d.ts.map +1 -0
- package/dist/behavior-config.js +52 -0
- package/dist/behavior-config.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +626 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/container-protocol.d.ts +72 -0
- package/dist/container-protocol.d.ts.map +1 -0
- package/dist/container-protocol.js +3 -0
- package/dist/container-protocol.js.map +1 -0
- package/dist/container-runner.d.ts +59 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +813 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/cost.d.ts +9 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +11 -0
- package/dist/cost.js.map +1 -0
- package/dist/dashboard.d.ts +58 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +471 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.d.ts +99 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +423 -0
- package/dist/db.js.map +1 -0
- package/dist/error-messages.d.ts +17 -0
- package/dist/error-messages.d.ts.map +1 -0
- package/dist/error-messages.js +109 -0
- package/dist/error-messages.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2072 -0
- package/dist/index.js.map +1 -0
- package/dist/locks.d.ts +2 -0
- package/dist/locks.d.ts.map +1 -0
- package/dist/locks.js +26 -0
- package/dist/locks.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/maintenance.d.ts +13 -0
- package/dist/maintenance.d.ts.map +1 -0
- package/dist/maintenance.js +151 -0
- package/dist/maintenance.js.map +1 -0
- package/dist/memory-embeddings.d.ts +13 -0
- package/dist/memory-embeddings.d.ts.map +1 -0
- package/dist/memory-embeddings.js +126 -0
- package/dist/memory-embeddings.js.map +1 -0
- package/dist/memory-recall.d.ts +8 -0
- package/dist/memory-recall.d.ts.map +1 -0
- package/dist/memory-recall.js +127 -0
- package/dist/memory-recall.js.map +1 -0
- package/dist/memory-store.d.ts +149 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +787 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/metrics.d.ts +12 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +134 -0
- package/dist/metrics.js.map +1 -0
- package/dist/model-registry.d.ts +67 -0
- package/dist/model-registry.d.ts.map +1 -0
- package/dist/model-registry.js +230 -0
- package/dist/model-registry.js.map +1 -0
- package/dist/mount-security.d.ts +37 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +284 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/paths.d.ts +80 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +149 -0
- package/dist/paths.js.map +1 -0
- package/dist/personalization.d.ts +6 -0
- package/dist/personalization.d.ts.map +1 -0
- package/dist/personalization.js +180 -0
- package/dist/personalization.js.map +1 -0
- package/dist/progress.d.ts +15 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +92 -0
- package/dist/progress.js.map +1 -0
- package/dist/runtime-config.d.ts +227 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +297 -0
- package/dist/runtime-config.js.map +1 -0
- package/dist/task-scheduler.d.ts +9 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +195 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/telegram-format.d.ts +3 -0
- package/dist/telegram-format.d.ts.map +1 -0
- package/dist/telegram-format.js +200 -0
- package/dist/telegram-format.js.map +1 -0
- package/dist/tool-budgets.d.ts +16 -0
- package/dist/tool-budgets.d.ts.map +1 -0
- package/dist/tool-budgets.js +83 -0
- package/dist/tool-budgets.js.map +1 -0
- package/dist/tool-policy.d.ts +18 -0
- package/dist/tool-policy.d.ts.map +1 -0
- package/dist/tool-policy.js +84 -0
- package/dist/tool-policy.js.map +1 -0
- package/dist/trace-writer.d.ts +39 -0
- package/dist/trace-writer.d.ts.map +1 -0
- package/dist/trace-writer.js +27 -0
- package/dist/trace-writer.js.map +1 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +30 -0
- package/dist/utils.js.map +1 -0
- package/launchd/com.dotclaw.plist +32 -0
- package/package.json +89 -0
- package/scripts/autotune.js +53 -0
- package/scripts/bootstrap.js +348 -0
- package/scripts/configure.js +200 -0
- package/scripts/doctor.js +164 -0
- package/scripts/init.js +209 -0
- package/scripts/install.sh +219 -0
- package/systemd/dotclaw.service +22 -0
|
@@ -0,0 +1,1720 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import dns from 'dns/promises';
|
|
5
|
+
import net from 'net';
|
|
6
|
+
import { tool as sdkTool, type Tool } from '@openrouter/sdk';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { createIpcHandlers, IpcContext } from './ipc.js';
|
|
9
|
+
import type { AgentRuntimeConfig } from './agent-config.js';
|
|
10
|
+
|
|
11
|
+
type ToolConfig = {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
inputSchema: z.ZodTypeAny;
|
|
15
|
+
outputSchema?: z.ZodTypeAny;
|
|
16
|
+
eventSchema?: z.ZodTypeAny;
|
|
17
|
+
nextTurnParams?: Record<string, unknown>;
|
|
18
|
+
requireApproval?: boolean | ((params: unknown, context: unknown) => boolean | Promise<boolean>);
|
|
19
|
+
execute: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const tool = sdkTool as unknown as (config: ToolConfig) => Tool;
|
|
23
|
+
|
|
24
|
+
type ToolRuntime = {
|
|
25
|
+
outputLimitBytes: number;
|
|
26
|
+
bashTimeoutMs: number;
|
|
27
|
+
bashOutputLimitBytes: number;
|
|
28
|
+
webfetchMaxBytes: number;
|
|
29
|
+
webfetchTimeoutMs: number;
|
|
30
|
+
websearchTimeoutMs: number;
|
|
31
|
+
pluginHttpTimeoutMs: number;
|
|
32
|
+
grepMaxFileBytes: number;
|
|
33
|
+
pluginMaxBytes: number;
|
|
34
|
+
toolSummary: {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
maxBytes: number;
|
|
37
|
+
model: string;
|
|
38
|
+
maxOutputTokens: number;
|
|
39
|
+
tools: string[];
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
};
|
|
42
|
+
openrouter: {
|
|
43
|
+
siteUrl: string;
|
|
44
|
+
siteName: string;
|
|
45
|
+
};
|
|
46
|
+
enableBash: boolean;
|
|
47
|
+
enableWebSearch: boolean;
|
|
48
|
+
enableWebFetch: boolean;
|
|
49
|
+
webfetchBlockPrivate: boolean;
|
|
50
|
+
webfetchAllowlist: string[];
|
|
51
|
+
webfetchBlocklist: string[];
|
|
52
|
+
pluginDirs: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function buildToolRuntime(config: AgentRuntimeConfig['agent']): ToolRuntime {
|
|
56
|
+
const webfetchAllowlist = (config.tools.webfetch.allowlist || [])
|
|
57
|
+
.map(normalizeDomain)
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
const webfetchBlocklist = (config.tools.webfetch.blocklist || [])
|
|
60
|
+
.map(normalizeDomain)
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
const pluginDirs = Array.from(new Set([
|
|
63
|
+
...config.tools.plugin.dirs,
|
|
64
|
+
...DEFAULT_PLUGIN_DIRS
|
|
65
|
+
]));
|
|
66
|
+
const toolSummaryTools = (config.tools.toolSummary.tools || [])
|
|
67
|
+
.map(toolName => toolName.trim().toLowerCase())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
const toolSummaryTimeoutMs = Math.min(config.openrouter.timeoutMs, 30_000);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
outputLimitBytes: config.tools.outputLimitBytes,
|
|
73
|
+
bashTimeoutMs: config.tools.bash.timeoutMs,
|
|
74
|
+
bashOutputLimitBytes: config.tools.bash.outputLimitBytes,
|
|
75
|
+
webfetchMaxBytes: config.tools.webfetch.maxBytes,
|
|
76
|
+
webfetchTimeoutMs: config.tools.webfetch.timeoutMs,
|
|
77
|
+
websearchTimeoutMs: config.tools.websearch.timeoutMs,
|
|
78
|
+
pluginHttpTimeoutMs: config.tools.plugin.httpTimeoutMs,
|
|
79
|
+
grepMaxFileBytes: config.tools.grepMaxFileBytes,
|
|
80
|
+
pluginMaxBytes: config.tools.plugin.maxBytes,
|
|
81
|
+
toolSummary: {
|
|
82
|
+
enabled: config.tools.toolSummary.enabled,
|
|
83
|
+
maxBytes: config.tools.toolSummary.maxBytes,
|
|
84
|
+
model: config.models.toolSummary,
|
|
85
|
+
maxOutputTokens: config.tools.toolSummary.maxOutputTokens,
|
|
86
|
+
tools: toolSummaryTools,
|
|
87
|
+
timeoutMs: toolSummaryTimeoutMs
|
|
88
|
+
},
|
|
89
|
+
openrouter: {
|
|
90
|
+
siteUrl: config.openrouter.siteUrl,
|
|
91
|
+
siteName: config.openrouter.siteName
|
|
92
|
+
},
|
|
93
|
+
enableBash: config.tools.enableBash,
|
|
94
|
+
enableWebSearch: config.tools.enableWebSearch,
|
|
95
|
+
enableWebFetch: config.tools.enableWebFetch,
|
|
96
|
+
webfetchBlockPrivate: config.tools.webfetch.blockPrivate,
|
|
97
|
+
webfetchAllowlist,
|
|
98
|
+
webfetchBlocklist,
|
|
99
|
+
pluginDirs
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const WORKSPACE_GROUP = '/workspace/group';
|
|
104
|
+
const WORKSPACE_GLOBAL = '/workspace/global';
|
|
105
|
+
const WORKSPACE_EXTRA = '/workspace/extra';
|
|
106
|
+
const WORKSPACE_PROJECT = '/workspace/project';
|
|
107
|
+
|
|
108
|
+
const DEFAULT_PLUGIN_DIRS = [
|
|
109
|
+
path.join(WORKSPACE_GROUP, 'plugins'),
|
|
110
|
+
path.join(WORKSPACE_GLOBAL, 'plugins')
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const PLUGIN_SCHEMA = z.object({
|
|
114
|
+
name: z.string().min(1),
|
|
115
|
+
description: z.string().min(1),
|
|
116
|
+
type: z.enum(['http', 'bash']),
|
|
117
|
+
method: z.string().optional(),
|
|
118
|
+
url: z.string().optional(),
|
|
119
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
120
|
+
query_params: z.record(z.string(), z.string()).optional(),
|
|
121
|
+
body: z.record(z.string(), z.any()).optional(),
|
|
122
|
+
command: z.string().optional(),
|
|
123
|
+
input: z.record(z.string(), z.enum(['string', 'number', 'boolean'])).optional(),
|
|
124
|
+
required: z.array(z.string()).optional()
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
type PluginConfig = z.infer<typeof PLUGIN_SCHEMA>;
|
|
128
|
+
|
|
129
|
+
export type ToolCallRecord = {
|
|
130
|
+
name: string;
|
|
131
|
+
args?: unknown;
|
|
132
|
+
ok: boolean;
|
|
133
|
+
duration_ms?: number;
|
|
134
|
+
error?: string;
|
|
135
|
+
output_bytes?: number;
|
|
136
|
+
output_truncated?: boolean;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type ToolCallLogger = (record: ToolCallRecord) => void;
|
|
140
|
+
|
|
141
|
+
export type ToolPolicy = {
|
|
142
|
+
allow?: string[];
|
|
143
|
+
deny?: string[];
|
|
144
|
+
max_per_run?: Record<string, number>;
|
|
145
|
+
default_max_per_run?: number;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function getAllowedRoots(isMain: boolean): string[] {
|
|
149
|
+
const roots = [WORKSPACE_GROUP, WORKSPACE_GLOBAL, WORKSPACE_EXTRA];
|
|
150
|
+
if (isMain) roots.push(WORKSPACE_PROJECT);
|
|
151
|
+
return roots.map(root => path.resolve(root));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isWithinRoot(targetPath: string, root: string): boolean {
|
|
155
|
+
return targetPath === root || targetPath.startsWith(`${root}${path.sep}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolvePath(inputPath: string, isMain: boolean, mustExist = false): string {
|
|
159
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
160
|
+
throw new Error('Path is required');
|
|
161
|
+
}
|
|
162
|
+
const roots = getAllowedRoots(isMain);
|
|
163
|
+
const resolved = path.isAbsolute(inputPath)
|
|
164
|
+
? path.resolve(inputPath)
|
|
165
|
+
: path.resolve(WORKSPACE_GROUP, inputPath);
|
|
166
|
+
|
|
167
|
+
if (!roots.some(root => isWithinRoot(resolved, root))) {
|
|
168
|
+
throw new Error(`Path is outside allowed roots: ${resolved}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (mustExist && !fs.existsSync(resolved)) {
|
|
172
|
+
throw new Error(`Path does not exist: ${resolved}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return resolved;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function limitText(text: string, maxBytes: number): { text: string; truncated: boolean } {
|
|
179
|
+
if (Buffer.byteLength(text, 'utf-8') <= maxBytes) {
|
|
180
|
+
return { text, truncated: false };
|
|
181
|
+
}
|
|
182
|
+
const buffer = Buffer.from(text, 'utf-8');
|
|
183
|
+
const truncated = buffer.subarray(0, maxBytes).toString('utf-8');
|
|
184
|
+
return { text: truncated, truncated: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeDomain(value: string): string {
|
|
188
|
+
let normalized = value.trim().toLowerCase();
|
|
189
|
+
normalized = normalized.replace(/^[a-z]+:\/\//, '');
|
|
190
|
+
normalized = normalized.split('/')[0];
|
|
191
|
+
normalized = normalized.split(':')[0];
|
|
192
|
+
return normalized;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function fetchWithTimeout(
|
|
196
|
+
url: string,
|
|
197
|
+
options: RequestInit,
|
|
198
|
+
timeoutMs: number,
|
|
199
|
+
label: string
|
|
200
|
+
): Promise<Response> {
|
|
201
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
202
|
+
return fetch(url, options);
|
|
203
|
+
}
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
206
|
+
try {
|
|
207
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const name = err instanceof Error ? err.name : '';
|
|
210
|
+
if (name === 'AbortError') {
|
|
211
|
+
throw new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
212
|
+
}
|
|
213
|
+
throw err;
|
|
214
|
+
} finally {
|
|
215
|
+
clearTimeout(timeout);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function fetchWithRedirects(params: {
|
|
220
|
+
url: string;
|
|
221
|
+
options: RequestInit;
|
|
222
|
+
timeoutMs: number;
|
|
223
|
+
label: string;
|
|
224
|
+
allowlist: string[];
|
|
225
|
+
blocklist: string[];
|
|
226
|
+
blockPrivate: boolean;
|
|
227
|
+
maxRedirects?: number;
|
|
228
|
+
}): Promise<Response> {
|
|
229
|
+
let currentUrl = params.url;
|
|
230
|
+
let options: RequestInit = { ...params.options, redirect: 'manual' };
|
|
231
|
+
const maxRedirects = Number.isFinite(params.maxRedirects) ? Number(params.maxRedirects) : 5;
|
|
232
|
+
let redirectsRemaining = maxRedirects;
|
|
233
|
+
|
|
234
|
+
while (true) {
|
|
235
|
+
const response = await fetchWithTimeout(currentUrl, options, params.timeoutMs, params.label);
|
|
236
|
+
const status = response.status;
|
|
237
|
+
const location = response.headers.get('location');
|
|
238
|
+
if (status >= 300 && status < 400 && location) {
|
|
239
|
+
if (redirectsRemaining <= 0) {
|
|
240
|
+
throw new Error(`Too many redirects fetching ${params.url}`);
|
|
241
|
+
}
|
|
242
|
+
redirectsRemaining -= 1;
|
|
243
|
+
const nextUrl = new URL(location, currentUrl).toString();
|
|
244
|
+
await assertUrlAllowed({
|
|
245
|
+
url: nextUrl,
|
|
246
|
+
allowlist: params.allowlist,
|
|
247
|
+
blocklist: params.blocklist,
|
|
248
|
+
blockPrivate: params.blockPrivate
|
|
249
|
+
});
|
|
250
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
251
|
+
const forceGet = status === 303 || ((status === 301 || status === 302) && method === 'POST');
|
|
252
|
+
if (forceGet) {
|
|
253
|
+
options = { ...options, method: 'GET' };
|
|
254
|
+
delete (options as { body?: unknown }).body;
|
|
255
|
+
}
|
|
256
|
+
currentUrl = nextUrl;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
return response;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isLocalHostname(hostname: string): boolean {
|
|
264
|
+
const normalized = hostname.toLowerCase();
|
|
265
|
+
return normalized === 'localhost'
|
|
266
|
+
|| normalized === 'ip6-localhost'
|
|
267
|
+
|| normalized.endsWith('.local')
|
|
268
|
+
|| normalized === 'metadata.google.internal'
|
|
269
|
+
|| normalized === 'metadata';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isPrivateIpv4(ip: string): boolean {
|
|
273
|
+
const parts = ip.split('.').map(part => parseInt(part, 10));
|
|
274
|
+
if (parts.length !== 4 || parts.some(part => Number.isNaN(part))) return false;
|
|
275
|
+
const [a, b] = parts;
|
|
276
|
+
if (a === 10) return true;
|
|
277
|
+
if (a === 127) return true;
|
|
278
|
+
if (a === 169 && b === 254) return true;
|
|
279
|
+
if (a === 192 && b === 168) return true;
|
|
280
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
281
|
+
if (a === 100 && b >= 64 && b <= 127) return true; // carrier-grade NAT
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isPrivateIpv6(ip: string): boolean {
|
|
286
|
+
const normalized = ip.toLowerCase();
|
|
287
|
+
return normalized === '::1'
|
|
288
|
+
|| normalized.startsWith('fc')
|
|
289
|
+
|| normalized.startsWith('fd')
|
|
290
|
+
|| normalized.startsWith('fe80')
|
|
291
|
+
|| normalized.startsWith('::ffff:127.')
|
|
292
|
+
|| normalized.startsWith('::ffff:10.')
|
|
293
|
+
|| normalized.startsWith('::ffff:192.168.')
|
|
294
|
+
|| normalized.startsWith('::ffff:172.');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isPrivateIp(ip: string): boolean {
|
|
298
|
+
const version = net.isIP(ip);
|
|
299
|
+
if (version === 4) return isPrivateIpv4(ip);
|
|
300
|
+
if (version === 6) return isPrivateIpv6(ip);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function resolveIps(hostname: string): Promise<string[]> {
|
|
305
|
+
try {
|
|
306
|
+
const results = await dns.lookup(hostname, { all: true });
|
|
307
|
+
return results.map(record => record.address);
|
|
308
|
+
} catch {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function assertUrlAllowed(params: {
|
|
314
|
+
url: string;
|
|
315
|
+
allowlist: string[];
|
|
316
|
+
blocklist: string[];
|
|
317
|
+
blockPrivate: boolean;
|
|
318
|
+
}) {
|
|
319
|
+
let hostname: string;
|
|
320
|
+
try {
|
|
321
|
+
hostname = new URL(params.url).hostname.toLowerCase();
|
|
322
|
+
} catch {
|
|
323
|
+
throw new Error(`Invalid URL: ${params.url}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const isBlocked = params.blocklist.some(domain =>
|
|
327
|
+
hostname === domain || hostname.endsWith(`.${domain}`)
|
|
328
|
+
);
|
|
329
|
+
if (isBlocked) {
|
|
330
|
+
throw new Error(`WebFetch blocked for host: ${hostname}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (params.allowlist.length > 0) {
|
|
334
|
+
const isAllowed = params.allowlist.some(domain =>
|
|
335
|
+
hostname === domain || hostname.endsWith(`.${domain}`)
|
|
336
|
+
);
|
|
337
|
+
if (!isAllowed) {
|
|
338
|
+
throw new Error(`WebFetch not allowed for host: ${hostname}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (params.blockPrivate) {
|
|
343
|
+
if (isLocalHostname(hostname)) {
|
|
344
|
+
throw new Error(`WebFetch blocked for local host: ${hostname}`);
|
|
345
|
+
}
|
|
346
|
+
if (isPrivateIp(hostname)) {
|
|
347
|
+
throw new Error(`WebFetch blocked for private IP: ${hostname}`);
|
|
348
|
+
}
|
|
349
|
+
const resolved = await resolveIps(hostname);
|
|
350
|
+
for (const ip of resolved) {
|
|
351
|
+
if (isPrivateIp(ip)) {
|
|
352
|
+
throw new Error(`WebFetch blocked for private IP: ${ip}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function sanitizeToolArgs(args: unknown): unknown {
|
|
359
|
+
if (!args || typeof args !== 'object') return args;
|
|
360
|
+
const record = { ...(args as Record<string, unknown>) };
|
|
361
|
+
|
|
362
|
+
if ('content' in record && typeof record.content === 'string') {
|
|
363
|
+
record.content = `<redacted:${(record.content as string).length}>`;
|
|
364
|
+
}
|
|
365
|
+
if ('text' in record && typeof record.text === 'string') {
|
|
366
|
+
record.text = `<redacted:${(record.text as string).length}>`;
|
|
367
|
+
}
|
|
368
|
+
if ('old_text' in record && typeof record.old_text === 'string') {
|
|
369
|
+
record.old_text = `<redacted:${(record.old_text as string).length}>`;
|
|
370
|
+
}
|
|
371
|
+
if ('new_text' in record && typeof record.new_text === 'string') {
|
|
372
|
+
record.new_text = `<redacted:${(record.new_text as string).length}>`;
|
|
373
|
+
}
|
|
374
|
+
if ('command' in record && typeof record.command === 'string') {
|
|
375
|
+
record.command = (record.command as string).slice(0, 200);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return record;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function toPosixPath(inputPath: string): string {
|
|
382
|
+
return inputPath.split(path.sep).join('/');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function globToRegex(pattern: string): RegExp {
|
|
386
|
+
let regex = '';
|
|
387
|
+
let i = 0;
|
|
388
|
+
while (i < pattern.length) {
|
|
389
|
+
const char = pattern[i];
|
|
390
|
+
if (char === '*') {
|
|
391
|
+
const next = pattern[i + 1];
|
|
392
|
+
if (next === '*') {
|
|
393
|
+
const nextNext = pattern[i + 2];
|
|
394
|
+
if (nextNext === '/') {
|
|
395
|
+
regex += '(?:.*/)?';
|
|
396
|
+
i += 3;
|
|
397
|
+
} else {
|
|
398
|
+
regex += '.*';
|
|
399
|
+
i += 2;
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
regex += '[^/]*';
|
|
404
|
+
i += 1;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (char === '?') {
|
|
408
|
+
regex += '[^/]';
|
|
409
|
+
i += 1;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if ('\\^$+?.()|{}[]'.includes(char)) {
|
|
413
|
+
regex += `\\${char}`;
|
|
414
|
+
} else {
|
|
415
|
+
regex += char;
|
|
416
|
+
}
|
|
417
|
+
i += 1;
|
|
418
|
+
}
|
|
419
|
+
return new RegExp(`^${regex}$`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function expandEnv(value: string): string {
|
|
423
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_match, key: string) => {
|
|
424
|
+
return process.env[key] || '';
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function interpolateTemplate(value: string, args: Record<string, unknown>): string {
|
|
429
|
+
const expanded = expandEnv(value);
|
|
430
|
+
return expanded.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_match, key: string) => {
|
|
431
|
+
const replacement = args[key];
|
|
432
|
+
if (replacement === undefined || replacement === null) return '';
|
|
433
|
+
return String(replacement);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function shellEscape(value: string): string {
|
|
438
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildInputSchema(config: PluginConfig) {
|
|
442
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
443
|
+
const input = config.input || {};
|
|
444
|
+
const required = new Set(config.required || []);
|
|
445
|
+
|
|
446
|
+
for (const [key, value] of Object.entries(input)) {
|
|
447
|
+
let schema: z.ZodTypeAny;
|
|
448
|
+
if (value === 'number') schema = z.number();
|
|
449
|
+
else if (value === 'boolean') schema = z.boolean();
|
|
450
|
+
else schema = z.string();
|
|
451
|
+
shape[key] = required.has(key) ? schema : schema.optional();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return z.object(shape).passthrough();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function loadPluginConfigs(dirs: string[]): PluginConfig[] {
|
|
458
|
+
const configs: PluginConfig[] = [];
|
|
459
|
+
|
|
460
|
+
for (const dir of dirs) {
|
|
461
|
+
if (!fs.existsSync(dir)) continue;
|
|
462
|
+
const files = fs.readdirSync(dir).filter(file => file.endsWith('.json'));
|
|
463
|
+
for (const file of files) {
|
|
464
|
+
try {
|
|
465
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8'));
|
|
466
|
+
const parsed = PLUGIN_SCHEMA.parse(raw);
|
|
467
|
+
configs.push(parsed);
|
|
468
|
+
} catch {
|
|
469
|
+
// ignore malformed plugin
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return configs;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getSearchRoot(patternPosix: string): string {
|
|
478
|
+
const globIndex = patternPosix.search(/[*?]/);
|
|
479
|
+
if (globIndex === -1) {
|
|
480
|
+
return patternPosix;
|
|
481
|
+
}
|
|
482
|
+
const slashIndex = patternPosix.lastIndexOf('/', globIndex);
|
|
483
|
+
if (slashIndex <= 0) return '/';
|
|
484
|
+
return patternPosix.slice(0, slashIndex);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function walkFileTree(
|
|
488
|
+
rootPath: string,
|
|
489
|
+
options: { includeFiles: boolean; includeDirs: boolean; maxResults: number }
|
|
490
|
+
): string[] {
|
|
491
|
+
const results: string[] = [];
|
|
492
|
+
const stack: string[] = [rootPath];
|
|
493
|
+
|
|
494
|
+
while (stack.length > 0 && results.length < options.maxResults) {
|
|
495
|
+
const current = stack.pop();
|
|
496
|
+
if (!current) continue;
|
|
497
|
+
let stats: fs.Stats;
|
|
498
|
+
try {
|
|
499
|
+
stats = fs.lstatSync(current);
|
|
500
|
+
} catch {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (stats.isSymbolicLink()) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (stats.isDirectory()) {
|
|
507
|
+
if (options.includeDirs) results.push(current);
|
|
508
|
+
let entries: fs.Dirent[];
|
|
509
|
+
try {
|
|
510
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
511
|
+
} catch {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
for (const entry of entries) {
|
|
515
|
+
if (results.length >= options.maxResults) break;
|
|
516
|
+
const nextPath = path.join(current, entry.name);
|
|
517
|
+
if (entry.isSymbolicLink()) continue;
|
|
518
|
+
if (entry.isDirectory()) {
|
|
519
|
+
stack.push(nextPath);
|
|
520
|
+
} else if (entry.isFile()) {
|
|
521
|
+
if (options.includeFiles) results.push(nextPath);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} else if (stats.isFile()) {
|
|
525
|
+
if (options.includeFiles) results.push(current);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return results;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function runCommand(command: string, timeoutMs: number, outputLimit: number, cwd = WORKSPACE_GROUP) {
|
|
533
|
+
return new Promise<{
|
|
534
|
+
stdout: string;
|
|
535
|
+
stderr: string;
|
|
536
|
+
exitCode: number | null;
|
|
537
|
+
durationMs: number;
|
|
538
|
+
truncated: boolean;
|
|
539
|
+
}>((resolve) => {
|
|
540
|
+
const start = Date.now();
|
|
541
|
+
const child = spawn('/bin/bash', ['-lc', command], {
|
|
542
|
+
cwd,
|
|
543
|
+
env: process.env
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
let stdout = '';
|
|
547
|
+
let stderr = '';
|
|
548
|
+
let truncated = false;
|
|
549
|
+
const maxBytes = outputLimit;
|
|
550
|
+
|
|
551
|
+
const append = (chunk: Buffer | string, isStdout: boolean) => {
|
|
552
|
+
if (truncated) return;
|
|
553
|
+
const text = chunk.toString();
|
|
554
|
+
const remaining = maxBytes - Buffer.byteLength(stdout + stderr, 'utf-8');
|
|
555
|
+
if (remaining <= 0) {
|
|
556
|
+
truncated = true;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const toAdd = Buffer.byteLength(text, 'utf-8') > remaining
|
|
560
|
+
? Buffer.from(text).subarray(0, remaining).toString('utf-8')
|
|
561
|
+
: text;
|
|
562
|
+
if (isStdout) {
|
|
563
|
+
stdout += toAdd;
|
|
564
|
+
} else {
|
|
565
|
+
stderr += toAdd;
|
|
566
|
+
}
|
|
567
|
+
if (Buffer.byteLength(stdout + stderr, 'utf-8') >= maxBytes) {
|
|
568
|
+
truncated = true;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
child.stdout.on('data', (data) => append(data, true));
|
|
573
|
+
child.stderr.on('data', (data) => append(data, false));
|
|
574
|
+
|
|
575
|
+
const timeout = setTimeout(() => {
|
|
576
|
+
child.kill('SIGKILL');
|
|
577
|
+
}, timeoutMs);
|
|
578
|
+
|
|
579
|
+
child.on('close', (code) => {
|
|
580
|
+
clearTimeout(timeout);
|
|
581
|
+
resolve({
|
|
582
|
+
stdout,
|
|
583
|
+
stderr,
|
|
584
|
+
exitCode: code,
|
|
585
|
+
durationMs: Date.now() - start,
|
|
586
|
+
truncated
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
child.on('error', (err) => {
|
|
591
|
+
clearTimeout(timeout);
|
|
592
|
+
resolve({
|
|
593
|
+
stdout,
|
|
594
|
+
stderr: `${stderr}\n${err instanceof Error ? err.message : String(err)}`.trim(),
|
|
595
|
+
exitCode: 1,
|
|
596
|
+
durationMs: Date.now() - start,
|
|
597
|
+
truncated
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function readFileSafe(filePath: string, maxBytes: number) {
|
|
604
|
+
const stat = fs.statSync(filePath);
|
|
605
|
+
if (stat.size <= maxBytes) {
|
|
606
|
+
return { content: fs.readFileSync(filePath, 'utf-8'), truncated: false, size: stat.size };
|
|
607
|
+
}
|
|
608
|
+
const fd = fs.openSync(filePath, 'r');
|
|
609
|
+
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
610
|
+
const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
|
|
611
|
+
fs.closeSync(fd);
|
|
612
|
+
return { content: buffer.subarray(0, bytesRead).toString('utf-8'), truncated: true, size: stat.size };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function readResponseWithLimit(response: Response, maxBytes: number): Promise<{ body: Buffer; truncated: boolean }> {
|
|
616
|
+
const reader = response.body?.getReader();
|
|
617
|
+
if (!reader) {
|
|
618
|
+
const buffer = await response.arrayBuffer();
|
|
619
|
+
if (buffer.byteLength <= maxBytes) {
|
|
620
|
+
return { body: Buffer.from(buffer), truncated: false };
|
|
621
|
+
}
|
|
622
|
+
return { body: Buffer.from(buffer).subarray(0, maxBytes), truncated: true };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const chunks: Uint8Array[] = [];
|
|
626
|
+
let total = 0;
|
|
627
|
+
let truncated = false;
|
|
628
|
+
|
|
629
|
+
while (true) {
|
|
630
|
+
const { done, value } = await reader.read();
|
|
631
|
+
if (done) break;
|
|
632
|
+
if (!value || value.byteLength === 0) continue;
|
|
633
|
+
const remaining = maxBytes - total;
|
|
634
|
+
if (remaining <= 0) {
|
|
635
|
+
truncated = true;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
if (value.byteLength > remaining) {
|
|
639
|
+
chunks.push(value.subarray(0, remaining));
|
|
640
|
+
total += remaining;
|
|
641
|
+
truncated = true;
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
chunks.push(value);
|
|
645
|
+
total += value.byteLength;
|
|
646
|
+
if (total >= maxBytes) {
|
|
647
|
+
truncated = true;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (truncated) {
|
|
653
|
+
try {
|
|
654
|
+
await reader.cancel();
|
|
655
|
+
} catch {
|
|
656
|
+
// ignore
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return { body: Buffer.concat(chunks, total), truncated };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
type ToolSummaryPayload = {
|
|
664
|
+
text: string;
|
|
665
|
+
metadata: {
|
|
666
|
+
toolName: string;
|
|
667
|
+
url?: string;
|
|
668
|
+
status?: number;
|
|
669
|
+
contentType?: string | null;
|
|
670
|
+
truncated?: boolean;
|
|
671
|
+
};
|
|
672
|
+
apply: (summary: string) => unknown;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
function toolSummaryMatches(name: string, patterns: string[]): boolean {
|
|
676
|
+
if (patterns.length === 0) return false;
|
|
677
|
+
const normalized = name.toLowerCase();
|
|
678
|
+
for (const pattern of patterns) {
|
|
679
|
+
if (pattern === '*') return true;
|
|
680
|
+
if (pattern.endsWith('*')) {
|
|
681
|
+
const prefix = pattern.slice(0, -1);
|
|
682
|
+
if (normalized.startsWith(prefix)) return true;
|
|
683
|
+
} else if (normalized === pattern) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function getToolSummaryPayload(name: string, result: unknown): ToolSummaryPayload | null {
|
|
691
|
+
if (!result || typeof result !== 'object') return null;
|
|
692
|
+
const record = result as Record<string, unknown>;
|
|
693
|
+
|
|
694
|
+
if (name === 'WebFetch' && typeof record.content === 'string') {
|
|
695
|
+
return {
|
|
696
|
+
text: record.content,
|
|
697
|
+
metadata: {
|
|
698
|
+
toolName: name,
|
|
699
|
+
url: typeof record.url === 'string' ? record.url : undefined,
|
|
700
|
+
status: typeof record.status === 'number' ? record.status : undefined,
|
|
701
|
+
contentType: typeof record.contentType === 'string' ? record.contentType : undefined,
|
|
702
|
+
truncated: Boolean(record.truncated)
|
|
703
|
+
},
|
|
704
|
+
apply: (summary) => ({
|
|
705
|
+
...record,
|
|
706
|
+
content: summary,
|
|
707
|
+
truncated: true
|
|
708
|
+
})
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (name.startsWith('plugin__') && typeof record.body === 'string') {
|
|
713
|
+
return {
|
|
714
|
+
text: record.body,
|
|
715
|
+
metadata: {
|
|
716
|
+
toolName: name,
|
|
717
|
+
status: typeof record.status === 'number' ? record.status : undefined,
|
|
718
|
+
contentType: typeof record.contentType === 'string' ? record.contentType : undefined,
|
|
719
|
+
truncated: Boolean(record.truncated)
|
|
720
|
+
},
|
|
721
|
+
apply: (summary) => ({
|
|
722
|
+
...record,
|
|
723
|
+
body: summary,
|
|
724
|
+
truncated: true
|
|
725
|
+
})
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function buildOpenRouterHeaders(runtime: ToolRuntime): Record<string, string> | null {
|
|
733
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
734
|
+
if (!apiKey) return null;
|
|
735
|
+
const headers: Record<string, string> = {
|
|
736
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
737
|
+
'Content-Type': 'application/json'
|
|
738
|
+
};
|
|
739
|
+
if (runtime.openrouter.siteUrl) {
|
|
740
|
+
headers['HTTP-Referer'] = runtime.openrouter.siteUrl;
|
|
741
|
+
}
|
|
742
|
+
if (runtime.openrouter.siteName) {
|
|
743
|
+
headers['X-Title'] = runtime.openrouter.siteName;
|
|
744
|
+
}
|
|
745
|
+
return headers;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function summarizeToolOutput(payload: ToolSummaryPayload, runtime: ToolRuntime, maxInputBytes: number): Promise<string | null> {
|
|
749
|
+
const headers = buildOpenRouterHeaders(runtime);
|
|
750
|
+
if (!headers) return null;
|
|
751
|
+
|
|
752
|
+
const { text, truncated: inputTruncated } = limitText(payload.text, maxInputBytes);
|
|
753
|
+
if (!text.trim()) return null;
|
|
754
|
+
|
|
755
|
+
const metadataLines = [
|
|
756
|
+
`Tool: ${payload.metadata.toolName}`,
|
|
757
|
+
payload.metadata.url ? `URL: ${payload.metadata.url}` : null,
|
|
758
|
+
typeof payload.metadata.status === 'number' ? `Status: ${payload.metadata.status}` : null,
|
|
759
|
+
payload.metadata.contentType ? `Content-Type: ${payload.metadata.contentType}` : null,
|
|
760
|
+
`Original bytes: ${Buffer.byteLength(payload.text, 'utf-8')}`,
|
|
761
|
+
`Original truncated: ${payload.metadata.truncated ? 'true' : 'false'}`,
|
|
762
|
+
`Input truncated for summary: ${inputTruncated ? 'true' : 'false'}`
|
|
763
|
+
].filter(Boolean).join('\n');
|
|
764
|
+
|
|
765
|
+
const systemPrompt = [
|
|
766
|
+
'You summarize tool output for downstream reasoning.',
|
|
767
|
+
'Return a concise, factual summary with key entities, products, and dates.',
|
|
768
|
+
'Use short bullet points when helpful.',
|
|
769
|
+
'If the content is incomplete or truncated, mention that clearly.'
|
|
770
|
+
].join(' ');
|
|
771
|
+
|
|
772
|
+
const userPrompt = `${metadataLines}\n\nContent:\n${text}`;
|
|
773
|
+
|
|
774
|
+
const controller = new AbortController();
|
|
775
|
+
const timeout = setTimeout(() => controller.abort(), runtime.toolSummary.timeoutMs);
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
779
|
+
method: 'POST',
|
|
780
|
+
headers,
|
|
781
|
+
body: JSON.stringify({
|
|
782
|
+
model: runtime.toolSummary.model,
|
|
783
|
+
messages: [
|
|
784
|
+
{ role: 'system', content: systemPrompt },
|
|
785
|
+
{ role: 'user', content: userPrompt }
|
|
786
|
+
],
|
|
787
|
+
max_tokens: runtime.toolSummary.maxOutputTokens,
|
|
788
|
+
temperature: 0.2
|
|
789
|
+
}),
|
|
790
|
+
signal: controller.signal
|
|
791
|
+
});
|
|
792
|
+
const bodyText = await response.text();
|
|
793
|
+
if (!response.ok) {
|
|
794
|
+
console.error(`[agent-runner] Tool summary failed (${response.status}): ${bodyText.slice(0, 300)}`);
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const data = JSON.parse(bodyText);
|
|
798
|
+
const content = data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text;
|
|
799
|
+
if (!content || !String(content).trim()) return null;
|
|
800
|
+
const summary = String(content).trim();
|
|
801
|
+
return `Summary:\n${summary}`;
|
|
802
|
+
} catch (err) {
|
|
803
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
804
|
+
console.error('[agent-runner] Tool summary timed out');
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
} finally {
|
|
808
|
+
clearTimeout(timeout);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function maybeSummarizeToolResult<T>(name: string, result: T, runtime: ToolRuntime): Promise<T> {
|
|
813
|
+
if (!runtime.toolSummary.enabled) return result;
|
|
814
|
+
if (!toolSummaryMatches(name, runtime.toolSummary.tools)) return result;
|
|
815
|
+
const payload = getToolSummaryPayload(name, result);
|
|
816
|
+
if (!payload) return result;
|
|
817
|
+
const contentBytes = Buffer.byteLength(payload.text, 'utf-8');
|
|
818
|
+
if (!Number.isFinite(runtime.toolSummary.maxBytes) || contentBytes <= runtime.toolSummary.maxBytes) {
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
const summary = await summarizeToolOutput(payload, runtime, runtime.toolSummary.maxBytes);
|
|
822
|
+
if (!summary) return result;
|
|
823
|
+
const limited = limitText(summary, runtime.outputLimitBytes);
|
|
824
|
+
return payload.apply(limited.text) as T;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function createTools(ctx: IpcContext, config: AgentRuntimeConfig['agent'], options?: { onToolCall?: ToolCallLogger; policy?: ToolPolicy }) {
|
|
828
|
+
const runtime = buildToolRuntime(config);
|
|
829
|
+
const ipc = createIpcHandlers(ctx, config.ipc);
|
|
830
|
+
const isMain = ctx.isMain;
|
|
831
|
+
const onToolCall = options?.onToolCall;
|
|
832
|
+
const policy = options?.policy;
|
|
833
|
+
const allowList = (policy?.allow || []).map(item => item.toLowerCase());
|
|
834
|
+
const denyList = (policy?.deny || []).map(item => item.toLowerCase());
|
|
835
|
+
const maxPerRunConfig = policy?.max_per_run || {};
|
|
836
|
+
const maxPerRun = new Map<string, number>();
|
|
837
|
+
for (const [key, value] of Object.entries(maxPerRunConfig)) {
|
|
838
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
839
|
+
maxPerRun.set(key.toLowerCase(), value);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const defaultMax = policy?.default_max_per_run ?? 12;
|
|
843
|
+
const usageCounts = new Map<string, number>();
|
|
844
|
+
|
|
845
|
+
const enableBash = runtime.enableBash;
|
|
846
|
+
const enableWebSearch = runtime.enableWebSearch;
|
|
847
|
+
const enableWebFetch = runtime.enableWebFetch;
|
|
848
|
+
const blockPrivate = runtime.webfetchBlockPrivate;
|
|
849
|
+
const webFetchAllowlist = runtime.webfetchAllowlist;
|
|
850
|
+
const webFetchBlocklist = runtime.webfetchBlocklist;
|
|
851
|
+
|
|
852
|
+
const wrapExecute = <TInput, TOutput>(name: string, execute: (args: TInput) => Promise<TOutput>) => {
|
|
853
|
+
return async (args: TInput): Promise<TOutput> => {
|
|
854
|
+
const start = Date.now();
|
|
855
|
+
try {
|
|
856
|
+
const normalized = name.toLowerCase();
|
|
857
|
+
if (denyList.includes(normalized)) {
|
|
858
|
+
throw new Error(`Tool is disabled by policy: ${name}`);
|
|
859
|
+
}
|
|
860
|
+
if (allowList.length > 0 && !allowList.includes(normalized)) {
|
|
861
|
+
throw new Error(`Tool not allowed by policy: ${name}`);
|
|
862
|
+
}
|
|
863
|
+
const currentCount = usageCounts.get(name) || 0;
|
|
864
|
+
const maxAllowed = maxPerRun.get(normalized) ?? defaultMax;
|
|
865
|
+
if (Number.isFinite(maxAllowed) && maxAllowed > 0 && currentCount >= maxAllowed) {
|
|
866
|
+
throw new Error(`Tool usage limit reached for ${name} (max ${maxAllowed} per run)`);
|
|
867
|
+
}
|
|
868
|
+
usageCounts.set(name, currentCount + 1);
|
|
869
|
+
|
|
870
|
+
const rawResult = await execute(args);
|
|
871
|
+
const result = await maybeSummarizeToolResult(name, rawResult, runtime);
|
|
872
|
+
let outputBytes: number | undefined;
|
|
873
|
+
let outputTruncated: boolean | undefined;
|
|
874
|
+
try {
|
|
875
|
+
const serialized = JSON.stringify(result);
|
|
876
|
+
outputBytes = Buffer.byteLength(serialized, 'utf-8');
|
|
877
|
+
} catch {
|
|
878
|
+
// ignore serialization failure
|
|
879
|
+
}
|
|
880
|
+
if (result && typeof result === 'object' && 'truncated' in (result as Record<string, unknown>)) {
|
|
881
|
+
outputTruncated = Boolean((result as Record<string, unknown>).truncated);
|
|
882
|
+
}
|
|
883
|
+
onToolCall?.({
|
|
884
|
+
name,
|
|
885
|
+
args: sanitizeToolArgs(args),
|
|
886
|
+
ok: true,
|
|
887
|
+
duration_ms: Date.now() - start,
|
|
888
|
+
output_bytes: outputBytes,
|
|
889
|
+
output_truncated: outputTruncated
|
|
890
|
+
});
|
|
891
|
+
return result;
|
|
892
|
+
} catch (err) {
|
|
893
|
+
onToolCall?.({
|
|
894
|
+
name,
|
|
895
|
+
args: sanitizeToolArgs(args),
|
|
896
|
+
ok: false,
|
|
897
|
+
duration_ms: Date.now() - start,
|
|
898
|
+
error: err instanceof Error ? err.message : String(err)
|
|
899
|
+
});
|
|
900
|
+
throw err;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const bashTool = tool({
|
|
906
|
+
name: 'Bash',
|
|
907
|
+
description: 'Run a shell command inside the container. CWD is /workspace/group.',
|
|
908
|
+
inputSchema: z.object({
|
|
909
|
+
command: z.string().describe('Command to run'),
|
|
910
|
+
timeoutMs: z.number().int().positive().optional().describe('Timeout in milliseconds')
|
|
911
|
+
}),
|
|
912
|
+
outputSchema: z.object({
|
|
913
|
+
stdout: z.string(),
|
|
914
|
+
stderr: z.string(),
|
|
915
|
+
exitCode: z.number().int().nullable(),
|
|
916
|
+
durationMs: z.number(),
|
|
917
|
+
truncated: z.boolean()
|
|
918
|
+
}),
|
|
919
|
+
execute: wrapExecute('Bash', async ({ command, timeoutMs }: { command: string; timeoutMs?: number }) => {
|
|
920
|
+
return runCommand(command, timeoutMs || runtime.bashTimeoutMs, runtime.bashOutputLimitBytes);
|
|
921
|
+
})
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
const pythonTool = tool({
|
|
925
|
+
name: 'Python',
|
|
926
|
+
description: 'Execute Python code in a sandboxed environment. Available packages: pandas, numpy, requests, beautifulsoup4, matplotlib. CWD is /workspace/group.',
|
|
927
|
+
inputSchema: z.object({
|
|
928
|
+
code: z.string().describe('Python code to execute'),
|
|
929
|
+
timeoutMs: z.number().int().positive().optional().describe('Timeout in milliseconds (default 30000)')
|
|
930
|
+
}),
|
|
931
|
+
outputSchema: z.object({
|
|
932
|
+
stdout: z.string(),
|
|
933
|
+
stderr: z.string(),
|
|
934
|
+
exitCode: z.number().int().nullable(),
|
|
935
|
+
durationMs: z.number(),
|
|
936
|
+
truncated: z.boolean()
|
|
937
|
+
}),
|
|
938
|
+
execute: wrapExecute('Python', async ({ code, timeoutMs }: { code: string; timeoutMs?: number }) => {
|
|
939
|
+
// Write code to a temp file and execute it
|
|
940
|
+
const tempFile = `/tmp/script_${Date.now()}_${Math.random().toString(36).slice(2)}.py`;
|
|
941
|
+
fs.writeFileSync(tempFile, code);
|
|
942
|
+
try {
|
|
943
|
+
const result = await runCommand(`python3 ${tempFile}`, timeoutMs || 30000, runtime.bashOutputLimitBytes);
|
|
944
|
+
return result;
|
|
945
|
+
} finally {
|
|
946
|
+
try {
|
|
947
|
+
fs.unlinkSync(tempFile);
|
|
948
|
+
} catch {
|
|
949
|
+
// Ignore cleanup errors
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
const gitCloneTool = tool({
|
|
956
|
+
name: 'GitClone',
|
|
957
|
+
description: 'Clone a git repository into the workspace.',
|
|
958
|
+
inputSchema: z.object({
|
|
959
|
+
repo: z.string().describe('Git repository URL'),
|
|
960
|
+
dest: z.string().optional().describe('Destination path (relative to /workspace/group)'),
|
|
961
|
+
depth: z.number().int().positive().optional().describe('Shallow clone depth'),
|
|
962
|
+
branch: z.string().optional().describe('Branch or tag to checkout')
|
|
963
|
+
}),
|
|
964
|
+
outputSchema: z.object({
|
|
965
|
+
path: z.string(),
|
|
966
|
+
stdout: z.string(),
|
|
967
|
+
stderr: z.string(),
|
|
968
|
+
exitCode: z.number().int().nullable(),
|
|
969
|
+
durationMs: z.number(),
|
|
970
|
+
truncated: z.boolean()
|
|
971
|
+
}),
|
|
972
|
+
execute: wrapExecute('GitClone', async ({ repo, dest, depth, branch }: { repo: string; dest?: string; depth?: number; branch?: string }) => {
|
|
973
|
+
const targetPath = dest
|
|
974
|
+
? resolvePath(dest, isMain, false)
|
|
975
|
+
: resolvePath(path.basename(repo.replace(/\.git$/, '')) || 'repo', isMain, false);
|
|
976
|
+
if (fs.existsSync(targetPath)) {
|
|
977
|
+
throw new Error(`Destination already exists: ${targetPath}`);
|
|
978
|
+
}
|
|
979
|
+
const args = [
|
|
980
|
+
'git', 'clone',
|
|
981
|
+
depth ? `--depth ${Math.floor(depth)}` : '',
|
|
982
|
+
branch ? `--branch ${shellEscape(branch)}` : '',
|
|
983
|
+
shellEscape(repo),
|
|
984
|
+
shellEscape(targetPath)
|
|
985
|
+
].filter(Boolean).join(' ');
|
|
986
|
+
return runCommand(args, runtime.bashTimeoutMs, runtime.bashOutputLimitBytes);
|
|
987
|
+
})
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
const npmInstallTool = tool({
|
|
991
|
+
name: 'NpmInstall',
|
|
992
|
+
description: 'Install npm packages in the workspace.',
|
|
993
|
+
inputSchema: z.object({
|
|
994
|
+
packages: z.array(z.string()).optional().describe('Packages to install (default: install from package.json)'),
|
|
995
|
+
dev: z.boolean().optional().describe('Install as dev dependencies'),
|
|
996
|
+
path: z.string().optional().describe('Working directory (relative to /workspace/group)')
|
|
997
|
+
}),
|
|
998
|
+
outputSchema: z.object({
|
|
999
|
+
stdout: z.string(),
|
|
1000
|
+
stderr: z.string(),
|
|
1001
|
+
exitCode: z.number().int().nullable(),
|
|
1002
|
+
durationMs: z.number(),
|
|
1003
|
+
truncated: z.boolean()
|
|
1004
|
+
}),
|
|
1005
|
+
execute: wrapExecute('NpmInstall', async ({ packages, dev, path: workdir }: { packages?: string[]; dev?: boolean; path?: string }) => {
|
|
1006
|
+
const cwd = workdir ? resolvePath(workdir, isMain, true) : WORKSPACE_GROUP;
|
|
1007
|
+
const pkgList = packages && packages.length > 0 ? packages.map(shellEscape).join(' ') : '';
|
|
1008
|
+
const devFlag = dev ? '--save-dev' : '';
|
|
1009
|
+
const command = `npm install ${devFlag} ${pkgList}`.trim();
|
|
1010
|
+
return runCommand(command, runtime.bashTimeoutMs, runtime.bashOutputLimitBytes, cwd);
|
|
1011
|
+
})
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
const readTool = tool({
|
|
1015
|
+
name: 'Read',
|
|
1016
|
+
description: 'Read a file from the mounted workspace.',
|
|
1017
|
+
inputSchema: z.object({
|
|
1018
|
+
path: z.string().describe('File path (relative to /workspace/group or absolute within mounts)'),
|
|
1019
|
+
maxBytes: z.number().int().positive().optional().describe('Maximum bytes to read')
|
|
1020
|
+
}),
|
|
1021
|
+
outputSchema: z.object({
|
|
1022
|
+
path: z.string(),
|
|
1023
|
+
content: z.string(),
|
|
1024
|
+
truncated: z.boolean(),
|
|
1025
|
+
size: z.number()
|
|
1026
|
+
}),
|
|
1027
|
+
execute: wrapExecute('Read', async ({ path: inputPath, maxBytes }: { path: string; maxBytes?: number }) => {
|
|
1028
|
+
const resolved = resolvePath(inputPath, isMain, true);
|
|
1029
|
+
const { content, truncated, size } = await readFileSafe(resolved, Math.min(maxBytes || runtime.outputLimitBytes, runtime.outputLimitBytes));
|
|
1030
|
+
return { path: resolved, content, truncated, size };
|
|
1031
|
+
})
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const writeTool = tool({
|
|
1035
|
+
name: 'Write',
|
|
1036
|
+
description: 'Write a file to the mounted workspace.',
|
|
1037
|
+
inputSchema: z.object({
|
|
1038
|
+
path: z.string().describe('File path (relative to /workspace/group or absolute within mounts)'),
|
|
1039
|
+
content: z.string().describe('File contents'),
|
|
1040
|
+
overwrite: z.boolean().optional().describe('Overwrite if file exists (default true)')
|
|
1041
|
+
}),
|
|
1042
|
+
outputSchema: z.object({
|
|
1043
|
+
path: z.string(),
|
|
1044
|
+
bytesWritten: z.number()
|
|
1045
|
+
}),
|
|
1046
|
+
execute: wrapExecute('Write', async ({ path: inputPath, content, overwrite }: { path: string; content: string; overwrite?: boolean }) => {
|
|
1047
|
+
const resolved = resolvePath(inputPath, isMain, false);
|
|
1048
|
+
if (fs.existsSync(resolved) && overwrite === false) {
|
|
1049
|
+
throw new Error(`File already exists: ${resolved}`);
|
|
1050
|
+
}
|
|
1051
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
1052
|
+
fs.writeFileSync(resolved, content);
|
|
1053
|
+
return { path: resolved, bytesWritten: Buffer.byteLength(content, 'utf-8') };
|
|
1054
|
+
})
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
const editTool = tool({
|
|
1058
|
+
name: 'Edit',
|
|
1059
|
+
description: 'Replace a substring in a file.',
|
|
1060
|
+
inputSchema: z.object({
|
|
1061
|
+
path: z.string().describe('File path (relative to /workspace/group or absolute within mounts)'),
|
|
1062
|
+
old_text: z.string().describe('Text to replace'),
|
|
1063
|
+
new_text: z.string().describe('Replacement text')
|
|
1064
|
+
}),
|
|
1065
|
+
outputSchema: z.object({
|
|
1066
|
+
path: z.string(),
|
|
1067
|
+
replaced: z.boolean(),
|
|
1068
|
+
occurrences: z.number()
|
|
1069
|
+
}),
|
|
1070
|
+
execute: wrapExecute('Edit', async ({ path: inputPath, old_text, new_text }: { path: string; old_text: string; new_text: string }) => {
|
|
1071
|
+
if (!old_text) {
|
|
1072
|
+
throw new Error('old_text must be non-empty');
|
|
1073
|
+
}
|
|
1074
|
+
const resolved = resolvePath(inputPath, isMain, true);
|
|
1075
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
1076
|
+
const occurrences = content.split(old_text).length - 1;
|
|
1077
|
+
if (occurrences === 0) {
|
|
1078
|
+
return { path: resolved, replaced: false, occurrences: 0 };
|
|
1079
|
+
}
|
|
1080
|
+
const updated = content.replace(old_text, new_text);
|
|
1081
|
+
fs.writeFileSync(resolved, updated);
|
|
1082
|
+
return { path: resolved, replaced: true, occurrences };
|
|
1083
|
+
})
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const globTool = tool({
|
|
1087
|
+
name: 'Glob',
|
|
1088
|
+
description: 'List files matching a glob pattern (relative to /workspace/group).',
|
|
1089
|
+
inputSchema: z.object({
|
|
1090
|
+
pattern: z.string().describe('Glob pattern'),
|
|
1091
|
+
maxResults: z.number().int().positive().optional().describe('Maximum results')
|
|
1092
|
+
}),
|
|
1093
|
+
outputSchema: z.object({
|
|
1094
|
+
matches: z.array(z.string())
|
|
1095
|
+
}),
|
|
1096
|
+
execute: wrapExecute('Glob', async ({ pattern, maxResults }: { pattern: string; maxResults?: number }) => {
|
|
1097
|
+
const roots = getAllowedRoots(isMain);
|
|
1098
|
+
const absolutePattern = path.isAbsolute(pattern)
|
|
1099
|
+
? resolvePath(pattern, isMain, false)
|
|
1100
|
+
: path.resolve(WORKSPACE_GROUP, pattern);
|
|
1101
|
+
const patternPosix = toPosixPath(absolutePattern);
|
|
1102
|
+
|
|
1103
|
+
if (!roots.some(root => isWithinRoot(absolutePattern, root))) {
|
|
1104
|
+
throw new Error(`Glob pattern is outside allowed roots: ${pattern}`);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (!/[*?]/.test(patternPosix)) {
|
|
1108
|
+
if (!fs.existsSync(absolutePattern)) {
|
|
1109
|
+
return { matches: [] };
|
|
1110
|
+
}
|
|
1111
|
+
return { matches: [absolutePattern] };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const searchRoot = getSearchRoot(patternPosix);
|
|
1115
|
+
if (!roots.some(root => isWithinRoot(searchRoot, root))) {
|
|
1116
|
+
throw new Error(`Glob search root is outside allowed roots: ${searchRoot}`);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const regex = globToRegex(patternPosix);
|
|
1120
|
+
const limit = Math.min(maxResults || 200, 2000);
|
|
1121
|
+
const candidates = walkFileTree(searchRoot, {
|
|
1122
|
+
includeFiles: true,
|
|
1123
|
+
includeDirs: true,
|
|
1124
|
+
maxResults: limit * 5
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const matches = candidates.filter(candidate => {
|
|
1128
|
+
const posixCandidate = toPosixPath(candidate);
|
|
1129
|
+
return regex.test(posixCandidate);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
return { matches: matches.slice(0, limit) };
|
|
1133
|
+
})
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
const grepTool = tool({
|
|
1137
|
+
name: 'Grep',
|
|
1138
|
+
description: 'Search for a pattern in files.',
|
|
1139
|
+
inputSchema: z.object({
|
|
1140
|
+
pattern: z.string().describe('Search pattern (plain text or regex)'),
|
|
1141
|
+
path: z.string().optional().describe('File or directory path (default /workspace/group)'),
|
|
1142
|
+
glob: z.string().optional().describe('Glob pattern to filter files (default **/*)'),
|
|
1143
|
+
regex: z.boolean().optional().describe('Treat pattern as regex'),
|
|
1144
|
+
maxResults: z.number().int().positive().optional().describe('Maximum matches')
|
|
1145
|
+
}),
|
|
1146
|
+
outputSchema: z.object({
|
|
1147
|
+
matches: z.array(z.object({
|
|
1148
|
+
path: z.string(),
|
|
1149
|
+
lineNumber: z.number(),
|
|
1150
|
+
line: z.string()
|
|
1151
|
+
}))
|
|
1152
|
+
}),
|
|
1153
|
+
execute: wrapExecute('Grep', async ({
|
|
1154
|
+
pattern,
|
|
1155
|
+
path: targetPath,
|
|
1156
|
+
glob,
|
|
1157
|
+
regex,
|
|
1158
|
+
maxResults
|
|
1159
|
+
}: { pattern: string; path?: string; glob?: string; regex?: boolean; maxResults?: number }) => {
|
|
1160
|
+
const basePath = resolvePath(targetPath || WORKSPACE_GROUP, isMain, true);
|
|
1161
|
+
const stats = fs.statSync(basePath);
|
|
1162
|
+
const limit = Math.min(maxResults || 200, 2000);
|
|
1163
|
+
const results: Array<{ path: string; lineNumber: number; line: string }> = [];
|
|
1164
|
+
|
|
1165
|
+
const matcher = regex ? new RegExp(pattern, 'i') : null;
|
|
1166
|
+
const globPattern = glob || '**/*';
|
|
1167
|
+
const globRegex = globToRegex(toPosixPath(globPattern));
|
|
1168
|
+
|
|
1169
|
+
const files = stats.isFile()
|
|
1170
|
+
? [basePath]
|
|
1171
|
+
: walkFileTree(basePath, {
|
|
1172
|
+
includeFiles: true,
|
|
1173
|
+
includeDirs: false,
|
|
1174
|
+
maxResults: limit * 50
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
for (const file of files) {
|
|
1178
|
+
if (results.length >= limit) break;
|
|
1179
|
+
const relative = toPosixPath(path.relative(basePath, file) || '');
|
|
1180
|
+
if (relative && !globRegex.test(relative)) continue;
|
|
1181
|
+
let content: string;
|
|
1182
|
+
try {
|
|
1183
|
+
const stat = fs.statSync(file);
|
|
1184
|
+
if (stat.size > runtime.grepMaxFileBytes) continue;
|
|
1185
|
+
content = fs.readFileSync(file, 'utf-8');
|
|
1186
|
+
} catch {
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
const lines = content.split('\n');
|
|
1190
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1191
|
+
const line = lines[i];
|
|
1192
|
+
const match = matcher ? matcher.test(line) : line.includes(pattern);
|
|
1193
|
+
if (match) {
|
|
1194
|
+
results.push({ path: file, lineNumber: i + 1, line });
|
|
1195
|
+
if (results.length >= limit) break;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return { matches: results };
|
|
1201
|
+
})
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
const webFetchTool = tool({
|
|
1205
|
+
name: 'WebFetch',
|
|
1206
|
+
description: 'Fetch a URL and return its contents.',
|
|
1207
|
+
inputSchema: z.object({
|
|
1208
|
+
url: z.string().describe('URL to fetch'),
|
|
1209
|
+
maxBytes: z.number().int().positive().optional().describe('Max bytes to read')
|
|
1210
|
+
}),
|
|
1211
|
+
outputSchema: z.object({
|
|
1212
|
+
url: z.string(),
|
|
1213
|
+
status: z.number(),
|
|
1214
|
+
contentType: z.string().nullable(),
|
|
1215
|
+
content: z.string(),
|
|
1216
|
+
truncated: z.boolean()
|
|
1217
|
+
}),
|
|
1218
|
+
execute: wrapExecute('WebFetch', async ({ url, maxBytes }: { url: string; maxBytes?: number }) => {
|
|
1219
|
+
await assertUrlAllowed({
|
|
1220
|
+
url,
|
|
1221
|
+
allowlist: webFetchAllowlist,
|
|
1222
|
+
blocklist: webFetchBlocklist,
|
|
1223
|
+
blockPrivate
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
const response = await fetchWithRedirects({
|
|
1227
|
+
url,
|
|
1228
|
+
options: {
|
|
1229
|
+
headers: {
|
|
1230
|
+
'User-Agent': 'DotClaw/1.0',
|
|
1231
|
+
'Accept': 'text/html,application/json,text/plain,*/*'
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
timeoutMs: runtime.webfetchTimeoutMs,
|
|
1235
|
+
label: 'WebFetch',
|
|
1236
|
+
allowlist: webFetchAllowlist,
|
|
1237
|
+
blocklist: webFetchBlocklist,
|
|
1238
|
+
blockPrivate
|
|
1239
|
+
});
|
|
1240
|
+
const { body, truncated } = await readResponseWithLimit(response, maxBytes || runtime.webfetchMaxBytes);
|
|
1241
|
+
const contentType = response.headers.get('content-type');
|
|
1242
|
+
let content = '';
|
|
1243
|
+
if (contentType && (contentType.includes('text') || contentType.includes('json'))) {
|
|
1244
|
+
content = body.toString('utf-8');
|
|
1245
|
+
} else {
|
|
1246
|
+
content = body.toString('utf-8');
|
|
1247
|
+
}
|
|
1248
|
+
const limited = limitText(content, runtime.outputLimitBytes);
|
|
1249
|
+
return {
|
|
1250
|
+
url: response.url || url,
|
|
1251
|
+
status: response.status,
|
|
1252
|
+
contentType,
|
|
1253
|
+
content: limited.text,
|
|
1254
|
+
truncated: truncated || limited.truncated
|
|
1255
|
+
};
|
|
1256
|
+
})
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
const webSearchInputSchema = z.object({
|
|
1260
|
+
query: z.string().describe('Search query'),
|
|
1261
|
+
count: z.number().int().positive().optional().describe('Number of results (default 5)'),
|
|
1262
|
+
offset: z.number().int().nonnegative().optional().describe('Offset for pagination'),
|
|
1263
|
+
safesearch: z.enum(['off', 'moderate', 'strict']).optional().describe('Safe search setting')
|
|
1264
|
+
});
|
|
1265
|
+
const webSearchOutputSchema = z.object({
|
|
1266
|
+
query: z.string(),
|
|
1267
|
+
results: z.array(z.object({
|
|
1268
|
+
title: z.string().nullable(),
|
|
1269
|
+
url: z.string().nullable(),
|
|
1270
|
+
description: z.string().nullable()
|
|
1271
|
+
}))
|
|
1272
|
+
});
|
|
1273
|
+
type WebSearchInput = z.infer<typeof webSearchInputSchema>;
|
|
1274
|
+
type WebSearchOutput = z.infer<typeof webSearchOutputSchema>;
|
|
1275
|
+
|
|
1276
|
+
const webSearchTool = tool({
|
|
1277
|
+
name: 'WebSearch',
|
|
1278
|
+
description: 'Search the web using Brave Search API.',
|
|
1279
|
+
inputSchema: webSearchInputSchema,
|
|
1280
|
+
outputSchema: webSearchOutputSchema,
|
|
1281
|
+
execute: wrapExecute('WebSearch', async ({
|
|
1282
|
+
query,
|
|
1283
|
+
count,
|
|
1284
|
+
offset,
|
|
1285
|
+
safesearch
|
|
1286
|
+
}: WebSearchInput): Promise<WebSearchOutput> => {
|
|
1287
|
+
const apiKey = process.env.BRAVE_SEARCH_API_KEY;
|
|
1288
|
+
if (!apiKey) {
|
|
1289
|
+
throw new Error('BRAVE_SEARCH_API_KEY is not set');
|
|
1290
|
+
}
|
|
1291
|
+
const params = new URLSearchParams({
|
|
1292
|
+
q: query,
|
|
1293
|
+
count: String(Math.min(count || 5, 20)),
|
|
1294
|
+
offset: String(offset || 0),
|
|
1295
|
+
safesearch: safesearch || 'moderate'
|
|
1296
|
+
});
|
|
1297
|
+
const response = await fetchWithTimeout(`https://api.search.brave.com/res/v1/web/search?${params.toString()}`, {
|
|
1298
|
+
headers: {
|
|
1299
|
+
'Accept': 'application/json',
|
|
1300
|
+
'X-Subscription-Token': apiKey
|
|
1301
|
+
}
|
|
1302
|
+
}, runtime.websearchTimeoutMs, 'WebSearch');
|
|
1303
|
+
if (!response.ok) {
|
|
1304
|
+
const text = await response.text();
|
|
1305
|
+
throw new Error(`Brave search error (${response.status}): ${text}`);
|
|
1306
|
+
}
|
|
1307
|
+
const data = await response.json() as { web?: { results?: Array<Record<string, unknown>> } };
|
|
1308
|
+
const toMaybeString = (value: unknown): string | null =>
|
|
1309
|
+
typeof value === 'string' ? value : null;
|
|
1310
|
+
const results: WebSearchOutput['results'] = (data?.web?.results || []).map((result) => ({
|
|
1311
|
+
title: toMaybeString(result?.title),
|
|
1312
|
+
url: toMaybeString(result?.url),
|
|
1313
|
+
description: toMaybeString(result?.description) ?? toMaybeString(result?.snippet)
|
|
1314
|
+
}));
|
|
1315
|
+
return { query, results };
|
|
1316
|
+
})
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
const pluginTools = loadPluginConfigs(runtime.pluginDirs).map((config) => {
|
|
1320
|
+
const inputSchema = buildInputSchema(config);
|
|
1321
|
+
const toolName = `plugin__${config.name}`;
|
|
1322
|
+
|
|
1323
|
+
if (config.type === 'http') {
|
|
1324
|
+
return tool({
|
|
1325
|
+
name: toolName,
|
|
1326
|
+
description: config.description,
|
|
1327
|
+
inputSchema,
|
|
1328
|
+
outputSchema: z.object({
|
|
1329
|
+
status: z.number(),
|
|
1330
|
+
contentType: z.string().nullable(),
|
|
1331
|
+
body: z.string(),
|
|
1332
|
+
truncated: z.boolean()
|
|
1333
|
+
}),
|
|
1334
|
+
execute: wrapExecute(toolName, async (args: Record<string, unknown>) => {
|
|
1335
|
+
if (!config.url) {
|
|
1336
|
+
throw new Error(`Plugin ${config.name} missing url`);
|
|
1337
|
+
}
|
|
1338
|
+
const method = (config.method || 'GET').toUpperCase();
|
|
1339
|
+
let url = interpolateTemplate(config.url, args);
|
|
1340
|
+
|
|
1341
|
+
const queryParams = config.query_params || {};
|
|
1342
|
+
const queryEntries: Record<string, string> = {};
|
|
1343
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
1344
|
+
queryEntries[key] = interpolateTemplate(String(value), args);
|
|
1345
|
+
}
|
|
1346
|
+
const queryString = new URLSearchParams(queryEntries).toString();
|
|
1347
|
+
if (queryString) {
|
|
1348
|
+
url += (url.includes('?') ? '&' : '?') + queryString;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
await assertUrlAllowed({
|
|
1352
|
+
url,
|
|
1353
|
+
allowlist: webFetchAllowlist,
|
|
1354
|
+
blocklist: webFetchBlocklist,
|
|
1355
|
+
blockPrivate
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
const headers: Record<string, string> = {};
|
|
1359
|
+
if (config.headers) {
|
|
1360
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
1361
|
+
headers[key] = interpolateTemplate(String(value), args);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
let body: string | undefined;
|
|
1366
|
+
if (config.body && method !== 'GET') {
|
|
1367
|
+
const payload: Record<string, unknown> = {};
|
|
1368
|
+
for (const [key, value] of Object.entries(config.body)) {
|
|
1369
|
+
if (typeof value === 'string') {
|
|
1370
|
+
payload[key] = interpolateTemplate(value, args);
|
|
1371
|
+
} else {
|
|
1372
|
+
payload[key] = value;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
body = JSON.stringify(payload);
|
|
1376
|
+
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const response = await fetchWithRedirects({
|
|
1380
|
+
url,
|
|
1381
|
+
options: { method, headers, body },
|
|
1382
|
+
timeoutMs: runtime.pluginHttpTimeoutMs,
|
|
1383
|
+
label: 'Plugin HTTP',
|
|
1384
|
+
allowlist: webFetchAllowlist,
|
|
1385
|
+
blocklist: webFetchBlocklist,
|
|
1386
|
+
blockPrivate
|
|
1387
|
+
});
|
|
1388
|
+
const { body: responseBody, truncated } = await readResponseWithLimit(response, runtime.pluginMaxBytes);
|
|
1389
|
+
const contentType = response.headers.get('content-type');
|
|
1390
|
+
const text = responseBody.toString('utf-8');
|
|
1391
|
+
const limited = limitText(text, runtime.outputLimitBytes);
|
|
1392
|
+
return {
|
|
1393
|
+
status: response.status,
|
|
1394
|
+
contentType,
|
|
1395
|
+
body: limited.text,
|
|
1396
|
+
truncated: truncated || limited.truncated
|
|
1397
|
+
};
|
|
1398
|
+
})
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (config.type === 'bash') {
|
|
1403
|
+
return tool({
|
|
1404
|
+
name: toolName,
|
|
1405
|
+
description: config.description,
|
|
1406
|
+
inputSchema,
|
|
1407
|
+
outputSchema: z.object({
|
|
1408
|
+
stdout: z.string(),
|
|
1409
|
+
stderr: z.string(),
|
|
1410
|
+
exitCode: z.number().int().nullable(),
|
|
1411
|
+
durationMs: z.number(),
|
|
1412
|
+
truncated: z.boolean()
|
|
1413
|
+
}),
|
|
1414
|
+
execute: wrapExecute(toolName, async (args: Record<string, unknown>) => {
|
|
1415
|
+
if (!config.command) {
|
|
1416
|
+
throw new Error(`Plugin ${config.name} missing command`);
|
|
1417
|
+
}
|
|
1418
|
+
const command = interpolateTemplate(config.command, args);
|
|
1419
|
+
return runCommand(command, runtime.bashTimeoutMs, runtime.bashOutputLimitBytes);
|
|
1420
|
+
})
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
return null;
|
|
1425
|
+
}).filter(Boolean) as Tool[];
|
|
1426
|
+
|
|
1427
|
+
const sendMessageTool = tool({
|
|
1428
|
+
name: 'mcp__dotclaw__send_message',
|
|
1429
|
+
description: 'Send a message to the current Telegram chat.',
|
|
1430
|
+
inputSchema: z.object({
|
|
1431
|
+
text: z.string().describe('The message text to send')
|
|
1432
|
+
}),
|
|
1433
|
+
outputSchema: z.object({
|
|
1434
|
+
ok: z.boolean(),
|
|
1435
|
+
id: z.string().optional()
|
|
1436
|
+
}),
|
|
1437
|
+
execute: wrapExecute('mcp__dotclaw__send_message', async ({ text }: { text: string }) => ipc.sendMessage(text))
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
const scheduleTaskTool = tool({
|
|
1441
|
+
name: 'mcp__dotclaw__schedule_task',
|
|
1442
|
+
description: 'Schedule a recurring or one-time task.',
|
|
1443
|
+
inputSchema: z.object({
|
|
1444
|
+
prompt: z.string().describe('Task prompt'),
|
|
1445
|
+
schedule_type: z.enum(['cron', 'interval', 'once']),
|
|
1446
|
+
schedule_value: z.string(),
|
|
1447
|
+
context_mode: z.enum(['group', 'isolated']).optional(),
|
|
1448
|
+
target_group: z.string().optional()
|
|
1449
|
+
}),
|
|
1450
|
+
outputSchema: z.object({
|
|
1451
|
+
ok: z.boolean(),
|
|
1452
|
+
id: z.string().optional(),
|
|
1453
|
+
error: z.string().optional()
|
|
1454
|
+
}),
|
|
1455
|
+
execute: wrapExecute('mcp__dotclaw__schedule_task', async (args: { prompt: string; schedule_type: 'cron' | 'interval' | 'once'; schedule_value: string; context_mode?: 'group' | 'isolated'; target_group?: string }) =>
|
|
1456
|
+
ipc.scheduleTask(args))
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
const listTasksTool = tool({
|
|
1460
|
+
name: 'mcp__dotclaw__list_tasks',
|
|
1461
|
+
description: 'List all scheduled tasks.',
|
|
1462
|
+
inputSchema: z.object({}),
|
|
1463
|
+
outputSchema: z.object({
|
|
1464
|
+
ok: z.boolean(),
|
|
1465
|
+
tasks: z.array(z.any())
|
|
1466
|
+
}),
|
|
1467
|
+
execute: wrapExecute('mcp__dotclaw__list_tasks', async () => ipc.listTasks())
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
const pauseTaskTool = tool({
|
|
1471
|
+
name: 'mcp__dotclaw__pause_task',
|
|
1472
|
+
description: 'Pause a scheduled task.',
|
|
1473
|
+
inputSchema: z.object({
|
|
1474
|
+
task_id: z.string()
|
|
1475
|
+
}),
|
|
1476
|
+
outputSchema: z.object({
|
|
1477
|
+
ok: z.boolean()
|
|
1478
|
+
}),
|
|
1479
|
+
execute: wrapExecute('mcp__dotclaw__pause_task', async ({ task_id }: { task_id: string }) => ipc.pauseTask(task_id))
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
const resumeTaskTool = tool({
|
|
1483
|
+
name: 'mcp__dotclaw__resume_task',
|
|
1484
|
+
description: 'Resume a paused task.',
|
|
1485
|
+
inputSchema: z.object({
|
|
1486
|
+
task_id: z.string()
|
|
1487
|
+
}),
|
|
1488
|
+
outputSchema: z.object({
|
|
1489
|
+
ok: z.boolean()
|
|
1490
|
+
}),
|
|
1491
|
+
execute: wrapExecute('mcp__dotclaw__resume_task', async ({ task_id }: { task_id: string }) => ipc.resumeTask(task_id))
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
const cancelTaskTool = tool({
|
|
1495
|
+
name: 'mcp__dotclaw__cancel_task',
|
|
1496
|
+
description: 'Cancel a scheduled task.',
|
|
1497
|
+
inputSchema: z.object({
|
|
1498
|
+
task_id: z.string()
|
|
1499
|
+
}),
|
|
1500
|
+
outputSchema: z.object({
|
|
1501
|
+
ok: z.boolean()
|
|
1502
|
+
}),
|
|
1503
|
+
execute: wrapExecute('mcp__dotclaw__cancel_task', async ({ task_id }: { task_id: string }) => ipc.cancelTask(task_id))
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
const updateTaskTool = tool({
|
|
1507
|
+
name: 'mcp__dotclaw__update_task',
|
|
1508
|
+
description: 'Update a scheduled task (state, prompt, schedule, or status).',
|
|
1509
|
+
inputSchema: z.object({
|
|
1510
|
+
task_id: z.string(),
|
|
1511
|
+
state_json: z.string().optional(),
|
|
1512
|
+
prompt: z.string().optional(),
|
|
1513
|
+
schedule_type: z.enum(['cron', 'interval', 'once']).optional(),
|
|
1514
|
+
schedule_value: z.string().optional(),
|
|
1515
|
+
context_mode: z.enum(['group', 'isolated']).optional(),
|
|
1516
|
+
status: z.enum(['active', 'paused', 'completed']).optional()
|
|
1517
|
+
}),
|
|
1518
|
+
outputSchema: z.object({
|
|
1519
|
+
ok: z.boolean(),
|
|
1520
|
+
error: z.string().optional()
|
|
1521
|
+
}),
|
|
1522
|
+
execute: wrapExecute('mcp__dotclaw__update_task', async (args: { task_id: string; state_json?: string; prompt?: string; schedule_type?: string; schedule_value?: string; context_mode?: string; status?: string }) =>
|
|
1523
|
+
ipc.updateTask(args))
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
const registerGroupTool = tool({
|
|
1527
|
+
name: 'mcp__dotclaw__register_group',
|
|
1528
|
+
description: 'Register a new Telegram chat (main group only).',
|
|
1529
|
+
inputSchema: z.object({
|
|
1530
|
+
jid: z.string(),
|
|
1531
|
+
name: z.string(),
|
|
1532
|
+
folder: z.string(),
|
|
1533
|
+
trigger: z.string().optional()
|
|
1534
|
+
}),
|
|
1535
|
+
outputSchema: z.object({
|
|
1536
|
+
ok: z.boolean(),
|
|
1537
|
+
error: z.string().optional()
|
|
1538
|
+
}),
|
|
1539
|
+
execute: wrapExecute('mcp__dotclaw__register_group', async ({ jid, name, folder, trigger }: { jid: string; name: string; folder: string; trigger?: string }) =>
|
|
1540
|
+
ipc.registerGroup({ jid, name, folder, trigger }))
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
const removeGroupTool = tool({
|
|
1544
|
+
name: 'mcp__dotclaw__remove_group',
|
|
1545
|
+
description: 'Remove a registered Telegram chat by chat id, name, or folder (main group only).',
|
|
1546
|
+
inputSchema: z.object({
|
|
1547
|
+
identifier: z.string().describe('Chat id, group name, or folder')
|
|
1548
|
+
}),
|
|
1549
|
+
outputSchema: z.object({
|
|
1550
|
+
ok: z.boolean(),
|
|
1551
|
+
error: z.string().optional()
|
|
1552
|
+
}),
|
|
1553
|
+
execute: wrapExecute('mcp__dotclaw__remove_group', async ({ identifier }: { identifier: string }) =>
|
|
1554
|
+
ipc.removeGroup({ identifier }))
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
const listGroupsTool = tool({
|
|
1558
|
+
name: 'mcp__dotclaw__list_groups',
|
|
1559
|
+
description: 'List registered groups (main group only).',
|
|
1560
|
+
inputSchema: z.object({}),
|
|
1561
|
+
outputSchema: z.object({
|
|
1562
|
+
ok: z.boolean(),
|
|
1563
|
+
result: z.any().optional(),
|
|
1564
|
+
error: z.string().optional()
|
|
1565
|
+
}),
|
|
1566
|
+
execute: wrapExecute('mcp__dotclaw__list_groups', async () => ipc.listGroups())
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
const setModelTool = tool({
|
|
1570
|
+
name: 'mcp__dotclaw__set_model',
|
|
1571
|
+
description: 'Set the active OpenRouter model (main group only).',
|
|
1572
|
+
inputSchema: z.object({
|
|
1573
|
+
model: z.string().describe('OpenRouter model ID (e.g., moonshotai/kimi-k2.5)'),
|
|
1574
|
+
scope: z.enum(['global', 'group', 'user']).optional(),
|
|
1575
|
+
target_id: z.string().optional().describe('Optional group folder or user id for scoped overrides')
|
|
1576
|
+
}),
|
|
1577
|
+
outputSchema: z.object({
|
|
1578
|
+
ok: z.boolean(),
|
|
1579
|
+
error: z.string().optional()
|
|
1580
|
+
}),
|
|
1581
|
+
execute: wrapExecute('mcp__dotclaw__set_model', async ({ model, scope, target_id }: { model: string; scope?: 'global' | 'group' | 'user'; target_id?: string }) =>
|
|
1582
|
+
ipc.setModel({ model, scope, target_id }))
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
const memoryUpsertTool = tool({
|
|
1586
|
+
name: 'mcp__dotclaw__memory_upsert',
|
|
1587
|
+
description: 'Upsert long-term memory items (use for durable user or group facts/preferences).',
|
|
1588
|
+
inputSchema: z.object({
|
|
1589
|
+
items: z.array(z.object({
|
|
1590
|
+
scope: z.enum(['user', 'group', 'global']),
|
|
1591
|
+
subject_id: z.string().optional(),
|
|
1592
|
+
type: z.enum(['identity', 'preference', 'fact', 'relationship', 'project', 'task', 'note', 'archive']),
|
|
1593
|
+
kind: z.enum(['semantic', 'episodic', 'procedural', 'preference']).optional(),
|
|
1594
|
+
conflict_key: z.string().optional(),
|
|
1595
|
+
content: z.string(),
|
|
1596
|
+
importance: z.number().min(0).max(1).optional(),
|
|
1597
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
1598
|
+
tags: z.array(z.string()).optional(),
|
|
1599
|
+
ttl_days: z.number().optional()
|
|
1600
|
+
})),
|
|
1601
|
+
source: z.string().optional(),
|
|
1602
|
+
target_group: z.string().optional()
|
|
1603
|
+
}),
|
|
1604
|
+
outputSchema: z.object({
|
|
1605
|
+
ok: z.boolean(),
|
|
1606
|
+
result: z.any().optional(),
|
|
1607
|
+
error: z.string().optional()
|
|
1608
|
+
}),
|
|
1609
|
+
execute: wrapExecute('mcp__dotclaw__memory_upsert', async ({ items, source, target_group }: { items: unknown[]; source?: string; target_group?: string }) =>
|
|
1610
|
+
ipc.memoryUpsert({ items, source, target_group }))
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
const memoryForgetTool = tool({
|
|
1614
|
+
name: 'mcp__dotclaw__memory_forget',
|
|
1615
|
+
description: 'Forget long-term memory items by id or content.',
|
|
1616
|
+
inputSchema: z.object({
|
|
1617
|
+
ids: z.array(z.string()).optional(),
|
|
1618
|
+
content: z.string().optional(),
|
|
1619
|
+
scope: z.enum(['user', 'group', 'global']).optional(),
|
|
1620
|
+
userId: z.string().optional(),
|
|
1621
|
+
target_group: z.string().optional()
|
|
1622
|
+
}),
|
|
1623
|
+
outputSchema: z.object({
|
|
1624
|
+
ok: z.boolean(),
|
|
1625
|
+
result: z.any().optional(),
|
|
1626
|
+
error: z.string().optional()
|
|
1627
|
+
}),
|
|
1628
|
+
execute: wrapExecute('mcp__dotclaw__memory_forget', async (args: { ids?: string[]; content?: string; scope?: string; userId?: string; target_group?: string }) =>
|
|
1629
|
+
ipc.memoryForget(args))
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
const memoryListTool = tool({
|
|
1633
|
+
name: 'mcp__dotclaw__memory_list',
|
|
1634
|
+
description: 'List long-term memory items for the current group/user.',
|
|
1635
|
+
inputSchema: z.object({
|
|
1636
|
+
scope: z.enum(['user', 'group', 'global']).optional(),
|
|
1637
|
+
type: z.enum(['identity', 'preference', 'fact', 'relationship', 'project', 'task', 'note', 'archive']).optional(),
|
|
1638
|
+
userId: z.string().optional(),
|
|
1639
|
+
limit: z.number().int().positive().optional(),
|
|
1640
|
+
target_group: z.string().optional()
|
|
1641
|
+
}),
|
|
1642
|
+
outputSchema: z.object({
|
|
1643
|
+
ok: z.boolean(),
|
|
1644
|
+
result: z.any().optional(),
|
|
1645
|
+
error: z.string().optional()
|
|
1646
|
+
}),
|
|
1647
|
+
execute: wrapExecute('mcp__dotclaw__memory_list', async (args: { scope?: string; type?: string; userId?: string; limit?: number; target_group?: string }) =>
|
|
1648
|
+
ipc.memoryList(args))
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
const memorySearchTool = tool({
|
|
1652
|
+
name: 'mcp__dotclaw__memory_search',
|
|
1653
|
+
description: 'Search long-term memory items.',
|
|
1654
|
+
inputSchema: z.object({
|
|
1655
|
+
query: z.string(),
|
|
1656
|
+
userId: z.string().optional(),
|
|
1657
|
+
limit: z.number().int().positive().optional(),
|
|
1658
|
+
target_group: z.string().optional()
|
|
1659
|
+
}),
|
|
1660
|
+
outputSchema: z.object({
|
|
1661
|
+
ok: z.boolean(),
|
|
1662
|
+
result: z.any().optional(),
|
|
1663
|
+
error: z.string().optional()
|
|
1664
|
+
}),
|
|
1665
|
+
execute: wrapExecute('mcp__dotclaw__memory_search', async (args: { query: string; userId?: string; limit?: number; target_group?: string }) =>
|
|
1666
|
+
ipc.memorySearch(args))
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
const memoryStatsTool = tool({
|
|
1670
|
+
name: 'mcp__dotclaw__memory_stats',
|
|
1671
|
+
description: 'Get memory stats for the group/user.',
|
|
1672
|
+
inputSchema: z.object({
|
|
1673
|
+
userId: z.string().optional(),
|
|
1674
|
+
target_group: z.string().optional()
|
|
1675
|
+
}),
|
|
1676
|
+
outputSchema: z.object({
|
|
1677
|
+
ok: z.boolean(),
|
|
1678
|
+
result: z.any().optional(),
|
|
1679
|
+
error: z.string().optional()
|
|
1680
|
+
}),
|
|
1681
|
+
execute: wrapExecute('mcp__dotclaw__memory_stats', async (args: { userId?: string; target_group?: string }) =>
|
|
1682
|
+
ipc.memoryStats(args))
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
const tools: Tool[] = [
|
|
1686
|
+
readTool,
|
|
1687
|
+
writeTool,
|
|
1688
|
+
editTool,
|
|
1689
|
+
globTool,
|
|
1690
|
+
grepTool,
|
|
1691
|
+
gitCloneTool,
|
|
1692
|
+
npmInstallTool,
|
|
1693
|
+
sendMessageTool,
|
|
1694
|
+
scheduleTaskTool,
|
|
1695
|
+
listTasksTool,
|
|
1696
|
+
pauseTaskTool,
|
|
1697
|
+
resumeTaskTool,
|
|
1698
|
+
cancelTaskTool,
|
|
1699
|
+
updateTaskTool,
|
|
1700
|
+
memoryUpsertTool,
|
|
1701
|
+
memoryForgetTool,
|
|
1702
|
+
memoryListTool,
|
|
1703
|
+
memorySearchTool,
|
|
1704
|
+
memoryStatsTool,
|
|
1705
|
+
registerGroupTool,
|
|
1706
|
+
removeGroupTool,
|
|
1707
|
+
listGroupsTool,
|
|
1708
|
+
setModelTool,
|
|
1709
|
+
...pluginTools
|
|
1710
|
+
];
|
|
1711
|
+
|
|
1712
|
+
if (enableBash) {
|
|
1713
|
+
tools.push(bashTool as Tool);
|
|
1714
|
+
tools.push(pythonTool as Tool);
|
|
1715
|
+
}
|
|
1716
|
+
if (enableWebSearch) tools.push(webSearchTool as Tool);
|
|
1717
|
+
if (enableWebFetch) tools.push(webFetchTool as Tool);
|
|
1718
|
+
|
|
1719
|
+
return tools;
|
|
1720
|
+
}
|