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.
@@ -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}\nПараметры: ${JSON.stringify(t2.parameters)}`)
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(`Доступ за пределы рабочей директории: ${p}`);
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: выход за пределы ' + root + ' запрещён (cd ' + raw + ')');
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(`⏱ Таймаут после ${timeout}ms — процесс убит.`);
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(`Убит сигналом: ${err.signal}`);
70
+ parts.push(`Killed by signal: ${err.signal}`);
66
71
  }
67
72
  else {
68
- parts.push(`Ошибка: ${err.message}`);
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: 'Прочитать содержимое файла. Опционально: offset и limit (строки).',
91
- parameters: { path: 'string', offset: 'number?', limit: 'number?' },
92
- fn: async ({ path: p, offset, limit }) => {
93
- const file = safe(String(p));
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
- return lines.slice(start, end).join('\n');
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: 'Создать или перезаписать файл. content — текст; content_base64 — тот же ' +
104
- 'контент в base64 (используй, если текст содержит , обратные слэши, ' +
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(String(p));
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 `Файл записан: ${p}`;
154
+ return `File written: ${p}`;
119
155
  },
120
156
  },
121
157
  {
122
158
  name: 'Edit',
123
- description: 'Точечная замена строки в файле. old_string должен встречаться один раз. ' +
124
- 'old_base64/new_base64 — те же строки в 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(String(p));
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(`Строка не найдена в ${p}: "${oldStr.slice(0, 60)}..."`);
179
+ throw new Error(`String not found in ${p}: "${oldStr.slice(0, 60)}..."`);
144
180
  }
145
181
  if (occurrences > 1) {
146
- throw new Error(`Строка встречается ${occurrences} раз в ${p}. Уточните old_string.`);
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 `Отредактирован: ${p}`;
188
+ return `Edited: ${p}`;
153
189
  },
154
190
  },
155
191
  {
156
192
  name: 'Bash',
157
- description: 'Выполнить shell-команду в рабочей директории (cmd.exe на Windows, sh на Linux/macOS). ' +
158
- 'Не использовать для long-running процессов (серверы) — уйдут в таймаут. ' +
159
- 'Не использовать для команд, требующих интерактивного ввода. ' +
160
- 'Для git — инструменты Git*. Для интернета — WebFetch / WebSearch.',
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(String(command));
164
- return runShell(String(command), timeout);
199
+ assertCommandInsideRoot(req(command, 'command'));
200
+ return runShell(req(command, 'command'), timeout);
165
201
  },
166
202
  },
167
203
  {
168
204
  name: 'Glob',
169
- description: 'Найти файлы по glob-паттерну (например, "**/*.js").',
170
- parameters: { pattern: 'string' },
171
- fn: async ({ pattern }) => {
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(String(pattern), { cwd: workdir })) {
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(workdir, f);
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
- parameters: { pattern: 'string', path: 'string?' },
189
- fn: async ({ pattern, path: searchPath }) => {
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 = String(pattern).replace(/"/g, '\\"');
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
- return runShell(`grep -rn -E ${JSON.stringify(pattern)} ${JSON.stringify(target)} || true`);
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: 'Загрузить URL и вернуть очищенный текст страницы (без HTML-тегов). ' +
116
- 'Используй для чтения документации, статей, README на GitHub. ' +
117
- 'Если страница рендерится JavaScript-ом и текст пустой — попробуй ещё раз с render=true.',
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 `Ошибка: URL должен начинаться с http:// или https://`;
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
- `Страница почти пустая в сыром HTML (${text.length} символов). ` +
147
- `Похоже, контент рендерится JavaScript-ом. ` +
148
- `Повтори вызов с render=true.\n\n---\n${text}`);
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 `Ошибка загрузки ${url}: ${e.message}`;
153
+ return `Error fetching ${url}: ${e.message}`;
154
154
  }
155
155
  },
156
156
  },
157
157
  {
158
158
  name: 'WebSearch',
159
- description: 'Поиск в интернете через DuckDuckGo HTML (без API-ключа). ' +
160
- 'Возвращает список результатов: заголовок, URL, краткое описание.',
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 'Ошибка: query пустой.';
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 вернул HTTP ${r.status}`;
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 `Результатов не найдено. Возможно, изменился формат выдачи DuckDuckGo.`;
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 `Ошибка поиска: ${e.message}`;
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[...обрезано на ${limit} символах]` : ''));
218
+ (truncated ? `\n\n[...truncated at ${limit} chars]` : ''));
219
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {