zames_pro 2.13.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/agent-loop.js +43 -43
- package/dist/commands.js +201 -0
- package/dist/extraTools.js +352 -0
- package/dist/gitTools.js +13 -13
- package/dist/i18n.js +12 -0
- package/dist/index.js +161 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools.js +100 -46
- package/dist/web.js +15 -15
- package/package.json +1 -1
package/dist/system-prompt.js
CHANGED
|
@@ -50,7 +50,7 @@ export function renderContextSection(context) {
|
|
|
50
50
|
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, }) {
|
|
51
51
|
const t = translate(locale);
|
|
52
52
|
const toolDescriptions = tools
|
|
53
|
-
.map((t2) => `### ${t2.name}\n${t2.description}\
|
|
53
|
+
.map((t2) => `### ${t2.name}\n${t2.description}\nParameters: ${JSON.stringify(t2.parameters)}`)
|
|
54
54
|
.join('\n\n');
|
|
55
55
|
const gitSection = gitContext
|
|
56
56
|
? `\n## Git context\n\n${gitContext}\n`
|
package/dist/tools.js
CHANGED
|
@@ -3,6 +3,11 @@ import path from 'path';
|
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import { createGitTools } from './gitTools.js';
|
|
5
5
|
import { createWebTools } from './web.js';
|
|
6
|
+
import { createExtraTools } from './extraTools.js';
|
|
7
|
+
// NOTE: tool descriptions and tool-result strings are AGENT-FACING, not
|
|
8
|
+
// user-facing. They must be in ENGLISH (like Claude Code / Codex). Only the
|
|
9
|
+
// strings the OPERATOR sees (help, spinner, service messages) are localized —
|
|
10
|
+
// those live in src/i18n.ts (CATALOG) and are picked by the locale setting.
|
|
6
11
|
export function createTools(workdir, { undo } = {}) {
|
|
7
12
|
const root = path.resolve(workdir);
|
|
8
13
|
const safe = (p) => {
|
|
@@ -11,7 +16,7 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
11
16
|
// (C:\work\proj vs C:\work\proj-old). We compute via relative().
|
|
12
17
|
const rel = path.relative(root, resolved);
|
|
13
18
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
14
|
-
throw new Error(
|
|
19
|
+
throw new Error(`Access outside the working directory is forbidden: ${p}`);
|
|
15
20
|
}
|
|
16
21
|
return resolved;
|
|
17
22
|
};
|
|
@@ -28,7 +33,7 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
28
33
|
const target = path.resolve(root, raw);
|
|
29
34
|
const rel = path.relative(root, target);
|
|
30
35
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
31
|
-
throw new Error('Sandbox:
|
|
36
|
+
throw new Error('Sandbox: leaving ' + root + ' is forbidden (cd ' + raw + ')');
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
};
|
|
@@ -51,21 +56,21 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
51
56
|
const errStr = (stderr || '').toString();
|
|
52
57
|
if (!err) {
|
|
53
58
|
const combined = (out + errStr).trim();
|
|
54
|
-
resolve(combined || '(
|
|
59
|
+
resolve(combined || '(command produced no output)');
|
|
55
60
|
return;
|
|
56
61
|
}
|
|
57
62
|
const parts = [];
|
|
58
63
|
if (err.killed) {
|
|
59
|
-
parts.push(`⏱
|
|
64
|
+
parts.push(`⏱ Timeout after ${timeout}ms — process killed.`);
|
|
60
65
|
}
|
|
61
66
|
else if (err.code !== undefined && err.code !== null) {
|
|
62
67
|
parts.push(`Exit code: ${err.code}`);
|
|
63
68
|
}
|
|
64
69
|
else if (err.signal) {
|
|
65
|
-
parts.push(
|
|
70
|
+
parts.push(`Killed by signal: ${err.signal}`);
|
|
66
71
|
}
|
|
67
72
|
else {
|
|
68
|
-
parts.push(
|
|
73
|
+
parts.push(`Error: ${err.message}`);
|
|
69
74
|
}
|
|
70
75
|
if (out.trim())
|
|
71
76
|
parts.push(`stdout:\n${out.trim()}`);
|
|
@@ -84,45 +89,76 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
84
89
|
}
|
|
85
90
|
return String(content ?? '');
|
|
86
91
|
};
|
|
92
|
+
// Guard against missing/placeholder required args. Without this, a tool call
|
|
93
|
+
// that omits `path` (or sends it as null) becomes String(undefined) ===
|
|
94
|
+
// "undefined" and the tool silently creates a file literally named
|
|
95
|
+
// "undefined" in the working directory. `req()` rejects that up front.
|
|
96
|
+
const req = (v, name) => {
|
|
97
|
+
if (v === undefined || v === null) {
|
|
98
|
+
throw new Error(`Missing required argument: ${name}`);
|
|
99
|
+
}
|
|
100
|
+
const s = String(v);
|
|
101
|
+
if (s === '' || s === 'undefined' || s === 'null') {
|
|
102
|
+
throw new Error(`Invalid required argument ${name}: ${JSON.stringify(v)}`);
|
|
103
|
+
}
|
|
104
|
+
return s;
|
|
105
|
+
};
|
|
87
106
|
const baseTools = [
|
|
88
107
|
{
|
|
89
108
|
name: 'Read',
|
|
90
|
-
description: '
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
description: 'Read a file. Optional: offset and limit (lines). ' +
|
|
110
|
+
'numbered=true adds cat -n style line numbers (1-based) for reference ' +
|
|
111
|
+
'only — do NOT copy the numbers into Edit old_string. ' +
|
|
112
|
+
'Lines longer than 2000 chars are truncated.',
|
|
113
|
+
parameters: {
|
|
114
|
+
path: 'string',
|
|
115
|
+
offset: 'number?',
|
|
116
|
+
limit: 'number?',
|
|
117
|
+
numbered: 'boolean?',
|
|
118
|
+
},
|
|
119
|
+
fn: async ({ path: p, offset, limit, numbered }) => {
|
|
120
|
+
const file = safe(req(p, 'path'));
|
|
94
121
|
const content = await fs.readFile(file, 'utf-8');
|
|
122
|
+
if (content === '')
|
|
123
|
+
return '(file is empty)';
|
|
95
124
|
const lines = content.split('\n');
|
|
96
125
|
const start = offset ?? 0;
|
|
97
126
|
const end = limit ? start + limit : lines.length;
|
|
98
|
-
|
|
127
|
+
const slice = lines.slice(start, end);
|
|
128
|
+
const clip = (l) => l.length > 2000 ? l.slice(0, 2000) + ' …[line truncated]' : l;
|
|
129
|
+
if (!numbered)
|
|
130
|
+
return slice.map(clip).join('\n');
|
|
131
|
+
const width = String(start + slice.length).length;
|
|
132
|
+
return slice
|
|
133
|
+
.map((l, i) => String(start + i + 1).padStart(width) + '\t' + clip(l))
|
|
134
|
+
.join('\n');
|
|
99
135
|
},
|
|
100
136
|
},
|
|
101
137
|
{
|
|
102
138
|
name: 'Write',
|
|
103
|
-
description: '
|
|
104
|
-
'
|
|
105
|
-
'
|
|
139
|
+
description: 'Create or overwrite a file. content — text; content_base64 — the same ' +
|
|
140
|
+
'content base64-encoded (use it if the text contains $, backslashes, ' +
|
|
141
|
+
'newlines or other characters that may get corrupted in transit).',
|
|
106
142
|
parameters: {
|
|
107
143
|
path: 'string',
|
|
108
144
|
content: 'string?',
|
|
109
145
|
content_base64: 'string?',
|
|
110
146
|
},
|
|
111
147
|
fn: async ({ path: p, content, content_base64 }) => {
|
|
112
|
-
const file = safe(
|
|
148
|
+
const file = safe(req(p, 'path'));
|
|
113
149
|
const text = decodeContent(content, content_base64);
|
|
114
150
|
if (undo)
|
|
115
151
|
await undo.backup(file);
|
|
116
152
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
117
153
|
await fs.writeFile(file, text, 'utf-8');
|
|
118
|
-
return
|
|
154
|
+
return `File written: ${p}`;
|
|
119
155
|
},
|
|
120
156
|
},
|
|
121
157
|
{
|
|
122
158
|
name: 'Edit',
|
|
123
|
-
description: '
|
|
124
|
-
'old_base64/new_base64 —
|
|
125
|
-
'
|
|
159
|
+
description: 'Replace a string in a file. old_string must occur exactly once. ' +
|
|
160
|
+
'old_base64/new_base64 — the same strings base64-encoded (if the text ' +
|
|
161
|
+
'contains special characters that may get corrupted).',
|
|
126
162
|
parameters: {
|
|
127
163
|
path: 'string',
|
|
128
164
|
old_string: 'string?',
|
|
@@ -131,81 +167,99 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
131
167
|
new_base64: 'string?',
|
|
132
168
|
},
|
|
133
169
|
fn: async ({ path: p, old_string, new_string, old_base64, new_base64 }) => {
|
|
134
|
-
const file = safe(
|
|
170
|
+
const file = safe(req(p, 'path'));
|
|
135
171
|
const oldStr = decodeContent(old_string, old_base64);
|
|
136
172
|
const newStr = decodeContent(new_string, new_base64);
|
|
137
173
|
if (oldStr === '') {
|
|
138
|
-
throw new Error('old_string
|
|
174
|
+
throw new Error('old_string is empty — nothing to replace.');
|
|
139
175
|
}
|
|
140
176
|
let content = await fs.readFile(file, 'utf-8');
|
|
141
177
|
const occurrences = content.split(oldStr).length - 1;
|
|
142
178
|
if (occurrences === 0) {
|
|
143
|
-
throw new Error(
|
|
179
|
+
throw new Error(`String not found in ${p}: "${oldStr.slice(0, 60)}..."`);
|
|
144
180
|
}
|
|
145
181
|
if (occurrences > 1) {
|
|
146
|
-
throw new Error(
|
|
182
|
+
throw new Error(`String occurs ${occurrences} times in ${p}. Make old_string more specific.`);
|
|
147
183
|
}
|
|
148
184
|
if (undo)
|
|
149
185
|
await undo.backup(file);
|
|
150
186
|
content = content.replace(oldStr, newStr);
|
|
151
187
|
await fs.writeFile(file, content, 'utf-8');
|
|
152
|
-
return
|
|
188
|
+
return `Edited: ${p}`;
|
|
153
189
|
},
|
|
154
190
|
},
|
|
155
191
|
{
|
|
156
192
|
name: 'Bash',
|
|
157
|
-
description: '
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
193
|
+
description: 'Run a shell command in the working directory (cmd.exe on Windows, sh on Linux/macOS). ' +
|
|
194
|
+
'Do not use for long-running processes (servers) — they will hit the timeout. ' +
|
|
195
|
+
'Do not use for commands that require interactive input. ' +
|
|
196
|
+
'For git use the Git* tools. For the web use WebFetch / WebSearch.',
|
|
161
197
|
parameters: { command: 'string', timeout: 'number?' },
|
|
162
198
|
fn: async ({ command, timeout }) => {
|
|
163
|
-
assertCommandInsideRoot(
|
|
164
|
-
return runShell(
|
|
199
|
+
assertCommandInsideRoot(req(command, 'command'));
|
|
200
|
+
return runShell(req(command, 'command'), timeout);
|
|
165
201
|
},
|
|
166
202
|
},
|
|
167
203
|
{
|
|
168
204
|
name: 'Glob',
|
|
169
|
-
description: '
|
|
170
|
-
|
|
171
|
-
|
|
205
|
+
description: 'Find files by glob pattern (e.g. "**/*.js"). ' +
|
|
206
|
+
'path (optional) — search directory inside the project.',
|
|
207
|
+
parameters: { pattern: 'string', path: 'string?' },
|
|
208
|
+
fn: async ({ pattern, path: searchPath }) => {
|
|
172
209
|
const { glob } = await import('fs/promises');
|
|
210
|
+
const cwd = searchPath ? safe(String(searchPath)) : workdir;
|
|
173
211
|
const results = [];
|
|
174
|
-
for await (const f of glob(
|
|
212
|
+
for await (const f of glob(req(pattern, 'pattern'), { cwd })) {
|
|
175
213
|
// Sandbox: ignore anything that goes outside root.
|
|
176
|
-
const abs = path.resolve(
|
|
214
|
+
const abs = path.resolve(cwd, f);
|
|
177
215
|
const rel = path.relative(root, abs);
|
|
178
216
|
if (rel.startsWith('..') || path.isAbsolute(rel))
|
|
179
217
|
continue;
|
|
180
218
|
results.push(f);
|
|
181
219
|
}
|
|
182
|
-
return results.length ? results.join('\n') : '
|
|
220
|
+
return results.length ? results.join('\n') : 'No matches found.';
|
|
183
221
|
},
|
|
184
222
|
},
|
|
185
223
|
{
|
|
186
224
|
name: 'Grep',
|
|
187
|
-
description: '
|
|
188
|
-
|
|
189
|
-
|
|
225
|
+
description: 'Search file contents (regular expression). include — file name ' +
|
|
226
|
+
'filter (e.g. "*.ts"). output: content (default, matching lines with ' +
|
|
227
|
+
'numbers), files_only (paths only), count (match count per file).',
|
|
228
|
+
parameters: {
|
|
229
|
+
pattern: 'string',
|
|
230
|
+
path: 'string?',
|
|
231
|
+
include: 'string?',
|
|
232
|
+
output: 'string?',
|
|
233
|
+
},
|
|
234
|
+
fn: async ({ pattern, path: searchPath, include, output }) => {
|
|
235
|
+
const pat = req(pattern, 'pattern');
|
|
190
236
|
const target = searchPath ? safe(String(searchPath)) : workdir;
|
|
237
|
+
const inc = include ? String(include).trim() : '';
|
|
238
|
+
const mode = String(output || 'content').toLowerCase();
|
|
191
239
|
if (process.platform === 'win32') {
|
|
192
|
-
const escaped =
|
|
193
|
-
const scope = searchPath
|
|
194
|
-
? '"' + target + '\\*'
|
|
195
|
-
: '*';
|
|
240
|
+
const escaped = pat.replace(/"/g, '\\"');
|
|
241
|
+
const scope = searchPath ? '"' + target + '\\*' : '*';
|
|
196
242
|
return runShell(`findstr /s /n /r /c:"${escaped}" ` + scope);
|
|
197
243
|
}
|
|
198
|
-
|
|
244
|
+
const flags = mode === 'files_only'
|
|
245
|
+
? '-rlE'
|
|
246
|
+
: mode === 'count'
|
|
247
|
+
? '-rcE'
|
|
248
|
+
: '-rnE';
|
|
249
|
+
const includeArg = inc ? ' --include=' + JSON.stringify(inc) : '';
|
|
250
|
+
return runShell(`grep ${flags}${includeArg} ${JSON.stringify(pat)} ` +
|
|
251
|
+
`${JSON.stringify(target)} || true`);
|
|
199
252
|
},
|
|
200
253
|
},
|
|
201
254
|
];
|
|
202
255
|
const gitTools = createGitTools(workdir);
|
|
203
256
|
const webTools = createWebTools();
|
|
257
|
+
const extraTools = createExtraTools(workdir, { undo });
|
|
204
258
|
const respondTool = {
|
|
205
259
|
name: 'respond',
|
|
206
|
-
description: '
|
|
260
|
+
description: 'Give the final answer to the user and finish the task.',
|
|
207
261
|
parameters: { message: 'string' },
|
|
208
262
|
fn: async ({ message }) => message,
|
|
209
263
|
};
|
|
210
|
-
return [...baseTools, ...gitTools, ...webTools, respondTool];
|
|
264
|
+
return [...baseTools, ...extraTools, ...gitTools, ...webTools, respondTool];
|
|
211
265
|
}
|
package/dist/web.js
CHANGED
|
@@ -112,9 +112,9 @@ export function createWebTools() {
|
|
|
112
112
|
return [
|
|
113
113
|
{
|
|
114
114
|
name: 'WebFetch',
|
|
115
|
-
description: '
|
|
116
|
-
'
|
|
117
|
-
'
|
|
115
|
+
description: 'Fetch a URL and return the cleaned text of the page (without HTML tags). ' +
|
|
116
|
+
'Use it to read docs, articles, READMEs on GitHub. ' +
|
|
117
|
+
'If the page is rendered by JavaScript and the text is empty — retry with render=true.',
|
|
118
118
|
parameters: {
|
|
119
119
|
url: 'string',
|
|
120
120
|
render: 'boolean?',
|
|
@@ -122,7 +122,7 @@ export function createWebTools() {
|
|
|
122
122
|
},
|
|
123
123
|
fn: async ({ url, render, maxChars }) => {
|
|
124
124
|
if (!/^https?:\/\//i.test(String(url))) {
|
|
125
|
-
return
|
|
125
|
+
return `Error: URL must start with http:// or https://`;
|
|
126
126
|
}
|
|
127
127
|
const limit = Math.min(Number(maxChars) || MAX_TEXT, 60_000);
|
|
128
128
|
try {
|
|
@@ -143,21 +143,21 @@ export function createWebTools() {
|
|
|
143
143
|
if (text.length < 200) {
|
|
144
144
|
return (`HTTP ${r.status} ${r.url}\n` +
|
|
145
145
|
`Content-Type: ${r.contentType}\n\n` +
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
`The page is almost empty in raw HTML (${text.length} chars). ` +
|
|
147
|
+
`It looks like the content is rendered by JavaScript. ` +
|
|
148
|
+
`Retry with render=true.\n\n---\n${text}`);
|
|
149
149
|
}
|
|
150
150
|
return formatResult(r.status, r.url, text, limit);
|
|
151
151
|
}
|
|
152
152
|
catch (e) {
|
|
153
|
-
return
|
|
153
|
+
return `Error fetching ${url}: ${e.message}`;
|
|
154
154
|
}
|
|
155
155
|
},
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
name: 'WebSearch',
|
|
159
|
-
description: '
|
|
160
|
-
'
|
|
159
|
+
description: 'Search the web via DuckDuckGo HTML (no API key). ' +
|
|
160
|
+
'Returns a list of results: title, URL, short description.',
|
|
161
161
|
parameters: {
|
|
162
162
|
query: 'string',
|
|
163
163
|
maxResults: 'number?',
|
|
@@ -165,7 +165,7 @@ export function createWebTools() {
|
|
|
165
165
|
fn: async ({ query, maxResults }) => {
|
|
166
166
|
const q = encodeURIComponent(String(query || ''));
|
|
167
167
|
if (!q)
|
|
168
|
-
return '
|
|
168
|
+
return 'Error: query is empty.';
|
|
169
169
|
const limit = Math.min(Math.max(Number(maxResults) || 8, 1), 20);
|
|
170
170
|
try {
|
|
171
171
|
const r = await httpFetch(`https://html.duckduckgo.com/html/?q=${q}`, {
|
|
@@ -175,7 +175,7 @@ export function createWebTools() {
|
|
|
175
175
|
},
|
|
176
176
|
});
|
|
177
177
|
if (r.status !== 200) {
|
|
178
|
-
return `DuckDuckGo
|
|
178
|
+
return `DuckDuckGo returned HTTP ${r.status}`;
|
|
179
179
|
}
|
|
180
180
|
// We parse with simple regexes. The html.duckduckgo.com format is stable.
|
|
181
181
|
const results = [];
|
|
@@ -194,14 +194,14 @@ export function createWebTools() {
|
|
|
194
194
|
results.push({ title, url: href, snippet });
|
|
195
195
|
}
|
|
196
196
|
if (!results.length) {
|
|
197
|
-
return
|
|
197
|
+
return `No results found. The DuckDuckGo output format may have changed.`;
|
|
198
198
|
}
|
|
199
199
|
const lines = results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}` +
|
|
200
200
|
(r.snippet ? `\n ${r.snippet}` : ''));
|
|
201
201
|
return lines.join('\n\n');
|
|
202
202
|
}
|
|
203
203
|
catch (e) {
|
|
204
|
-
return
|
|
204
|
+
return `Search error: ${e.message}`;
|
|
205
205
|
}
|
|
206
206
|
},
|
|
207
207
|
},
|
|
@@ -215,5 +215,5 @@ function formatResult(status, url, text, limit) {
|
|
|
215
215
|
truncated = true;
|
|
216
216
|
}
|
|
217
217
|
return (`HTTP ${status} ${url}\n\n${out}` +
|
|
218
|
-
(truncated ? `\n\n[
|
|
218
|
+
(truncated ? `\n\n[...truncated at ${limit} chars]` : ''));
|
|
219
219
|
}
|