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 CHANGED
@@ -76,6 +76,45 @@ zames --version
76
76
  zames --help
77
77
  ```
78
78
 
79
+ ## Tools
80
+
81
+ The agent has the same style of tools as Claude Code / Codex CLI:
82
+
83
+ - **File tools** — `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`. `Read`
84
+ returns raw content by default; pass `numbered=true` to get `cat -n`-style
85
+ line numbers (for reference only — do not paste them into `Edit`).
86
+ - **Extra tools** — `LS` (list a directory), `MultiEdit` (several edits to one
87
+ file applied atomically), `TodoWrite` (session task checklist), `ApplyPatch`
88
+ (multi-file patch in Codex's V4A format: `*** Begin Patch` … `*** End Patch`).
89
+ - **Git** — `GitStatus`, `GitDiff`, `GitLog`, `GitAdd`, `GitCommit`, `GitPush`.
90
+ - **Web** — `WebFetch`, `WebSearch`.
91
+ - **Service** — `respond` (final answer to the operator, ends the task).
92
+
93
+ All file tools stay inside the working directory (sandbox). `Write`/`Edit` and
94
+ `MultiEdit`/`ApplyPatch` make a backup (undo) before touching a file.
95
+
96
+ ## Slash commands
97
+
98
+ Type / in the prompt for hints (Tab completes). Besides the session and
99
+ config commands (/new, /chats, /resume, /cd, /status, /config,
100
+ /undo, /transcript, /mcp, /skills, /memory, /init, /reload,
101
+ /debug-dom, /help, /exit) there are a few that mirror Claude Code /
102
+ Codex CLI:
103
+
104
+ - /diff [--staged] — show the working-tree git diff (--staged for the index).
105
+ - /cost (alias /usage) — session stats: tasks, tool calls, duration
106
+ (DeepSeek web does not expose token counts).
107
+ - /export [file] — write the session transcript to a Markdown file
108
+ (zames-export-<stamp>.md by default).
109
+ - /doctor — diagnose node, git, config, browser, clipboard and MCP.
110
+ - /permissions — show the confirmation settings (Write/Edit/Bash + the
111
+ alwaysConfirm regex list).
112
+ - /add-dir <path> — validate an extra directory (the sandbox is fixed at
113
+ startup; relaunch with --dir to write there).
114
+ - /review [focus] [--staged] — ask the agent to review uncommitted changes
115
+ and report findings (no code changes).
116
+
117
+
79
118
  ## Project context, skills and memory
80
119
 
81
120
  Like Codex / Claude Code, zames reads project instructions and reusable
@@ -194,20 +194,20 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
194
194
  attempt: afterToolRetries,
195
195
  error: e.message,
196
196
  });
197
- safeWarning('browser.ask() не вернул ответ за ' +
197
+ safeWarning('browser.ask() did not return an answer within ' +
198
198
  Math.round(askDeadlineMs / 1000) +
199
- 'с — повторяю запрос.');
199
+ 's — retrying the request.');
200
200
  if (afterToolRetries < MAX_AFTER_TOOL_RETRIES) {
201
201
  afterToolRetries++;
202
202
  await new Promise((r) => setTimeout(r, 1500));
203
203
  continue;
204
204
  }
205
205
  transcript?.log('ask_timeout_exhausted', {
206
- message: 'ask() не вернул ответ и лимит повторов исчерпан',
206
+ message: 'ask() did not return an answer and the retry limit is exhausted',
207
207
  });
208
- safeWarning('browser.ask() перестал отвечать; лимит повторов исчерпан, ' +
209
- 'останавливаюсь. Ответа модели нет — проверьте чат DeepSeek вручную.');
210
- return 'ask() watchdog: ответ модели не получен';
208
+ safeWarning('browser.ask() stopped responding; retry limit exhausted, ' +
209
+ 'stopping. No model answer — check the DeepSeek chat manually.');
210
+ return 'ask() watchdog: no model answer received';
211
211
  }
212
212
  await reportChat();
213
213
  transcript?.log('assistant_raw', { response: rawResponse });
@@ -262,10 +262,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
262
262
  response: String(rawResponse || '').slice(0, 200),
263
263
  });
264
264
  message =
265
- 'Ты остановился после результата инструмента. Продолжи работу: ' +
266
- 'ответь РОВНО одним JSON-объектом вызова инструмента, без текста до и после, ' +
267
- 'например: {"tool": "Bash", "args": {"command": "..."}}. ' +
268
- 'Если задача действительно выполнена — вызови respond с итоговым сообщением.';
265
+ 'You stopped after a tool result. Continue the work: ' +
266
+ 'reply with EXACTLY one JSON tool-call object, no text before or after, ' +
267
+ 'for example: {"tool": "Bash", "args": {"command": "..."}}. ' +
268
+ 'If the task is really done — call respond with the final message.';
269
269
  await new Promise((r) => setTimeout(r, 1500));
270
270
  continue;
271
271
  }
@@ -287,7 +287,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
287
287
  if (parsedCalls.some((p) => p && p._permissive)) {
288
288
  transcript?.log('permissive_parse', { response: rawResponse });
289
289
  if (debugLog) {
290
- console.error('внимание: tool-call распознан нестрогим парсером');
290
+ console.error('warning: tool-call recognized by the permissive parser');
291
291
  }
292
292
  }
293
293
  if (!parsed) {
@@ -304,17 +304,17 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
304
304
  response: rawResponse,
305
305
  });
306
306
  if (debugLog) {
307
- console.error('внимание: ответ похож на tool-call, но не распознан (попытка ' +
307
+ console.error('warning: the answer looks like a tool-call but was not recognized (attempt ' +
308
308
  malformedRetries +
309
309
  '/' +
310
310
  MAX_MALFORMED_RETRIES +
311
311
  ')');
312
312
  }
313
313
  message =
314
- 'Твой предыдущий ответ не распознан как вызов инструмента. ' +
315
- 'Ответь РОВНО одним JSON-объектом вызова инструмента, без текста до и после. ' +
316
- 'НЕ используй XML/DSML-теги — только JSON. ' +
317
- 'Например: {"tool": "Read", "args": {"path": "src/index.js"}}';
314
+ 'Your previous answer was not recognized as a tool call. ' +
315
+ 'Reply with EXACTLY one JSON tool-call object, no text before or after. ' +
316
+ 'Do NOT use XML/DSML tags — plain JSON only. ' +
317
+ 'For example: {"tool": "Read", "args": {"path": "src/index.js"}}';
318
318
  continue;
319
319
  }
320
320
  const trimmed = (rawResponse || '').trim();
@@ -336,16 +336,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
336
336
  response: rawResponse,
337
337
  });
338
338
  if (debugLog) {
339
- console.error('внимание: пустой/служебный ответ, прошу продолжить (попытка ' +
339
+ console.error('warning: empty/service answer, asking to continue (attempt ' +
340
340
  stallRetries +
341
341
  '/' +
342
342
  MAX_STALL_RETRIES +
343
343
  ')');
344
344
  }
345
345
  message =
346
- 'Продолжи выполнение задачи. Если нужен инструмент — ответь РОВНО ' +
347
- 'одним JSON-объектом вызова: {"tool": "...", "args": {...}}. ' +
348
- 'Если задача выполнена — вызови инструмент respond с итоговым сообщением.';
346
+ 'Continue the task. If you need a tool — reply with EXACTLY ' +
347
+ 'one JSON tool-call object: {"tool": "...", "args": {...}}. ' +
348
+ 'If the task is done — call the respond tool with the final message.';
349
349
  continue;
350
350
  }
351
351
  // The answer looks like "I'll call a tool now", but contains no call.
@@ -361,18 +361,18 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
361
361
  response: rawResponse,
362
362
  });
363
363
  if (debugLog) {
364
- console.error('внимание: ответ похож на незавершённую работу, прошу продолжить (попытка ' +
364
+ console.error('warning: the answer looks like unfinished work, asking to continue (attempt ' +
365
365
  looksDoneRetries +
366
366
  '/' +
367
367
  MAX_LOOKSDONE_RETRIES +
368
368
  ')');
369
369
  }
370
370
  message =
371
- 'Похоже, ты собирался вызвать инструмент, но не вызвал. ' +
372
- 'Если задача ещё не выполнена — ответь РОВНО одним JSON-объектом ' +
373
- 'вызова инструмента, без текста до и после. ' +
374
- 'Если задача действительно выполнена — вызови respond с итоговым ' +
375
- 'сообщением оператору.';
371
+ 'It looks like you meant to call a tool but did not. ' +
372
+ 'If the task is not finished — reply with EXACTLY one JSON ' +
373
+ 'tool-call object, no text before or after. ' +
374
+ 'If the task is really done — call respond with the final ' +
375
+ 'message to the operator.';
376
376
  continue;
377
377
  }
378
378
  // STRICT: only tool calls and respond reach the operator. Plain text is
@@ -387,12 +387,12 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
387
387
  });
388
388
  message =
389
389
  (justRanTool
390
- ? 'Ты остановился после вызова инструмента и написал обычный текст. '
391
- : 'Ты написал обычный текст без вызова инструмента. ') +
392
- 'Задача ещё не завершена. Ответь РОВНО одним JSON-объектом вызова ' +
393
- 'инструмента, без текста до и после, например: ' +
390
+ ? 'You stopped after a tool call and wrote plain text. '
391
+ : 'You wrote plain text without a tool call. ') +
392
+ 'The task is not finished. Reply with EXACTLY one JSON tool-call ' +
393
+ 'object, no text before or after, for example: ' +
394
394
  '{\"tool\": \"Bash\", \"args\": {\"command\": \"...\"}}. ' +
395
- 'Если задача действительно выполнена — вызови respond с итоговым сообщением.';
395
+ 'If the task is really done — call respond with the final message.';
396
396
  continue;
397
397
  }
398
398
  // Budget exhausted. Ask for respond EXACTLY once more; if the model
@@ -404,8 +404,8 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
404
404
  response: rawResponse.slice(0, 500),
405
405
  });
406
406
  message =
407
- 'Последний шаг: вызови инструмент respond с итоговым сообщением ' +
408
- 'оператору. Не пиши обычный текст — только вызов respond, например: ' +
407
+ 'Last step: call the respond tool with the final message ' +
408
+ 'to the operator. Do not write plain text — only a respond call, for example: ' +
409
409
  '{\"tool\": \"respond\", \"args\": {\"message\": \"...\"}}';
410
410
  continue;
411
411
  }
@@ -450,16 +450,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
450
450
  stallRetries++;
451
451
  transcript?.log('empty_respond', { attempt: stallRetries });
452
452
  if (debugLog) {
453
- console.error('внимание: пустой respond, прошу продолжить (попытка ' +
453
+ console.error('warning: empty respond, asking to continue (attempt ' +
454
454
  stallRetries +
455
455
  '/' +
456
456
  MAX_STALL_RETRIES +
457
457
  ')');
458
458
  }
459
459
  message =
460
- 'Ты вызвал respond с пустым message. Если задача выполнена — ' +
461
- 'вызови respond с итоговым сообщением оператору. Если нет — ' +
462
- 'продолжи работу вызовом инструмента.';
460
+ 'You called respond with an empty message. If the task is done — ' +
461
+ 'call respond with the final message to the operator. If not — ' +
462
+ 'continue the work with a tool call.';
463
463
  continue;
464
464
  }
465
465
  // An EMPTY respond must NEVER be a silent final: the operator would see
@@ -469,9 +469,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
469
469
  // the task normally.
470
470
  if (!isMeaningfulRespond(msg)) {
471
471
  transcript?.log('empty_respond_exhausted', { response: rawResponse });
472
- safeWarning('Модель вызвала respond без текста, и лимит повторов исчерпан. ' +
473
- 'Проверьте чат DeepSeek вручную.');
474
- return 'Модель не сформировала итоговое сообщение (пустой respond).';
472
+ safeWarning('The model called respond without text, and the retry limit is exhausted. ' +
473
+ 'Check the DeepSeek chat manually.');
474
+ return 'The model produced no final message (empty respond).';
475
475
  }
476
476
  safeAssistantMessage(msg);
477
477
  transcript?.log('assistant_final', { message: msg });
@@ -485,7 +485,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
485
485
  for (const call of callsToRun) {
486
486
  const tool = tools.find((t) => t.name === call.tool);
487
487
  if (!tool) {
488
- const err = `Неизвестный инструмент: ${call.tool}`;
488
+ const err = `Unknown tool: ${call.tool}`;
489
489
  safeToolResult(err);
490
490
  transcript?.log('tool_error', { tool: call.tool, error: err });
491
491
  results.push({ tool: call.tool, result: err });
@@ -498,7 +498,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
498
498
  result = await tool.fn(call.args);
499
499
  }
500
500
  catch (e) {
501
- result = `Ошибка: ${e.message}`;
501
+ result = `Error: ${e.message}`;
502
502
  }
503
503
  safeToolResult(result);
504
504
  transcript?.log('tool_result', {
@@ -536,7 +536,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
536
536
  .join('\n\n');
537
537
  }
538
538
  }
539
- return 'Достигнут лимит итераций.';
539
+ return 'Iteration limit reached.';
540
540
  }
541
541
  // The answer looks like a tool call, but parseToolCall() did not recognize it.
542
542
  // Used as a safeguard against "the agent called a tool and stopped": in that
@@ -0,0 +1,201 @@
1
+ import path from 'path';
2
+ // Helpers for the extra slash commands (/diff, /cost, /export, /doctor,
3
+ // /permissions, /review, /add-dir). Pure functions, unit-tested without a
4
+ // live agent/browser. index.ts only renders their output.
5
+ const NL = String.fromCharCode(10);
6
+ const CR = String.fromCharCode(13);
7
+ // ---------- /diff ----------
8
+ export function formatDiff(diffText, opts = {}) {
9
+ const maxLines = opts.maxLines ?? 200;
10
+ const trimmed = String(diffText ?? '').split(CR + NL).join(NL).trimEnd();
11
+ if (!trimmed)
12
+ return '(no changes)';
13
+ const lines = trimmed.split(NL);
14
+ if (lines.length <= maxLines)
15
+ return trimmed;
16
+ const head = lines.slice(0, maxLines).join(NL);
17
+ const rest = lines.length - maxLines;
18
+ return (head +
19
+ NL +
20
+ '... [' + rest + ' more lines, use Bash git diff for the full output]');
21
+ }
22
+ export function diffGitArgs(staged = false) {
23
+ return staged ? 'git diff --staged' : 'git diff';
24
+ }
25
+ export function summarizeTranscript(entries) {
26
+ const stats = {
27
+ turns: 0,
28
+ toolCalls: 0,
29
+ toolCounts: {},
30
+ durationMs: 0,
31
+ startedAt: null,
32
+ };
33
+ for (const e of entries) {
34
+ if (!e || typeof e !== 'object')
35
+ continue;
36
+ if (e.type === 'user_task')
37
+ stats.turns++;
38
+ if (e.type === 'tool_call') {
39
+ stats.toolCalls++;
40
+ const tool = String(e.tool ?? '?');
41
+ stats.toolCounts[tool] = (stats.toolCounts[tool] || 0) + 1;
42
+ }
43
+ if (typeof e.elapsed === 'number' && e.elapsed > stats.durationMs) {
44
+ stats.durationMs = e.elapsed;
45
+ }
46
+ if (!stats.startedAt && typeof e.ts === 'string')
47
+ stats.startedAt = e.ts;
48
+ }
49
+ return stats;
50
+ }
51
+ export function parseTranscript(body) {
52
+ const out = [];
53
+ for (const line of String(body ?? '').split(NL)) {
54
+ const t = line.trim();
55
+ if (!t)
56
+ continue;
57
+ try {
58
+ const v = JSON.parse(t);
59
+ if (v && typeof v === 'object')
60
+ out.push(v);
61
+ }
62
+ catch {
63
+ // skip malformed lines
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ export function formatDuration(ms) {
69
+ const total = Math.max(0, Math.round(ms / 1000));
70
+ const h = Math.floor(total / 3600);
71
+ const m = Math.floor((total % 3600) / 60);
72
+ const s = total % 60;
73
+ const pad = (n) => String(n).padStart(2, '0');
74
+ if (h > 0)
75
+ return h + 'h ' + pad(m) + 'm ' + pad(s) + 's';
76
+ if (m > 0)
77
+ return m + 'm ' + pad(s) + 's';
78
+ return s + 's';
79
+ }
80
+ export function renderCost(stats, transcriptFile) {
81
+ const lines = [];
82
+ lines.push('Session stats (DeepSeek web does not expose token counts):');
83
+ lines.push(' tasks: ' + stats.turns);
84
+ lines.push(' tool calls: ' + stats.toolCalls);
85
+ const top = Object.entries(stats.toolCounts).sort((a, b) => b[1] - a[1]);
86
+ if (top.length) {
87
+ lines.push(' by tool:');
88
+ for (const [name, n] of top)
89
+ lines.push(' ' + name + ': ' + n);
90
+ }
91
+ lines.push(' duration: ' + formatDuration(stats.durationMs));
92
+ if (stats.startedAt)
93
+ lines.push(' started: ' + stats.startedAt);
94
+ lines.push(' transcript: ' + (transcriptFile || '(off)'));
95
+ return lines.join(NL);
96
+ }
97
+ // ---------- /export ----------
98
+ export function formatExport(entries, meta = {}) {
99
+ const out = [];
100
+ out.push('# zames session export');
101
+ out.push('');
102
+ if (meta.workdir)
103
+ out.push('- workdir: ' + meta.workdir);
104
+ if (meta.chatId)
105
+ out.push('- chat: ' + meta.chatId);
106
+ out.push('- exported: ' + new Date().toISOString());
107
+ out.push('');
108
+ for (const e of entries) {
109
+ const type = String(e.type ?? '?');
110
+ if (type === 'user_task') {
111
+ out.push('## Task');
112
+ out.push('');
113
+ out.push(String(e.task ?? ''));
114
+ out.push('');
115
+ }
116
+ else if (type === 'tool_call') {
117
+ out.push('- tool ' +
118
+ String(e.tool ?? '?') +
119
+ ' ' +
120
+ JSON.stringify(e.args ?? {}));
121
+ }
122
+ else if (type === 'tool_result') {
123
+ out.push(' -> ' +
124
+ String(e.tool ?? '?') +
125
+ ': ' +
126
+ String(e.result ?? '').slice(0, 500));
127
+ }
128
+ else if (type === 'assistant_final') {
129
+ out.push('## Answer');
130
+ out.push('');
131
+ out.push(String(e.message ?? ''));
132
+ out.push('');
133
+ }
134
+ }
135
+ return out.join(NL);
136
+ }
137
+ export function defaultExportPath(workdir, now = new Date()) {
138
+ const stamp = now
139
+ .toISOString()
140
+ .replace(/[:.]/g, '-')
141
+ .replace('T', '_')
142
+ .replace('Z', '');
143
+ return path.join(workdir, 'zames-export-' + stamp + '.md');
144
+ }
145
+ export function renderDoctor(d) {
146
+ const rows = [];
147
+ const row = (ok, label, value) => {
148
+ rows.push(' ' + (ok ? '[OK] ' : '[WARN]') + ' ' + label.padEnd(16) + ' ' + value);
149
+ };
150
+ row(true, 'node', d.nodeVersion);
151
+ row(true, 'platform', d.platform);
152
+ row(true, 'workdir', d.workdir);
153
+ row(d.gitOk, 'git', d.gitOk ? 'repo (' + (d.gitBranch || 'detached') + ')' : 'not a repository');
154
+ row(d.configOk, 'config', d.configOk ? 'loaded' : 'error: ' + (d.configError || 'unknown'));
155
+ row(true, 'browser', d.browserChannel ? d.browserChannel : 'bundled chromium');
156
+ row(!!d.clipboardTool, 'clipboard', d.clipboardTool || 'no tool found (image paste disabled)');
157
+ row(true, 'mcp', d.mcpServers + ' server(s), ' + d.mcpTools + ' tool(s)');
158
+ row(d.transcriptOk, 'transcript', d.transcriptOk ? 'on' : 'off');
159
+ return 'Doctor:' + NL + rows.join(NL);
160
+ }
161
+ export function renderPermissions(p) {
162
+ const onoff = (b) => (b ? 'ask' : 'allow');
163
+ const lines = [];
164
+ lines.push('Tool permissions (confirmation settings):');
165
+ lines.push(' Write: ' + onoff(p.write));
166
+ lines.push(' Edit: ' + onoff(p.edit));
167
+ lines.push(' Bash: ' + onoff(p.bash));
168
+ if (p.alwaysConfirm.length) {
169
+ lines.push(' Always confirm (regex):');
170
+ for (const re of p.alwaysConfirm)
171
+ lines.push(' ' + re);
172
+ }
173
+ lines.push('');
174
+ lines.push('Change with: /config set confirmation.write false (and .edit / .bash)');
175
+ return lines.join(NL);
176
+ }
177
+ // ---------- /add-dir ----------
178
+ export function resolveExtraDir(input, workdir) {
179
+ const raw = String(input ?? '').trim();
180
+ if (!raw)
181
+ return { error: 'Usage: /add-dir <path>' };
182
+ const abs = path.resolve(workdir, raw);
183
+ if (abs === path.resolve(workdir)) {
184
+ return { error: 'This is already the working directory.' };
185
+ }
186
+ return { path: abs };
187
+ }
188
+ // ---------- /review ----------
189
+ export function buildReviewPrompt(focus, hasStaged = false) {
190
+ const scope = hasStaged ? 'staged' : 'uncommitted';
191
+ const f = String(focus ?? '').trim();
192
+ let s = 'Review the ' +
193
+ scope +
194
+ ' changes in this repository (use GitDiff). ' +
195
+ 'Focus on real bugs: correctness, race conditions, error handling, ' +
196
+ 'security, and missing tests. Do NOT invent problems and do NOT change ' +
197
+ 'code: only report findings with file:line and a short rationale.';
198
+ if (f)
199
+ s += NL + 'Extra focus: ' + f;
200
+ return s;
201
+ }