zames_pro 2.6.0 → 2.8.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.
@@ -1,14 +1,14 @@
1
1
  import fs from 'fs/promises';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- // Извлечение СЫРОГО текста ответа модели из сетевых данных DeepSeek.
4
+ // Extraction of the RAW model answer text from DeepSeek's network data.
5
5
  //
6
- // Зачем: чтение ответа идёт из ОТРЕНДЕРЕННОГО DOM, а рендер DeepSeek
7
- // искажает ответ — превращает доллар-формулы в LaTeX (символ доллара
8
- // теряется), нормализует переводы строк, делает автолинки. Из-за этого
9
- // tool-call с шаблонными строками и экранированными переводами строк в
10
- // аргументах доходил до инструментов искажённым. Здесь мы достаём исходный
11
- // текст из тела сетевого ответа.
6
+ // Why: the answer is read from the RENDERED DOM, and DeepSeek's rendering
7
+ // distorts the answer — it turns dollar formulas into LaTeX (the dollar sign
8
+ // is lost), normalizes newlines, and auto-links. Because of this a tool-call
9
+ // with template strings and escaped newlines in its arguments reached the
10
+ // tools distorted. Here we extract the original text from the network
11
+ // response body.
12
12
  //
13
13
  // Format DeepSeek (SSE, chat.deepseek.com/api/v0/chat/completion):
14
14
  // * startovy fragment otveta lezhit v data-chanke s uzlom v.response
@@ -36,11 +36,11 @@ function parseDataLines(body) {
36
36
  }
37
37
  return out;
38
38
  }
39
- // Собирает текст ответа из SSE-потока. Поддерживает два формата:
40
- // * OpenAI-совместимый: choices[].delta.content (reasoning_content
41
- // игнорируется — это размышления, а не ответ);
42
- // * DeepSeek chat.deepseek.com: стартовый v.response.fragments[] и
43
- // инкрементальные APPEND-чанки response/fragments/-1/content.
39
+ // Assembles the answer text from the SSE stream. Supports two formats:
40
+ // * OpenAI-compatible: choices[].delta.content (reasoning_content
41
+ // is ignored — that's reasoning, not the answer);
42
+ // * DeepSeek chat.deepseek.com: the initial v.response.fragments[] and
43
+ // incremental APPEND chunks response/fragments/-1/content.
44
44
  export function extractFromSse(body) {
45
45
  let out = '';
46
46
  for (const obj of parseDataLines(body)) {
@@ -65,8 +65,8 @@ export function extractFromSse(body) {
65
65
  }
66
66
  continue;
67
67
  }
68
- // Инкрементальный APPEND: p='response/fragments/-1/content' -> v=строка;
69
- // последующие чанки идут с одним полем v.
68
+ // Incremental APPEND: p='response/fragments/-1/content' -> v=string;
69
+ // subsequent chunks come with a single v field.
70
70
  if (o.p === 'response/fragments/-1/content' && typeof o.v === 'string') {
71
71
  out += o.v;
72
72
  continue;
@@ -87,9 +87,9 @@ export function extractFromJson(body) {
87
87
  }
88
88
  return pickAnswerText(obj);
89
89
  }
90
- // Достает финальный текст ответа из узла, НЕ смешивая его с reasoning.
91
- // Порядок: choices/messages -> delta/message -> content; reasoning_content
92
- // намеренно не берем — это размышления модели, а не ответ.
90
+ // Extracts the final answer text from a node, WITHOUT mixing it with reasoning.
91
+ // Order: choices/messages -> delta/message -> content; reasoning_content is
92
+ // deliberately not taken — that's the model's reasoning, not the answer.
93
93
  function pickAnswerText(node) {
94
94
  if (node == null)
95
95
  return '';
@@ -124,15 +124,15 @@ function pickAnswerText(node) {
124
124
  }
125
125
  return '';
126
126
  }
127
- // Универсальная попытка: сначала SSE, затем обычный JSON.
127
+ // A universal attempt: first SSE, then regular JSON.
128
128
  export function extractAnswer(body) {
129
129
  const sse = extractFromSse(body);
130
130
  if (sse)
131
131
  return sse;
132
132
  return extractFromJson(body);
133
133
  }
134
- // Сохраняет тело сетевого ответа DeepSeek на диск для разбора постфактум.
135
- // Файлы лежат в ~/.zames/net-log — po nim видно реальный формат ответа.
134
+ // Saves the DeepSeek network response body to disk for post-mortem analysis.
135
+ // The files live in ~/.zames/net-log — from them the real answer format is visible.
136
136
  export async function dumpNetBody(url, body) {
137
137
  try {
138
138
  const dir = path.join(os.homedir(), '.zames', 'net-log');
@@ -4,10 +4,10 @@ import { theme } from './theme.js';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { ZAMES_HOME } from './config.js';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- // Исходники для самообзора — это .ts файлы. После сборки (tsc → dist/)
8
- // __dirname указывает на dist, где лежат .js. Поэтому ищем каталог с
9
- // исходниками: либо рядом (запуск из src/ через tsx), либо в ../src
10
- // (запуск собранного dist/ из корня репозитория).
7
+ // The sources for self-review are .ts files. After the build (tsc → dist/)
8
+ // __dirname points to dist, where the .js files live. So we look for the
9
+ // source directory: either nearby (running from src/ via tsx), or in ../src
10
+ // (running the built dist/ from the repo root).
11
11
  function resolveSrcDir() {
12
12
  const candidates = [__dirname, path.join(__dirname, '..', 'src')];
13
13
  for (const dir of candidates) {
@@ -22,9 +22,9 @@ function resolveSrcDir() {
22
22
  }
23
23
  const SRC_DIR = resolveSrcDir();
24
24
  const SNAP_ROOT = path.join(ZAMES_HOME, 'snapshots');
25
- // Динамический импорт логики с timestamp — чтобы при /reload (или авто-reload)
26
- // self-review использовал СВЕЖИЕ tools/agent-loop, а не закэшированные при
27
- // первой загрузке. Иначе reload не доходил бы до зависимостей self-review.
25
+ // Dynamic import of the logic with a timestamp — so that on /reload (or
26
+ // auto-reload) self-review uses FRESH tools/agent-loop, not the ones cached on
27
+ // first load. Otherwise reload wouldn't reach self-review's dependencies.
28
28
  async function loadFresh() {
29
29
  const stamp = Date.now();
30
30
  const toolsUrl = new URL('./tools.js', import.meta.url);
@@ -66,7 +66,7 @@ export async function selfReview({ browser, focus, transcript }) {
66
66
  `Проверь, что src/ не пуст и ты запускаешь агента из корня проекта.\n`));
67
67
  throw new Error('SRC_DIR пуст — нечего ревьюить');
68
68
  }
69
- // Копируем package.json для контекста — чтобы агент видел зависимости
69
+ // Copy package.json for context — so the agent sees the dependencies
70
70
  try {
71
71
  const pkg = await fs.readFile(path.resolve(SRC_DIR, '..', 'package.json'), 'utf-8');
72
72
  await fs.writeFile(path.join(snapDir, 'package.json'), pkg, 'utf-8');
@@ -104,10 +104,10 @@ export async function selfReview({ browser, focus, transcript }) {
104
104
  console.log();
105
105
  },
106
106
  });
107
- // Сохраняем отчёт
107
+ // Save the report
108
108
  const reportPath = path.join(snapDir, '_report.md');
109
109
  await fs.writeFile(reportPath, finalMessage || '(пусто)', 'utf-8');
110
- // Считаем, что изменилось
110
+ // Compute what changed
111
111
  const changed = await diffFiles(SRC_DIR, snapDir);
112
112
  console.log(theme.system('─'.repeat(60)));
113
113
  console.log(theme.system(`Отчёт: ${reportPath}`));
@@ -159,7 +159,7 @@ export async function selfApply({ name }) {
159
159
  const snapStat = await fs.stat(snapDir).catch(() => null);
160
160
  if (!snapStat)
161
161
  throw new Error(`Снапшот не найден: ${snapDir}`);
162
- // Бэкап текущего src перед перезаписью
162
+ // Back up the current src before overwriting
163
163
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
164
164
  const backupDir = path.join(SNAP_ROOT, `backup-before-apply-${stamp}`);
165
165
  await copyDirJsFiles(SRC_DIR, backupDir);
@@ -264,16 +264,16 @@ ${focusLine}
264
264
 
265
265
  When done, respond with a markdown report as the message:
266
266
 
267
- ## Найдено
267
+ ## Found
268
268
  - (bullet list of issues you found, with file:line if possible)
269
269
 
270
- ## Исправлено
271
- - (bullet list: file — что именно изменил)
270
+ ## Fixed
271
+ - (bullet list: file — what exactly you changed)
272
272
 
273
- ## Не исправлено (осознанно)
273
+ ## Not fixed (deliberately)
274
274
  - (bullet list: what you left alone and why)
275
275
 
276
- ## Проверка
276
+ ## Verification
277
277
  - (bullet list: what you verified, e.g. \`node --check\` results)
278
278
 
279
279
  Be honest and specific. If the code is fine in some area, say so.`;
package/dist/sessions.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
- // Сессии (чаты DeepSeek) храним в отдельной папке, чтобы они не терялись
5
- // после перезапуска процесса. Каждая сессия — отдельный JSON-файл
6
- // <id>.json в ~/.zames/.sessions. last.json указывает на последнюю
7
- // открытую сессию для конкретной рабочей директории.
4
+ // We store sessions (DeepSeek chats) in a separate folder so they aren't lost
5
+ // after a process restart. Each session is a separate JSON file
6
+ // <id>.json in ~/.zames/.sessions. last.json points to the last
7
+ // opened session for a specific working directory.
8
8
  const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
9
9
  const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
10
10
  function ensureDir() {
@@ -13,8 +13,8 @@ function ensureDir() {
13
13
  function safeName(id) {
14
14
  return String(id).replace(/[^a-zA-Z0-9_.-]/g, '_');
15
15
  }
16
- // Сохраняем/обновляем сессию. workdir нужен, чтобы при запуске из того же
17
- // проекта восстанавливать именно его последний чат.
16
+ // Save/update a session. workdir is needed so that when launched from the same
17
+ // project we restore exactly its last chat.
18
18
  export function saveSession({ id, title = '', workdir = '', }) {
19
19
  if (!id)
20
20
  return null;
@@ -63,16 +63,16 @@ function writeIndex({ id, workdir }) {
63
63
  }
64
64
  catch { }
65
65
  }
66
- // Последняя сессия для рабочей директории. Если для неё ничего нет —
67
- // возвращаем последнюю сессию вообще (полезно при запуске из нового места).
66
+ // The last session for the working directory. If there is none —
67
+ // we return the last session overall (useful when launched from a new place).
68
68
  export function loadLastSession(workdir = '') {
69
69
  try {
70
70
  const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
71
71
  const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {};
72
72
  const candidates = [byWorkdir[workdir], index.last].filter((x) => Boolean(x));
73
73
  for (const id of candidates) {
74
- // Файл сессии мог быть удалён — индекс тогда устарел, и сессию
75
- // восстанавливать нельзя. Пробуем следующий кандидат.
74
+ // The session file may have been deleted — the index is then stale and
75
+ // the session cannot be restored. We try the next candidate.
76
76
  const s = readSession(id);
77
77
  if (s)
78
78
  return s;
@@ -94,7 +94,7 @@ export function readSession(id) {
94
94
  return null;
95
95
  }
96
96
  }
97
- // Список всех сессий, свежие — первыми.
97
+ // List all sessions, newest first.
98
98
  export function listSessions() {
99
99
  try {
100
100
  ensureDir();
package/dist/spinner.js CHANGED
@@ -2,8 +2,8 @@ import ora from 'ora';
2
2
  import { theme } from './theme.js';
3
3
  import { renderMarkdown } from './markdown.js';
4
4
  import { translate } from './i18n.js';
5
- // Фразы для спиннера «агент работает». Выбираются в случайном порядке.
6
- // Текст берётся из i18n по ключу 'spinner.phrases' (строки разделены «|»).
5
+ // Phrases for the "agent is working" spinner. Chosen in random order.
6
+ // The text comes from i18n via the key 'spinner.phrases' (strings separated by "|").
7
7
  export function randomThinkingPhrase(locale = 'ru') {
8
8
  const raw = translate(locale)('spinner.phrases');
9
9
  const phrases = raw.split('|').filter((s) => s.trim().length);
@@ -11,7 +11,7 @@ export function randomThinkingPhrase(locale = 'ru') {
11
11
  return '';
12
12
  return phrases[Math.floor(Math.random() * phrases.length)];
13
13
  }
14
- // Убираем завершающее многоточие из фразы — точки анимируем отдельно.
14
+ // Strip the trailing ellipsis from the phrase — dots are animated separately.
15
15
  export function stripEllipsis(phrase) {
16
16
  return phrase.replace(/[.…]+\s*$/, '');
17
17
  }
@@ -20,8 +20,8 @@ export function createSpinner(locale = 'ru') {
20
20
  let dotTimer = null;
21
21
  let dotPhase = 0;
22
22
  let pending = null;
23
- // Анимация точек: старт с пустой строки (0 точек), затем рост.
24
- // Ширину выравниваем по максимуму (3), чтобы подсказка не смещалась.
23
+ // Dot animation: start from an empty string (0 dots), then grow.
24
+ // We align the width to the maximum (3) so the hint doesn't shift.
25
25
  const DOTS = ['', '.', '..', '...'];
26
26
  const DOTS_PAD = ' ';
27
27
  const start = (text) => {
@@ -38,14 +38,14 @@ export function createSpinner(locale = 'ru') {
38
38
  if (spinner)
39
39
  spinner.stop();
40
40
  };
41
- // Подсказка в строке статуса: во время работы агента терминал живой,
42
- // можно печатать следующее сообщение. Без неё это неочевидно.
41
+ // Hint in the status line: while the agent works the terminal is live,
42
+ // so you can type the next message. Without it this is not obvious.
43
43
  const HINT = theme.dim(' · ' + translate(locale)('spinner.hint'));
44
- // Запуск анимированного статуса: коричневый текст + «бегущие» точки.
44
+ // Start the animated status: brown text + "running" dots.
45
45
  const startThinking = () => {
46
46
  const base = theme.brown(stripEllipsis(randomThinkingPhrase(locale)));
47
- // Точки того же цвета, что и база, и фиксированной ширины — иначе
48
- // подсказка справа «прыгает» при смене фазы анимации.
47
+ // The dots are the same color as the base and of fixed width — otherwise
48
+ // the hint on the right "jumps" when the animation phase changes.
49
49
  const dots = (n) => theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
50
50
  start(base + dots(0) + HINT);
51
51
  dotPhase = 0;
@@ -60,8 +60,8 @@ export function createSpinner(locale = 'ru') {
60
60
  if (dotTimer.unref)
61
61
  dotTimer.unref();
62
62
  };
63
- // Показать набранный, но ещё не отправленный текст вместо спиннера.
64
- // Позволяет печатать сообщение прямо во время работы агента.
63
+ // Show the typed but not yet sent text instead of the spinner.
64
+ // Lets you type a message right while the agent is working.
65
65
  const showPending = () => {
66
66
  if (dotTimer) {
67
67
  clearInterval(dotTimer);
@@ -80,8 +80,8 @@ export function createSpinner(locale = 'ru') {
80
80
  }
81
81
  startThinking();
82
82
  },
83
- // Текст, который пользователь набирает во время работы агента.
84
- // Пустая строка / null — вернуть обычный спиннер.
83
+ // The text the user types while the agent is working.
84
+ // Empty string / null — restore the regular spinner.
85
85
  setPending: (text) => {
86
86
  pending = text && String(text).length ? String(text) : null;
87
87
  if (pending)
@@ -104,12 +104,16 @@ export function createSpinner(locale = 'ru') {
104
104
  stop();
105
105
  const NL = String.fromCharCode(10);
106
106
  const rendered = renderMarkdown(msg);
107
- // Маркер ответа модели: помогает визуально отделить его от ввода
108
- // пользователя (который подсвечен приглашением с золотой стрелкой).
107
+ // Model answer marker: helps visually separate it from the user's input
108
+ // (which is highlighted by the prompt with a golden arrow).
109
109
  console.log(NL + theme.assistant('● Ответ') + NL);
110
110
  console.log(rendered);
111
111
  console.log(theme.dim('─'.repeat(60)) + NL);
112
112
  },
113
+ warning: (msg) => {
114
+ stop();
115
+ console.log(String.fromCharCode(10) + theme.warn('⚠ ' + msg));
116
+ },
113
117
  stop,
114
118
  succeed: () => { },
115
119
  fail: () => { },
@@ -1,5 +1,5 @@
1
1
  import { translate } from './i18n.js';
2
- export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', }) {
2
+ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], }) {
3
3
  const t = translate(locale);
4
4
  const toolDescriptions = tools
5
5
  .map((t2) => `### ${t2.name}\n${t2.description}\nПараметры: ${JSON.stringify(t2.parameters)}`)
@@ -7,6 +7,10 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
7
7
  const gitSection = gitContext
8
8
  ? `\n## Git context\n\n${gitContext}\n`
9
9
  : '\n## Git context\n\nNot a git repository (or git is not installed).\n';
10
+ const attachSection = attachments.length
11
+ ? `\n## Attachments\n\nThe user attached files to this task: ${attachments.join(', ')}.\n` +
12
+ `Each [image#N] / [file#N] marker corresponds to a file the user pasted into the terminal; the file was uploaded to the chat and also saved under <project>/tmp. Look at the images in the chat; for files, read the copy in tmp if you need the contents.\n`
13
+ : '';
10
14
  return `You are a coding agent running in a terminal. You help the user with software engineering tasks by reading files, writing code, running commands, and iterating until the task is done.
11
15
 
12
16
  You work in the directory: ${workdir}
@@ -46,7 +50,7 @@ operator will not read it, so never use plain text.\n\nNO PROSE AROUND TOOL CALL
46
50
  You have access to the following tools:
47
51
 
48
52
  ${toolDescriptions}
49
- ${gitSection}
53
+ ${gitSection}${attachSection}
50
54
  ## How to use tools
51
55
 
52
56
  To call ONE tool, respond with ONLY a JSON object (no markdown fences, no extra text):
@@ -65,6 +69,29 @@ To give a final answer to the user, respond with:
65
69
 
66
70
  {"tool": "respond", "args": {"message": "your final answer here"}}
67
71
 
72
+ ## TOOL CALL FORMAT - EXACT REQUIREMENTS (read carefully)
73
+
74
+ The parser is strict about the SHAPE of your call. These mistakes have caused
75
+ the agent to silently stall (the call was not recognized, so the task ended):
76
+
77
+ 1. ALWAYS wrap the call in curly braces { ... }. Never emit tool-colon-quote
78
+ without the leading brace. Never drop the very first character.
79
+ 2. ALWAYS put DOUBLE quotes around keys and string values, e.g.
80
+ {"tool": "Read", "args": {"path": "a.ts"}}.
81
+ NEVER use single quotes; that is not valid JSON.
82
+ 3. NEVER wrap the call in markdown fences or in XML/DSML tags
83
+ (invoke, parameter, tool_calls, DSML variants). Plain JSON only.
84
+ 4. NEVER put prose before or after the JSON in the same response. Not even
85
+ a short lead-in. The whole response is the JSON.
86
+ 5. Do NOT truncate long calls. If a Bash command, file content, or Edit is
87
+ large, SPLIT it: run several smaller Bash commands, or write the file in
88
+ parts (Write then Edit). A cut-off JSON never parses and stalls the agent.
89
+ 6. One object per call; to call several independent tools at once, use a JSON
90
+ ARRAY of objects, still no prose around it.
91
+
92
+ If you catch yourself about to emit anything other than a bare JSON object or
93
+ array, stop and reformat it first. A malformed call is worse than a slow one.
94
+
68
95
  ## Git
69
96
 
70
97
  - You CAN commit and push using GitAdd / GitCommit / GitPush. Prefer these over raw \`git\` through Bash.
package/dist/theme.js CHANGED
@@ -1,32 +1,32 @@
1
1
  import chalk from 'chalk';
2
- // Спокойная палитра для тёмного терминала.
3
- // Приглушённые, слегка «выцветшие» тона без высокой насыщенности
4
- // (в духе Tokyo Night / Nord). Задача — комфортное чтение длинных
5
- // ответов, а не максимальная контрастность.
2
+ // A calm palette for a dark terminal.
3
+ // Muted, slightly "faded" tones without high saturation
4
+ // (in the spirit of Tokyo Night / Nord). The goal is comfortable reading of
5
+ // long answers, not maximum contrast.
6
6
  //
7
- // Роли:
8
- // - user — мягкий лавандово-серый (служебные сообщения)
9
- // - prompt — тёплое золотистое (приглашение ввода, заметное)
10
- // - dir — светло-голубое (имя рабочей директории)
11
- // - assistant — тёплый песочный (ответы модели)
12
- // - tool — приглушённый янтарный
13
- // - toolResult — серо-голубой
14
- // - system — нейтральный серый
15
- // - warn — спокойный охра
16
- // - error — приглушённый терракот
7
+ // Roles:
8
+ // - user — soft lavender-gray (service messages)
9
+ // - prompt — warm gold (input prompt, noticeable)
10
+ // - dir — light blue (working directory name)
11
+ // - assistant — warm sand (model answers)
12
+ // - tool — muted amber
13
+ // - toolResult — gray-blue
14
+ // - system — neutral gray
15
+ // - warn — calm ochre
16
+ // - error — muted terracotta
17
17
  export const theme = {
18
- user: chalk.hex('#b4b8d0'), // мягкий лавандово-серый
19
- prompt: chalk.hex('#c8b06a').bold, // приглашение ввода — тёплое золотистое, заметное
20
- dir: chalk.hex('#7fc4f0'), // имя рабочей директории — заметно голубое
21
- assistant: chalk.hex('#cfc9b0'), // тёплый песочный
22
- tool: chalk.hex('#c6a97e'), // приглушённый янтарный
23
- toolResult: chalk.hex('#8a9bb5'), // серо-голубой
24
- system: chalk.hex('#808896'), // нейтральный серый
25
- dim: chalk.hex('#5b616e'), // тёмно-серый
26
- warn: chalk.hex('#c9a86a'), // спокойный охра
27
- error: chalk.hex('#c98a80'), // приглушённый терракот
28
- success: chalk.hex('#a9c08c'), // мягкий шалфейный
29
- brown: chalk.hex('#a1723f'), // коричневый (статус/спиннер)
18
+ user: chalk.hex('#b4b8d0'), // soft lavender-gray
19
+ prompt: chalk.hex('#c8b06a').bold, // input prompt — warm gold, noticeable
20
+ dir: chalk.hex('#7fc4f0'), // working directory name — noticeably blue
21
+ assistant: chalk.hex('#cfc9b0'), // warm sand
22
+ tool: chalk.hex('#c6a97e'), // muted amber
23
+ toolResult: chalk.hex('#8a9bb5'), // gray-blue
24
+ system: chalk.hex('#808896'), // neutral gray
25
+ dim: chalk.hex('#5b616e'), // dark gray
26
+ warn: chalk.hex('#c9a86a'), // calm ochre
27
+ error: chalk.hex('#c98a80'), // muted terracotta
28
+ success: chalk.hex('#a9c08c'), // soft sage
29
+ brown: chalk.hex('#a1723f'), // brown (status/spinner)
30
30
  bold: chalk.bold,
31
31
  };
32
32
  export default theme;
package/dist/tools.js CHANGED
@@ -7,16 +7,16 @@ export function createTools(workdir, { undo } = {}) {
7
7
  const root = path.resolve(workdir);
8
8
  const safe = (p) => {
9
9
  const resolved = path.resolve(root, p);
10
- // startsWith(root) пропускал бы соседние пути с общим префиксом
11
- // (C:\work\proj vs C:\work\proj-old). Считаем через relative().
10
+ // startsWith(root) would let through sibling paths with a common prefix
11
+ // (C:\work\proj vs C:\work\proj-old). We compute via relative().
12
12
  const rel = path.relative(root, resolved);
13
13
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
14
14
  throw new Error(`Доступ за пределы рабочей директории: ${p}`);
15
15
  }
16
16
  return resolved;
17
17
  };
18
- // Sandbox (вариант A): не даём команде выйти выше root.
19
- // Это защитный барьер, а не полноценная изоляция ОС.
18
+ // Sandbox (option A): don't let the command go above root.
19
+ // This is a protective barrier, not full OS isolation.
20
20
  const assertCommandInsideRoot = (command) => {
21
21
  const cmd = String(command || '');
22
22
  const cdRe = /(?:^|[;&|]|\s)(?:cd|pushd)\s+([^;&|]+)/gi;
@@ -74,10 +74,10 @@ export function createTools(workdir, { undo } = {}) {
74
74
  resolve(parts.join('\n'));
75
75
  });
76
76
  });
77
- // Достаёт текст из content/content_base64. base64 нужен, потому что канал
78
- // передачи ответа модели может искажать символы ($, обратные слэши,
79
- // переводы строк). base64 состоит только из [A-Za-z0-9+/=] и искажению
80
- // не подвержен.
77
+ // Extracts text from content/content_base64. base64 is needed because the
78
+ // channel that delivers the model's answer may corrupt characters ($,
79
+ // backslashes, newlines). base64 consists only of [A-Za-z0-9+/=] and is not
80
+ // subject to corruption.
81
81
  const decodeContent = (content, contentBase64) => {
82
82
  if (typeof contentBase64 === 'string' && contentBase64.length) {
83
83
  return Buffer.from(contentBase64, 'base64').toString('utf-8');
@@ -172,7 +172,7 @@ export function createTools(workdir, { undo } = {}) {
172
172
  const { glob } = await import('fs/promises');
173
173
  const results = [];
174
174
  for await (const f of glob(String(pattern), { cwd: workdir })) {
175
- // Sandbox: игнорируем всё, что выходит за пределы root.
175
+ // Sandbox: ignore anything that goes outside root.
176
176
  const abs = path.resolve(workdir, f);
177
177
  const rel = path.relative(root, abs);
178
178
  if (rel.startsWith('..') || path.isAbsolute(rel))
@@ -17,8 +17,8 @@ export class Transcript {
17
17
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
18
18
  this.file = path.join(dir, `${sessionName}-${stamp}.jsonl`);
19
19
  this.stream = fs.createWriteStream(this.file, { flags: 'a' });
20
- // Ошибки записи (диск переполнен, файл удалён и т.п.) приходят
21
- // событием 'error'; без слушателя это uncaught exception.
20
+ // Write errors (disk full, file deleted, etc.) arrive as an 'error'
21
+ // event; without a listener this is an uncaught exception.
22
22
  this.stream.on('error', (e) => {
23
23
  console.error(`transcript: ошибка записи: ${e.message}`);
24
24
  this.enabled = false;
package/dist/types.js CHANGED
@@ -1,3 +1,3 @@
1
- // Общие типы-контракты для всего агента.
2
- // Здесь только типы — ни рантайм-кода, ни побочных эффектов.
1
+ // Shared contract types for the whole agent.
2
+ // Types only here — no runtime code, no side effects.
3
3
  export {};
package/dist/web.js CHANGED
@@ -1,27 +1,27 @@
1
1
  import { chromium } from 'playwright';
2
2
  const DEFAULT_TIMEOUT = 20_000;
3
3
  const MAX_TEXT = 12_000;
4
- // Убирает скрипты, стили, и превращает HTML в читабельный текст.
4
+ // Removes scripts, styles, and turns HTML into readable text.
5
5
  function htmlToText(html) {
6
- // Удаляем блоки, которые не нужны
6
+ // Remove blocks we don't need
7
7
  let s = html
8
8
  .replace(/<script[\s\S]*?<\/script>/gi, '')
9
9
  .replace(/<style[\s\S]*?<\/style>/gi, '')
10
10
  .replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
11
11
  .replace(/<svg[\s\S]*?<\/svg>/gi, '')
12
12
  .replace(/<!--[\s\S]*?-->/g, '');
13
- // Ссылки: <a href="URL">text</a> → text (URL)
13
+ // Links: <a href="URL">text</a> → text (URL)
14
14
  s = s.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
15
15
  const t = text.replace(/<[^>]+>/g, '').trim();
16
16
  return t ? `${t} (${href})` : href;
17
17
  });
18
- // Заголовки, параграфы, BR → переводы строк
18
+ // Headings, paragraphs, BR → newlines
19
19
  s = s
20
20
  .replace(/<\/(h[1-6]|p|div|li|tr|section|article)>/gi, '\n')
21
21
  .replace(/<br\s*\/?>/gi, '\n')
22
22
  .replace(/<li[^>]*>/gi, '- ')
23
23
  .replace(/<[^>]+>/g, '');
24
- // HTML entities — базовые
24
+ // HTML entities — the basic ones
25
25
  s = s
26
26
  .replace(/&nbsp;/g, ' ')
27
27
  .replace(/&amp;/g, '&')
@@ -34,7 +34,7 @@ function htmlToText(html) {
34
34
  .replace(/&hellip;/g, '…')
35
35
  .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
36
36
  .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)));
37
- // Сжимаем пустые строки
37
+ // Collapse blank lines
38
38
  s = s
39
39
  .split('\n')
40
40
  .map((l) => l.replace(/[ \t]+/g, ' ').trim())
@@ -42,7 +42,7 @@ function htmlToText(html) {
42
42
  .join('\n');
43
43
  return s;
44
44
  }
45
- // fetch с редиректами и таймаутом
45
+ // fetch with redirects and a timeout
46
46
  async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {} } = {}) {
47
47
  const ctrl = new AbortController();
48
48
  const t = setTimeout(() => ctrl.abort(), timeout);
@@ -64,8 +64,8 @@ async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {} } = {})
64
64
  clearTimeout(t);
65
65
  }
66
66
  }
67
- // Отдельный headless-браузер для JS-страниц.
68
- // Ленивая инициализация, чтобы не тратить ресурсы впустую.
67
+ // A separate headless browser for JS pages.
68
+ // Lazy initialization so we don't waste resources.
69
69
  let _headless = null;
70
70
  async function getHeadless() {
71
71
  if (_headless)
@@ -96,7 +96,7 @@ async function renderWithHeadless(url, { timeout = DEFAULT_TIMEOUT } = {}) {
96
96
  waitUntil: 'domcontentloaded',
97
97
  timeout,
98
98
  });
99
- // Дать JS немного времени дорисовать
99
+ // Give JS a little time to finish rendering
100
100
  await page.waitForTimeout(1500);
101
101
  const html = await page.content();
102
102
  const status = resp ? resp.status() : 0;
@@ -107,7 +107,7 @@ async function renderWithHeadless(url, { timeout = DEFAULT_TIMEOUT } = {}) {
107
107
  await ctx.close();
108
108
  }
109
109
  }
110
- // ---------- инструменты ----------
110
+ // ---------- tools ----------
111
111
  export function createWebTools() {
112
112
  return [
113
113
  {
@@ -132,14 +132,14 @@ export function createWebTools() {
132
132
  return formatResult(r.status, r.url, text, limit);
133
133
  }
134
134
  const r = await httpFetch(String(url));
135
- // Если это JSON/plain text — вернуть как есть
135
+ // If it's JSON/plain text — return as is
136
136
  if (/application\/json|text\/plain|text\/markdown/i.test(r.contentType)) {
137
137
  const trimmed = r.body.slice(0, limit);
138
138
  return `HTTP ${r.status} ${r.url}\nContent-Type: ${r.contentType}\n\n${trimmed}`;
139
139
  }
140
- // HTML — почистить
140
+ // HTML — clean it up
141
141
  const text = htmlToText(r.body);
142
- // Если текста почти нет — вероятно JS-страница, дать намёк
142
+ // If there's almost no text — probably a JS page, give a hint
143
143
  if (text.length < 200) {
144
144
  return (`HTTP ${r.status} ${r.url}\n` +
145
145
  `Content-Type: ${r.contentType}\n\n` +
@@ -177,13 +177,13 @@ export function createWebTools() {
177
177
  if (r.status !== 200) {
178
178
  return `DuckDuckGo вернул HTTP ${r.status}`;
179
179
  }
180
- // Парсим простыми регексами. Формат html.duckduckgo.com стабильный.
180
+ // We parse with simple regexes. The html.duckduckgo.com format is stable.
181
181
  const results = [];
182
182
  const re = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>)?/gi;
183
183
  let m;
184
184
  while ((m = re.exec(r.body)) && results.length < limit) {
185
185
  let href = m[1];
186
- // DuckDuckGo заворачивает ссылки в редирект вида /l/?uddg=...
186
+ // DuckDuckGo wraps links in a redirect like /l/?uddg=...
187
187
  const uddgMatch = href.match(/[?&]uddg=([^&]+)/);
188
188
  if (uddgMatch)
189
189
  href = decodeURIComponent(uddgMatch[1]);