zames_pro 2.0.0 → 2.1.1

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/dist/index.js CHANGED
@@ -76,19 +76,23 @@ function loadLastChat(workdir = '') {
76
76
  // их через динамический import с timestamp-query — Node кэширует ESM по URL,
77
77
  // поэтому такой import вернёт свежую версию модуля. Браузер, чат и текущее
78
78
  // состояние НЕ перезапускаются: обновляется только логика.
79
+ // В dev-режиме tsx грузит .ts-исходники из src/, а в собранном dist — .js.
80
+ // Динамический import с query ?t= идёт мимо переписывания расширений tsx,
81
+ // поэтому расширение подбираем сами — по фактическому файлу текущего модуля.
82
+ const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
79
83
  const RELOADABLE = [
80
- './tools.js',
81
- './agent-loop.js',
82
- './system-prompt.js',
83
- './gitTools.js',
84
- './web.js',
85
- './self-review.js',
86
- './diff.js',
87
- './undo.js',
88
- './confirm.js',
89
- './transcript.js',
90
- './spinner.js',
91
- './config.js',
84
+ 'tools',
85
+ 'agent-loop',
86
+ 'system-prompt',
87
+ 'gitTools',
88
+ 'web',
89
+ 'self-review',
90
+ 'diff',
91
+ 'undo',
92
+ 'confirm',
93
+ 'transcript',
94
+ 'spinner',
95
+ 'config',
92
96
  ];
93
97
  const mod = {
94
98
  createTools,
@@ -105,36 +109,37 @@ async function reloadModules() {
105
109
  const stamp = Date.now();
106
110
  const loaded = new Map();
107
111
  const errors = [];
108
- for (const rel of RELOADABLE) {
112
+ for (const base of RELOADABLE) {
113
+ const rel = './' + base + SRC_EXT;
109
114
  try {
110
115
  const url = new URL(rel, import.meta.url);
111
116
  url.searchParams.set('t', String(stamp));
112
117
  const m = (await import(url.href));
113
- loaded.set(rel, m);
118
+ loaded.set(base, m);
114
119
  }
115
120
  catch (e) {
116
- errors.push(`${rel}: ${e.message}`);
121
+ errors.push(rel + ': ' + e.message);
117
122
  }
118
123
  }
119
- const pick = (rel, name) => loaded.get(rel)?.[name];
120
- if (pick('./tools.js', 'createTools'))
121
- mod.createTools = pick('./tools.js', 'createTools');
122
- if (pick('./agent-loop.js', 'runAgentLoop'))
123
- mod.runAgentLoop = pick('./agent-loop.js', 'runAgentLoop');
124
- if (pick('./system-prompt.js', 'buildSystemPrompt'))
125
- mod.buildSystemPrompt = pick('./system-prompt.js', 'buildSystemPrompt');
126
- if (pick('./self-review.js', 'selfReview'))
127
- mod.selfReview = pick('./self-review.js', 'selfReview');
128
- if (pick('./self-review.js', 'selfDiff'))
129
- mod.selfDiff = pick('./self-review.js', 'selfDiff');
130
- if (pick('./self-review.js', 'selfApply'))
131
- mod.selfApply = pick('./self-review.js', 'selfApply');
132
- if (pick('./self-review.js', 'selfList'))
133
- mod.selfList = pick('./self-review.js', 'selfList');
134
- if (pick('./web.js', 'closeWeb'))
135
- mod.closeWeb = pick('./web.js', 'closeWeb');
136
- if (pick('./spinner.js', 'createSpinner'))
137
- mod.createSpinner = pick('./spinner.js', 'createSpinner');
124
+ const pick = (base, name) => loaded.get(base)?.[name];
125
+ if (pick('tools', 'createTools'))
126
+ mod.createTools = pick('tools', 'createTools');
127
+ if (pick('agent-loop', 'runAgentLoop'))
128
+ mod.runAgentLoop = pick('agent-loop', 'runAgentLoop');
129
+ if (pick('system-prompt', 'buildSystemPrompt'))
130
+ mod.buildSystemPrompt = pick('system-prompt', 'buildSystemPrompt');
131
+ if (pick('self-review', 'selfReview'))
132
+ mod.selfReview = pick('self-review', 'selfReview');
133
+ if (pick('self-review', 'selfDiff'))
134
+ mod.selfDiff = pick('self-review', 'selfDiff');
135
+ if (pick('self-review', 'selfApply'))
136
+ mod.selfApply = pick('self-review', 'selfApply');
137
+ if (pick('self-review', 'selfList'))
138
+ mod.selfList = pick('self-review', 'selfList');
139
+ if (pick('web', 'closeWeb'))
140
+ mod.closeWeb = pick('web', 'closeWeb');
141
+ if (pick('spinner', 'createSpinner'))
142
+ mod.createSpinner = pick('spinner', 'createSpinner');
138
143
  return { count: loaded.size, errors };
139
144
  }
140
145
  // Первичная загрузка, чтобы mod.buildSystemPrompt и остальные были заполнены.
@@ -340,7 +345,9 @@ async function promptOnce(question) {
340
345
  inPaste = true;
341
346
  if (before) {
342
347
  for (const ch of before) {
343
- if (ch === '\r' || ch === '\n') { /* внутри вставки — пропускаем */ }
348
+ if (ch === '\r' || ch === '\n') {
349
+ /* внутри вставки — пропускаем */
350
+ }
344
351
  else
345
352
  insertText(ch);
346
353
  }
@@ -447,7 +454,7 @@ async function promptOnce(question) {
447
454
  // в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
448
455
  // в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
449
456
  // последовательности (стрелки/Home/End/Delete игнорируются) и bracketed paste.
450
- function watchInput({ onEscape, onChange, onQueue } = {}) {
457
+ function watchInput({ onEscape, onChange, onQueue, } = {}) {
451
458
  const stdin = process.stdin;
452
459
  if (!stdin.isTTY || !stdin.setRawMode)
453
460
  return () => { };
@@ -474,9 +481,12 @@ function watchInput({ onEscape, onChange, onQueue } = {}) {
474
481
  // трактуем как пробелы — сообщение уходит одной строкой.
475
482
  const insert = (text) => {
476
483
  buf += text
477
- .split(CR + LF).join(' ')
478
- .split(CR).join(' ')
479
- .split(LF).join(' ');
484
+ .split(CR + LF)
485
+ .join(' ')
486
+ .split(CR)
487
+ .join(' ')
488
+ .split(LF)
489
+ .join(' ');
480
490
  };
481
491
  function onData(data) {
482
492
  let s = data.toString('utf-8');
@@ -580,7 +590,7 @@ async function resolveWorkdir() {
580
590
  }
581
591
  // ---------- task runner ----------
582
592
  async function runTask(browser, tools, taskText, workdir, opts) {
583
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor } = opts;
593
+ const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
584
594
  // В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
585
595
  // Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
586
596
  // (пайпы) — обычный спиннер + watchInput.
@@ -598,7 +608,8 @@ async function runTask(browser, tools, taskText, workdir, opts) {
598
608
  queue.push(text);
599
609
  ui.setPending(null);
600
610
  ui.stop();
601
- console.log(theme.user('📨 В очередь (' + queue.length + '): ') + theme.assistant(text));
611
+ console.log(theme.user('📨 В очередь (' + queue.length + '): ') +
612
+ theme.assistant(text));
602
613
  ui.thinking();
603
614
  },
604
615
  });
@@ -621,11 +632,12 @@ async function runTask(browser, tools, taskText, workdir, opts) {
621
632
  onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
622
633
  onToolResult: (result) => ui.toolResult(result),
623
634
  onAssistantMessage: (msg) => ui.assistant(msg),
635
+ onChatReady,
624
636
  debugLog: debug,
625
637
  });
626
638
  if (!queue.length)
627
639
  break;
628
- const queued = queue.shift() ?? "";
640
+ const queued = queue.shift() ?? '';
629
641
  ui.stop();
630
642
  if (editor) {
631
643
  editor.printAbove(theme.user('▶ Из очереди: ') + theme.assistant(queued));
@@ -739,6 +751,10 @@ async function main() {
739
751
  transcript,
740
752
  freshChat,
741
753
  sendSystemPrompt,
754
+ onChatReady: (chatId) => {
755
+ if (chatId)
756
+ saveLastChat(chatId, currentWorkdir);
757
+ },
742
758
  });
743
759
  await browser.close();
744
760
  await mod.closeWeb().catch(() => { });
@@ -815,7 +831,8 @@ async function main() {
815
831
  const buildPrompt = () => {
816
832
  let tail;
817
833
  if (reviewMode) {
818
- tail = theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
834
+ tail =
835
+ theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
819
836
  }
820
837
  else {
821
838
  tail = theme.dir(dirLabel(currentWorkdir));
@@ -860,14 +877,16 @@ async function main() {
860
877
  // оба потока в ed.printAbove.
861
878
  const origLog = console.log.bind(console);
862
879
  const origErr = console.error.bind(console);
863
- const fmt = (a) => typeof a === 'string' ? a : (() => {
864
- try {
865
- return JSON.stringify(a);
866
- }
867
- catch {
868
- return String(a);
869
- }
870
- })();
880
+ const fmt = (a) => typeof a === 'string'
881
+ ? a
882
+ : (() => {
883
+ try {
884
+ return JSON.stringify(a);
885
+ }
886
+ catch {
887
+ return String(a);
888
+ }
889
+ })();
871
890
  console.log = (...a) => ed.printAbove(a.map(fmt).join(' '));
872
891
  console.error = (...a) => ed.printAbove(a.map(fmt).join(' '));
873
892
  // Сохраняем на случай отладки.
@@ -886,7 +905,9 @@ async function main() {
886
905
  let tail;
887
906
  if (reviewMode) {
888
907
  tail =
889
- theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
908
+ theme.warn('REVIEW') +
909
+ theme.dim(':') +
910
+ theme.dir(reviewMode.snapName);
890
911
  }
891
912
  else {
892
913
  tail = theme.dir(dirLabel(currentWorkdir));
@@ -897,7 +918,7 @@ async function main() {
897
918
  catch {
898
919
  break;
899
920
  }
900
- const trimmed = (input || "").trim();
921
+ const trimmed = (input || '').trim();
901
922
  if (!trimmed)
902
923
  continue;
903
924
  const lower = trimmed.toLowerCase();
@@ -1138,7 +1159,9 @@ async function main() {
1138
1159
  saveLastChat(pick.id, currentWorkdir, pick.title);
1139
1160
  transcript.log('resume_chat', { id: pick.id, title: pick.title });
1140
1161
  console.log(theme.assistant(`Чат открыт.`) +
1141
- theme.system(resendPrompt ? ' Системный промпт будет переслан на следующей задаче.\n' : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1162
+ theme.system(resendPrompt
1163
+ ? ' Системный промпт будет переслан на следующей задаче.\n'
1164
+ : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1142
1165
  }
1143
1166
  catch (e) {
1144
1167
  console.error(theme.error('Не удалось открыть чат:'), e.message);
@@ -1188,7 +1211,8 @@ async function main() {
1188
1211
  sendSystemPromptNext = resendPrompt;
1189
1212
  saveLastChat(id, currentWorkdir);
1190
1213
  transcript.log('resume_chat', { id });
1191
- console.log(theme.assistant('Чат открыт.') + theme.system(String.fromCharCode(10)));
1214
+ console.log(theme.assistant('Чат открыт.') +
1215
+ theme.system(String.fromCharCode(10)));
1192
1216
  }
1193
1217
  catch (e) {
1194
1218
  console.error(theme.error('Не удалось открыть чат:'), e.message);
@@ -1337,6 +1361,15 @@ async function main() {
1337
1361
  sendSystemPrompt: sendSystemPromptNext,
1338
1362
  queue: pendingQueue,
1339
1363
  ui: editor || null,
1364
+ onChatReady: (chatId) => {
1365
+ // Сохраняем сессию сразу при начале диалога, не дожидаясь конца
1366
+ // задачи. Иначе при долгой/прерванной задаче чат не попадал в
1367
+ // ~/.zames/.sessions и терялся после перезапуска.
1368
+ if (chatId) {
1369
+ currentChatId = chatId;
1370
+ saveLastChat(chatId, currentWorkdir);
1371
+ }
1372
+ },
1340
1373
  });
1341
1374
  }
1342
1375
  finally {
@@ -1362,4 +1395,3 @@ main().catch((e) => {
1362
1395
  console.error(e.stack);
1363
1396
  process.exit(1);
1364
1397
  });
1365
- //# sourceMappingURL=index.js.map
package/dist/input.js CHANGED
@@ -521,4 +521,3 @@ export function selftest() {
521
521
  if (process.argv[1] && process.argv[1].endsWith('input.js') && process.argv.includes('--selftest')) {
522
522
  selftest();
523
523
  }
524
- //# sourceMappingURL=input.js.map
package/dist/markdown.js CHANGED
@@ -45,4 +45,3 @@ export function renderMarkdown(text) {
45
45
  }
46
46
  }
47
47
  export default renderMarkdown;
48
- //# sourceMappingURL=markdown.js.map
@@ -0,0 +1,145 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ // Извлечение СЫРОГО текста ответа модели из сетевых данных DeepSeek.
5
+ //
6
+ // Зачем: чтение ответа идёт из ОТРЕНДЕРЕННОГО DOM, а рендер DeepSeek
7
+ // искажает ответ — превращает доллар-формулы в LaTeX (символ доллара
8
+ // теряется), нормализует переводы строк, делает автолинки. Из-за этого
9
+ // tool-call с шаблонными строками и экранированными переводами строк в
10
+ // аргументах доходил до инструментов искажённым. Здесь мы достаём исходный
11
+ // текст из тела сетевого ответа.
12
+ //
13
+ // Format DeepSeek (SSE, chat.deepseek.com/api/v0/chat/completion):
14
+ // * startovy fragment otveta lezhit v data-chanke s uzlom v.response
15
+ // (v.response.fragments[] -> type RESPONSE -> content);
16
+ // * dalshe tekst dorashchivaetsya chankami vida
17
+ // {"p":"response/fragments/-1/content","o":"APPEND","v":"..."};
18
+ // u samogo pervogo APPEND est p i o, u posleduyushchikh — tolko v;
19
+ // * status generacii prikhodit v {"p":"response/status","o":"SET","v":"..."}
20
+ // i v {"p":"response","o":"BATCH","v":[{...quasi_status...}]}.
21
+ // Reasoning-chanki (thinking) z otvet NE popadayut.
22
+ const NL = String.fromCharCode(10);
23
+ function parseDataLines(body) {
24
+ const out = [];
25
+ for (const line of body.split(NL)) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed.startsWith('data:'))
28
+ continue;
29
+ const payload = trimmed.slice(5).trim();
30
+ if (!payload || payload === '[DONE]')
31
+ continue;
32
+ try {
33
+ out.push(JSON.parse(payload));
34
+ }
35
+ catch { }
36
+ }
37
+ return out;
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.
44
+ export function extractFromSse(body) {
45
+ let out = '';
46
+ for (const obj of parseDataLines(body)) {
47
+ if (obj == null || typeof obj !== 'object')
48
+ continue;
49
+ const o = obj;
50
+ // OpenAI-sovmestimyj format: choices[].delta.content.
51
+ if (Array.isArray(o.choices)) {
52
+ out += pickAnswerText(o);
53
+ continue;
54
+ }
55
+ // DeepSeek: startovy snapshot otveta v.response.fragments[].
56
+ const v = o.v;
57
+ const resp = v && typeof v === 'object'
58
+ ? v.response
59
+ : undefined;
60
+ if (resp && Array.isArray(resp.fragments)) {
61
+ for (const fr of resp.fragments) {
62
+ if (fr && fr.type === 'RESPONSE' && typeof fr.content === 'string') {
63
+ out += fr.content;
64
+ }
65
+ }
66
+ continue;
67
+ }
68
+ // Инкрементальный APPEND: p='response/fragments/-1/content' -> v=строка;
69
+ // последующие чанки идут с одним полем v.
70
+ if (o.p === 'response/fragments/-1/content' && typeof o.v === 'string') {
71
+ out += o.v;
72
+ continue;
73
+ }
74
+ if (o.p === undefined && typeof o.v === 'string') {
75
+ out += o.v;
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ export function extractFromJson(body) {
81
+ let obj;
82
+ try {
83
+ obj = JSON.parse(body);
84
+ }
85
+ catch {
86
+ return '';
87
+ }
88
+ return pickAnswerText(obj);
89
+ }
90
+ // Достает финальный текст ответа из узла, НЕ смешивая его с reasoning.
91
+ // Порядок: choices/messages -> delta/message -> content; reasoning_content
92
+ // намеренно не берем — это размышления модели, а не ответ.
93
+ function pickAnswerText(node) {
94
+ if (node == null)
95
+ return '';
96
+ if (typeof node === 'string')
97
+ return node;
98
+ if (Array.isArray(node)) {
99
+ return node.map(pickAnswerText).join('');
100
+ }
101
+ if (typeof node !== 'object')
102
+ return '';
103
+ const obj = node;
104
+ for (const key of ['choices', 'messages']) {
105
+ const v = obj[key];
106
+ if (Array.isArray(v) && v.length) {
107
+ const inner = v.map(pickAnswerText).join('');
108
+ if (inner)
109
+ return inner;
110
+ }
111
+ }
112
+ for (const key of ['content', 'text', 'answer', 'response']) {
113
+ const v = obj[key];
114
+ if (typeof v === 'string' && v)
115
+ return v;
116
+ }
117
+ for (const key of ['delta', 'message', 'data', 'payload', 'v']) {
118
+ const v = obj[key];
119
+ if (v && typeof v === 'object') {
120
+ const inner = pickAnswerText(v);
121
+ if (inner)
122
+ return inner;
123
+ }
124
+ }
125
+ return '';
126
+ }
127
+ // Универсальная попытка: сначала SSE, затем обычный JSON.
128
+ export function extractAnswer(body) {
129
+ const sse = extractFromSse(body);
130
+ if (sse)
131
+ return sse;
132
+ return extractFromJson(body);
133
+ }
134
+ // Сохраняет тело сетевого ответа DeepSeek на диск для разбора постфактум.
135
+ // Файлы лежат в ~/.zames/net-log — po nim видно реальный формат ответа.
136
+ export async function dumpNetBody(url, body) {
137
+ try {
138
+ const dir = path.join(os.homedir(), '.zames', 'net-log');
139
+ await fs.mkdir(dir, { recursive: true });
140
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
141
+ const safe = url.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 80);
142
+ await fs.writeFile(path.join(dir, stamp + '_' + safe + '.txt'), body, 'utf-8');
143
+ }
144
+ catch { }
145
+ }
@@ -278,4 +278,3 @@ When done, respond with a markdown report as the message:
278
278
 
279
279
  Be honest and specific. If the code is fine in some area, say so.`;
280
280
  }
281
- //# sourceMappingURL=self-review.js.map
package/dist/sessions.js CHANGED
@@ -120,4 +120,3 @@ export function listSessions() {
120
120
  export function sessionsDir() {
121
121
  return SESSIONS_DIR;
122
122
  }
123
- //# sourceMappingURL=sessions.js.map
package/dist/spinner.js CHANGED
@@ -135,4 +135,3 @@ export function createSpinner() {
135
135
  fail: () => { },
136
136
  };
137
137
  }
138
- //# sourceMappingURL=spinner.js.map
@@ -78,6 +78,20 @@ Your response MUST be valid JSON. This means:
78
78
 
79
79
  When in doubt — use forward slashes for all file paths.
80
80
 
81
+ ## CRITICAL: preserving special characters in Write/Edit
82
+
83
+ The channel that delivers your answer may corrupt certain characters in tool
84
+ arguments: the dollar sign in template literals, backslashes, newlines, and
85
+ identifiers (e.g. 'x.name'). To write or edit files containing such characters
86
+ reliably, pass the content in base64 instead of plain text:
87
+
88
+ - Write: use 'content_base64' instead of 'content'.
89
+ - Edit: use 'old_base64' / 'new_base64' instead of 'old_string' / 'new_string'.
90
+
91
+ base64 alphabet is [A-Za-z0-9+/=] and is not corrupted. Encode with the
92
+ base64 command or in Node via Buffer.from(text).toString('base64').
93
+ Use plain content/old_string only when the text has no special characters.
94
+
81
95
  ## CRITICAL: quoting inside shell commands
82
96
 
83
97
  When you call Bash, put the whole shell command in a single JSON string.
@@ -121,4 +135,3 @@ Anything you write outside that one respond is not read by the operator.
121
135
  - Do NOT use WebFetch on localhost, private IPs, or 'file://' — it will fail.
122
136
  `;
123
137
  }
124
- //# sourceMappingURL=system-prompt.js.map
package/dist/theme.js CHANGED
@@ -30,4 +30,3 @@ export const theme = {
30
30
  bold: chalk.bold,
31
31
  };
32
32
  export default theme;
33
- //# sourceMappingURL=theme.js.map
package/dist/tools.js CHANGED
@@ -74,6 +74,16 @@ 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
+ // не подвержен.
81
+ const decodeContent = (content, contentBase64) => {
82
+ if (typeof contentBase64 === 'string' && contentBase64.length) {
83
+ return Buffer.from(contentBase64, 'base64').toString('utf-8');
84
+ }
85
+ return String(content ?? '');
86
+ };
77
87
  const baseTools = [
78
88
  {
79
89
  name: 'Read',
@@ -90,41 +100,54 @@ export function createTools(workdir, { undo } = {}) {
90
100
  },
91
101
  {
92
102
  name: 'Write',
93
- description: 'Создать или перезаписать файл.',
94
- parameters: { path: 'string', content: 'string' },
95
- fn: async ({ path: p, content }) => {
103
+ description: 'Создать или перезаписать файл. content — текст; content_base64 — тот же ' +
104
+ 'контент в base64 (используй, если текст содержит , обратные слэши, ' +
105
+ 'переводы строк или другие символы, которые могут исказиться).',
106
+ parameters: {
107
+ path: 'string',
108
+ content: 'string?',
109
+ content_base64: 'string?',
110
+ },
111
+ fn: async ({ path: p, content, content_base64 }) => {
96
112
  const file = safe(String(p));
113
+ const text = decodeContent(content, content_base64);
97
114
  if (undo)
98
115
  await undo.backup(file);
99
116
  await fs.mkdir(path.dirname(file), { recursive: true });
100
- await fs.writeFile(file, String(content), 'utf-8');
101
- return `Файл записан: ${p}`;
117
+ await fs.writeFile(file, text, 'utf-8');
118
+ return `Файл записан: {p}`;
102
119
  },
103
120
  },
104
121
  {
105
122
  name: 'Edit',
106
- description: 'Точечная замена строки в файле. old_string должен встречаться один раз.',
123
+ description: 'Точечная замена строки в файле. old_string должен встречаться один раз. ' +
124
+ 'old_base64/new_base64 — те же строки в base64 (если текст содержит ' +
125
+ 'спецсимволы, которые могут исказиться).',
107
126
  parameters: {
108
127
  path: 'string',
109
- old_string: 'string',
110
- new_string: 'string',
128
+ old_string: 'string?',
129
+ new_string: 'string?',
130
+ old_base64: 'string?',
131
+ new_base64: 'string?',
111
132
  },
112
- fn: async ({ path: p, old_string, new_string }) => {
133
+ fn: async ({ path: p, old_string, new_string, old_base64, new_base64 }) => {
113
134
  const file = safe(String(p));
114
- if (typeof old_string !== 'string' || old_string === '') {
135
+ const oldStr = decodeContent(old_string, old_base64);
136
+ const newStr = decodeContent(new_string, new_base64);
137
+ if (oldStr === '') {
115
138
  throw new Error('old_string пустой — нечего заменять.');
116
139
  }
117
140
  let content = await fs.readFile(file, 'utf-8');
118
- const occurrences = content.split(old_string).length - 1;
141
+ const occurrences = content.split(oldStr).length - 1;
119
142
  if (occurrences === 0) {
120
- throw new Error(`Строка не найдена в ${p}: "${old_string.slice(0, 60)}..."`);
143
+ throw new Error(`Строка не найдена в ${p}: "${oldStr.slice(0, 60)}..."`);
121
144
  }
122
145
  if (occurrences > 1) {
123
146
  throw new Error(`Строка встречается ${occurrences} раз в ${p}. Уточните old_string.`);
124
147
  }
125
148
  if (undo)
126
149
  await undo.backup(file);
127
- content = content.replace(old_string, String(new_string));
150
+ content = content.replace(oldStr, newStr);
128
151
  await fs.writeFile(file, content, 'utf-8');
129
152
  return `Отредактирован: ${p}`;
130
153
  },
@@ -186,4 +209,3 @@ export function createTools(workdir, { undo } = {}) {
186
209
  };
187
210
  return [...baseTools, ...gitTools, ...webTools, respondTool];
188
211
  }
189
- //# sourceMappingURL=tools.js.map
@@ -48,4 +48,3 @@ export class Transcript {
48
48
  this.stream.end();
49
49
  }
50
50
  }
51
- //# sourceMappingURL=transcript.js.map
package/dist/types.js CHANGED
@@ -1,4 +1,3 @@
1
1
  // Общие типы-контракты для всего агента.
2
2
  // Здесь только типы — ни рантайм-кода, ни побочных эффектов.
3
3
  export {};
4
- //# sourceMappingURL=types.js.map
package/dist/undo.js CHANGED
@@ -97,4 +97,3 @@ export class UndoStore {
97
97
  return history.slice(-count).reverse();
98
98
  }
99
99
  }
100
- //# sourceMappingURL=undo.js.map
package/dist/web.js CHANGED
@@ -217,4 +217,3 @@ function formatResult(status, url, text, limit) {
217
217
  return (`HTTP ${status} ${url}\n\n${out}` +
218
218
  (truncated ? `\n\n[...обрезано на ${limit} символах]` : ''));
219
219
  }
220
- //# sourceMappingURL=web.js.map
@@ -65,4 +65,3 @@ export function parseXmlToolCalls(text) {
65
65
  return null;
66
66
  return calls.length === 1 ? calls[0] : calls;
67
67
  }
68
- //# sourceMappingURL=xml-toolcall.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,9 +15,12 @@
15
15
  "scripts": {
16
16
  "start": "node ./dist/index.js",
17
17
  "dev": "tsx ./src/index.ts --dev",
18
- "build": "tsc",
18
+ "build": "tsc -p tsconfig.build.json",
19
19
  "typecheck": "tsc --noEmit",
20
- "prepublishOnly": "npm run build"
20
+ "prepublishOnly": "npm run build",
21
+ "test": "tsx --test test/*.test.ts",
22
+ "test:watch": "tsx --test --watch test/*.test.ts",
23
+ "prepare": "husky"
21
24
  },
22
25
  "keywords": [
23
26
  "agent",
@@ -51,6 +54,7 @@
51
54
  },
52
55
  "devDependencies": {
53
56
  "@types/node": "^26.6.2",
57
+ "husky": "^9.1.7",
54
58
  "tsx": "^4.23.15",
55
59
  "typescript": "^7.0.2"
56
60
  }