zames_pro 2.19.0 → 2.21.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
@@ -8,7 +8,7 @@ import { DeepSeekBrowser } from './browser.js';
8
8
  import { createTools } from './tools.js';
9
9
  import { runAgentLoop } from './agent-loop.js';
10
10
  import { createSpinner } from './spinner.js';
11
- import { LineEditor, expandPastes, pasteReplacement } from './input.js';
11
+ import { LineEditor, expandPastes, pasteReplacement, } from './input.js';
12
12
  import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
13
13
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
14
14
  import { runConfigMenu } from './config-menu.js';
@@ -370,7 +370,9 @@ async function expandSlashTarget(workdir, name, rest) {
370
370
  body = '';
371
371
  }
372
372
  const header = 'Follow the skill "' + skill.name + '" (from ' + skill.path + ').';
373
- const extra = rest ? '\n\nAdditional instructions from the operator: ' + rest : '';
373
+ const extra = rest
374
+ ? '\n\nAdditional instructions from the operator: ' + rest
375
+ : '';
374
376
  return header + '\n\n' + body + extra;
375
377
  }
376
378
  }
@@ -403,7 +405,8 @@ async function resolveAttachPath(workdir, raw) {
403
405
  let s = String(raw || '').trim();
404
406
  if (!s)
405
407
  return null;
406
- if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
408
+ if ((s.startsWith('"') && s.endsWith('"')) ||
409
+ (s.startsWith("'") && s.endsWith("'"))) {
407
410
  s = s.slice(1, -1);
408
411
  }
409
412
  s = s.replace(/\\ /g, ' ');
@@ -1016,7 +1019,10 @@ async function main() {
1016
1019
  mcpPool = await mod.createMcpPool({ workdir: currentWorkdir });
1017
1020
  const st = mcpPool.status();
1018
1021
  if (st.toolCount > 0) {
1019
- const names = st.servers.filter((x) => !x.error).map((x) => x.name).join(', ');
1022
+ const names = st.servers
1023
+ .filter((x) => !x.error)
1024
+ .map((x) => x.name)
1025
+ .join(', ');
1020
1026
  console.log(theme.system(t('mcp.loaded', { n: String(st.toolCount), servers: names })));
1021
1027
  }
1022
1028
  for (const srv of st.servers) {
@@ -1117,8 +1123,6 @@ async function main() {
1117
1123
  // Messages the user typed while the agent worked. runTask takes them one
1118
1124
  // by one after the current task finishes.
1119
1125
  const pendingQueue = [];
1120
- // Attachments for the next submit (filled by the editor's onAttachments).
1121
- let pendingAttachments = [];
1122
1126
  // Show the "no clipboard image" hint only once per session.
1123
1127
  let clipboardWarned = false;
1124
1128
  // ---------- review mode state ----------
@@ -1185,7 +1189,9 @@ async function main() {
1185
1189
  let tail;
1186
1190
  if (reviewMode) {
1187
1191
  tail =
1188
- theme.warn(t('prompt.review')) + theme.dim(':') + theme.dir(reviewMode.snapName);
1192
+ theme.warn(t('prompt.review')) +
1193
+ theme.dim(':') +
1194
+ theme.dir(reviewMode.snapName);
1189
1195
  }
1190
1196
  else {
1191
1197
  tail = theme.dir(dirLabel(currentWorkdir));
@@ -1217,7 +1223,10 @@ async function main() {
1217
1223
  mime: image.mime,
1218
1224
  size: image.data.length,
1219
1225
  });
1220
- ed.printAbove(theme.system(t('msg.attached_image', { marker: att.marker, size: formatSize(image.data.length) })));
1226
+ ed.printAbove(theme.system(t('msg.attached_image', {
1227
+ marker: att.marker,
1228
+ size: formatSize(image.data.length),
1229
+ })));
1221
1230
  return att;
1222
1231
  }
1223
1232
  // Case 2: the paste is a path to a local file (drag&drop or copy path).
@@ -1235,7 +1244,11 @@ async function main() {
1235
1244
  mime,
1236
1245
  size: data.length,
1237
1246
  });
1238
- ed.printAbove(theme.system(t('msg.attached_file', { marker: att.marker, name, size: formatSize(data.length) })));
1247
+ ed.printAbove(theme.system(t('msg.attached_file', {
1248
+ marker: att.marker,
1249
+ name,
1250
+ size: formatSize(data.length),
1251
+ })));
1239
1252
  return att;
1240
1253
  };
1241
1254
  ed.onClipboard = async () => {
@@ -1932,10 +1945,16 @@ async function main() {
1932
1945
  console.log(theme.system(t('mcp.title', { n: String(st.toolCount) })));
1933
1946
  for (const srv of st.servers) {
1934
1947
  if (srv.error) {
1935
- console.log(' ' + theme.user(srv.name) + ' ' + theme.dim(t('mcp.status_error', { v: srv.error })));
1948
+ console.log(' ' +
1949
+ theme.user(srv.name) +
1950
+ ' ' +
1951
+ theme.dim(t('mcp.status_error', { v: srv.error })));
1936
1952
  }
1937
1953
  else {
1938
- console.log(' ' + theme.user(srv.name) + ' ' + theme.dim('(' + String(srv.tools.length) + ')'));
1954
+ console.log(' ' +
1955
+ theme.user(srv.name) +
1956
+ ' ' +
1957
+ theme.dim('(' + String(srv.tools.length) + ')'));
1939
1958
  }
1940
1959
  }
1941
1960
  continue;
@@ -2132,7 +2151,7 @@ async function main() {
2132
2151
  continue;
2133
2152
  }
2134
2153
  if (lower === '/diff' || lower.startsWith('/diff ')) {
2135
- const staged = lower.indexOf("--staged") !== -1;
2154
+ const staged = lower.indexOf('--staged') !== -1;
2136
2155
  const { runGit } = await import('./gitTools.js');
2137
2156
  const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
2138
2157
  if (probe.trim() !== 'true') {
@@ -2196,14 +2215,14 @@ async function main() {
2196
2215
  }
2197
2216
  }
2198
2217
  catch {
2199
- gitOk = false;
2218
+ // git is not available here; gitOk stays false.
2200
2219
  }
2201
2220
  let clipboardTool = null;
2202
2221
  try {
2203
2222
  clipboardTool = hasClipboardTool() ? 'available' : null;
2204
2223
  }
2205
2224
  catch {
2206
- clipboardTool = null;
2225
+ // not available; clipboardTool stays null
2207
2226
  }
2208
2227
  const mcpStatus = mcpPool
2209
2228
  ? mcpPool.status()
@@ -2283,7 +2302,9 @@ async function main() {
2283
2302
  // A model "answer" that is actually an error/abort sentinel is not a
2284
2303
  // summary - do not carry it over.
2285
2304
  if (!summary || /^\(прервано пользователем\)$/.test(summary)) {
2286
- console.error(theme.error(t('compact.summary_failed', { v: summary || t('common.unknown') })));
2305
+ console.error(theme.error(t('compact.summary_failed', {
2306
+ v: summary || t('common.unknown'),
2307
+ })));
2287
2308
  continue;
2288
2309
  }
2289
2310
  transcript.log('compact_summary', {
@@ -2335,7 +2356,7 @@ async function main() {
2335
2356
  }
2336
2357
  if (lower === '/review' || lower.startsWith('/review ')) {
2337
2358
  const rest = trimmed.slice('/review'.length).trim();
2338
- const staged = rest.indexOf("--staged") !== -1;
2359
+ const staged = rest.indexOf('--staged') !== -1;
2339
2360
  const focus = rest.replace(/--staged/g, '').trim();
2340
2361
  const reviewTask = buildReviewPrompt(focus, staged);
2341
2362
  const reviewTools = mod.createTools(currentWorkdir, { undo });
package/dist/input.js CHANGED
@@ -36,7 +36,11 @@ const PASTE_END = ESC + '[201~';
36
36
  export const PASTE_MIN_LINES = 3;
37
37
  // Number of lines in a pasted block (the trailing newline doesn't add a line).
38
38
  export function countPasteLines(raw) {
39
- const normalized = String(raw).split(CR + NL).join(NL).split(CR).join(NL);
39
+ const normalized = String(raw)
40
+ .split(CR + NL)
41
+ .join(NL)
42
+ .split(CR)
43
+ .join(NL);
40
44
  const trimmed = normalized.replace(/\n+$/, '');
41
45
  if (trimmed === '')
42
46
  return 1;
@@ -49,7 +53,11 @@ export function formatPasteMarker(lines) {
49
53
  // otherwise { marker, text }: the marker goes into the input line, and the
50
54
  // original text is expanded back on submit.
51
55
  export function pasteReplacement(raw) {
52
- const text = String(raw).split(CR + NL).join(NL).split(CR).join(NL);
56
+ const text = String(raw)
57
+ .split(CR + NL)
58
+ .join(NL)
59
+ .split(CR)
60
+ .join(NL);
53
61
  const lines = countPasteLines(text);
54
62
  if (lines < PASTE_MIN_LINES)
55
63
  return null;
@@ -174,6 +182,24 @@ export function layoutInput(promptStr, buf, cursor, cols) {
174
182
  // percentage of CONTEXT_LIMIT. Exported so it is unit-tested without a live
175
183
  // editor. A null/undefined/NaN count renders an empty string (no status).
176
184
  export const CONTEXT_LIMIT = 1_000_000;
185
+ // Thresholds for coloring the context counter. Below YELLOW the value is
186
+ // green (plenty of room), between YELLOW and RED it is yellow (getting full),
187
+ // above RED it is red (nearly exhausted). Percent of CONTEXT_LIMIT.
188
+ export const CONTEXT_YELLOW_PCT = 50;
189
+ export const CONTEXT_RED_PCT = 80;
190
+ // The color role for a context fill level, as a stable string so it can be
191
+ // unit-tested without a terminal: 'ok' | 'warn' | 'high'. Null tokens -> null.
192
+ export function tokenStatusLevel(tokens, limit = CONTEXT_LIMIT) {
193
+ if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
194
+ return null;
195
+ }
196
+ const pct = (tokens / limit) * 100;
197
+ if (pct >= CONTEXT_RED_PCT)
198
+ return 'high';
199
+ if (pct >= CONTEXT_YELLOW_PCT)
200
+ return 'warn';
201
+ return 'ok';
202
+ }
177
203
  export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
178
204
  if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
179
205
  return '';
@@ -186,7 +212,9 @@ export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
186
212
  }
187
213
  else if (n >= 1_000) {
188
214
  const k = n / 1_000;
189
- compact = (k >= 100 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '')) + 'k';
215
+ compact =
216
+ (k >= 100 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '')) +
217
+ 'k';
190
218
  }
191
219
  else {
192
220
  compact = String(n);
@@ -214,6 +242,13 @@ export class LineEditor {
214
242
  _animating;
215
243
  _dotPhase;
216
244
  _thinkBase;
245
+ // The last rendered status row(s) (status + right-aligned context). Cached so
246
+ // a re-render driven by TYPING (the buffer changed, the status did not) can
247
+ // skip erasing and re-printing the status line entirely. On terminals like
248
+ // Tabby a full erase+rewrite on every keystroke makes the whole screen blink;
249
+ // keeping the status untouched removes that flicker. Cleared whenever the
250
+ // status genuinely changes so the cache can never go stale.
251
+ _lastStatusBlock;
217
252
  _wasRaw;
218
253
  _onData;
219
254
  // History of sent messages (for the up/down arrows).
@@ -247,10 +282,12 @@ export class LineEditor {
247
282
  // percentage of the context limit, right-aligned above the input line. Null
248
283
  // hides it. Updated by the caller from browser.getLastTokenUsage().
249
284
  contextStatus;
285
+ // The raw token count behind contextStatus (for the color level).
286
+ contextTokens;
250
287
  // A callback the editor calls to fetch the CURRENT token count before every
251
288
  // status render, so the status line stays fresh without the caller polling.
252
289
  onContextQuery;
253
- constructor({ prompt = '> ', commands = [], locale = 'ru' } = {}) {
290
+ constructor({ prompt = '> ', commands = [], locale = 'ru', } = {}) {
254
291
  this.locale = locale;
255
292
  this.promptStr = prompt;
256
293
  this.buf = '';
@@ -268,6 +305,7 @@ export class LineEditor {
268
305
  this._animating = false;
269
306
  this._dotPhase = 0;
270
307
  this._thinkBase = '';
308
+ this._lastStatusBlock = '';
271
309
  this._wasRaw = false;
272
310
  this._onData = (b) => this._handle(b);
273
311
  this.history = [];
@@ -283,6 +321,7 @@ export class LineEditor {
283
321
  this.onClipboard = null;
284
322
  this.locked = false;
285
323
  this.contextStatus = null;
324
+ this.contextTokens = null;
286
325
  this.onContextQuery = null;
287
326
  }
288
327
  // The token status for the CURRENT render: refreshed from onContextQuery
@@ -291,7 +330,9 @@ export class LineEditor {
291
330
  if (this.onContextQuery) {
292
331
  try {
293
332
  const n = this.onContextQuery();
294
- this.contextStatus = n === null || n === undefined ? null : formatTokenStatus(n) || null;
333
+ this.contextTokens = typeof n === 'number' ? n : null;
334
+ this.contextStatus =
335
+ n === null || n === undefined ? null : formatTokenStatus(n) || null;
295
336
  }
296
337
  catch {
297
338
  // A broken callback must never break the render.
@@ -299,6 +340,19 @@ export class LineEditor {
299
340
  }
300
341
  return this.contextStatus;
301
342
  }
343
+ // The context text colored by fill level: green (ok), yellow (warn),
344
+ // red (high).
345
+ _contextText() {
346
+ const ctx = this._contextForRender();
347
+ if (!ctx)
348
+ return '';
349
+ const level = tokenStatusLevel(this.contextTokens);
350
+ if (level === 'high')
351
+ return theme.error(ctx);
352
+ if (level === 'warn')
353
+ return theme.warn(ctx);
354
+ return theme.success(ctx);
355
+ }
302
356
  // Read the OS clipboard for an image and insert its marker. Used when the
303
357
  // terminal sends no usable paste data (Ctrl+V / right-click / empty paste).
304
358
  async _tryClipboard() {
@@ -420,6 +474,8 @@ export class LineEditor {
420
474
  }
421
475
  // Update the token-context text shown at the right of the status line.
422
476
  setContextStatus(tokens) {
477
+ this.contextTokens =
478
+ typeof tokens === 'number' && Number.isFinite(tokens) ? tokens : null;
423
479
  this.contextStatus = formatTokenStatus(tokens) || null;
424
480
  this._render();
425
481
  }
@@ -450,35 +506,123 @@ export class LineEditor {
450
506
  }
451
507
  process.stdout.write(CR + ESC + '[J');
452
508
  this.rendered = false;
509
+ this._lastStatusBlock = '';
510
+ }
511
+ // Erase ONLY the input rows (leave the status block untouched). Used when a
512
+ // keystroke re-renders the line but the status/context did not change: the
513
+ // status stays on screen, only the input area is redrawn. This removes the
514
+ // full-screen flicker on terminals like Tabby, where erasing the whole block
515
+ // on every character made the display blink.
516
+ _eraseInputOnly(inputRows) {
517
+ if (!this.rendered)
518
+ return;
519
+ const rows = process.stdout.rows || 24;
520
+ const up = Math.min(Math.max(0, inputRows - 1), Math.max(0, rows - 1));
521
+ if (up > 0)
522
+ process.stdout.write(ESC + '[' + up + 'A');
523
+ process.stdout.write(CR + ESC + '[J');
524
+ this.rendered = false;
453
525
  }
454
526
  _writeBlock() {
455
527
  const cols = process.stdout.columns || 80;
456
- let out = '';
457
- let top = 0;
458
528
  // Status line above the input: the spinner/answer text on the left and the
459
529
  // token context right-aligned on the SAME row (10k · 12%). The context is
460
530
  // refreshed from onContextQuery() on every render, so it follows the live
461
531
  // DeepSeek counter without a polling timer of its own.
462
- const ctx = this._contextForRender();
463
- const ctxText = ctx ? theme.dim(ctx) : '';
532
+ //
533
+ // `top` MUST count EXACTLY the rows we print, or the next _eraseBlock()
534
+ // moves the cursor to the wrong row and the block (spinner + input) is
535
+ // re-drawn on top of the previous one instead of replacing it — the
536
+ // operator sees the status line duplicated/stacking, most visibly during
537
+ // the send-pause countdown (which re-renders about once per second).
538
+ // The earlier code computed `top = visRows(status + ' ' + ctxText)` even
539
+ // though it PRINTS `status + ' '.repeat(space) + ctxText` with a DIFFERENT
540
+ // total width, so at certain widths `top` disagreed with the printed rows.
541
+ // Both branches below now derive `top` from the exact string they print.
542
+ const ctxText = this._contextText();
543
+ let statusOut = '';
544
+ let top = 0;
464
545
  if (this.statusText) {
465
546
  if (ctxText) {
466
- const pad = Math.max(1, cols - visLen(this.statusText) - visLen(ctxText));
467
- out += this.statusText + ' '.repeat(pad) + ctxText + NL;
547
+ // Right-align the context on the SAME row as the status. When the
548
+ // status is too long to leave room, do NOT cram them together (that
549
+ // pushed the trailing `%` past the right edge and it got truncated):
550
+ // put the context on its own line instead.
551
+ const space = cols - visLen(this.statusText) - visLen(ctxText);
552
+ if (space >= 2) {
553
+ const row = this.statusText + ' '.repeat(space) + ctxText;
554
+ statusOut = row + NL;
555
+ top = visRows(row, cols);
556
+ }
557
+ else {
558
+ statusOut = this.statusText + NL;
559
+ top = visRows(this.statusText, cols);
560
+ const ctxRow = ' '.repeat(Math.max(0, cols - visLen(ctxText))) + ctxText;
561
+ statusOut += ctxRow + NL;
562
+ top += visRows(ctxRow, cols);
563
+ }
468
564
  }
469
565
  else {
470
- out += this.statusText + NL;
566
+ statusOut = this.statusText + NL;
567
+ // The status may wrap onto several lines — we account for this,
568
+ // otherwise the block erase misses and statuses pile up.
569
+ top = visRows(this.statusText, cols);
471
570
  }
472
- // The status may wrap onto several lines — we account for this,
473
- // otherwise the block erase misses and statuses pile up.
474
- top = visRows(this.statusText + (ctxText ? ' '.repeat(2) + ctxText : ''), cols);
475
571
  }
476
572
  else if (ctxText) {
477
573
  // Idle: no spinner, but the context still belongs on its own line just
478
574
  // above the input, right-aligned.
479
- out += ' '.repeat(Math.max(0, cols - visLen(ctxText))) + ctxText + NL;
480
- top = visRows(ctxText, cols);
575
+ const ctxRow = ' '.repeat(Math.max(0, cols - visLen(ctxText))) + ctxText;
576
+ statusOut = ctxRow + NL;
577
+ top = visRows(ctxRow, cols);
578
+ }
579
+ // Incremental re-render: when the status block is byte-identical to what is
580
+ // already on screen (the common case while typing — the buffer changes, the
581
+ // spinner/context does not) we redraw ONLY the input rows. Erasing and
582
+ // re-printing the whole block on every keystroke is what made the display
583
+ // flicker on terminals like Tabby. `top` is recovered from the cache so the
584
+ // cursor math stays correct.
585
+ const statusBlock = statusOut;
586
+ const statusUnchanged = this.rendered && statusBlock === this._lastStatusBlock;
587
+ if (statusUnchanged) {
588
+ // Count the real input rows via layout (wrapping aware).
589
+ const layOnly = layoutInput(this.promptStr, this.buf, this.cursor, cols);
590
+ const inputLineRows = layOnly.rows.length;
591
+ const suggOnly = this._suggestions();
592
+ const shownOnly = suggOnly.slice(0, 8);
593
+ const linesBelowOnly = shownOnly.length
594
+ ? shownOnly.length + (suggOnly.length > shownOnly.length ? 1 : 0)
595
+ : 0;
596
+ this._eraseInputOnly(inputLineRows + linesBelowOnly);
597
+ let outOnly = '';
598
+ outOnly += layOnly.rows.map((r) => r.prefix + r.text).join(NL);
599
+ if (shownOnly.length) {
600
+ const maxName = Math.max(...shownOnly.map((c) => c.name.length));
601
+ const lines = shownOnly.map((c) => {
602
+ const name = theme.prompt(c.name.padEnd(maxName));
603
+ const desc = theme.dim(' ' + c.description);
604
+ return ' ' + name + desc;
605
+ });
606
+ outOnly += NL + lines.join(NL);
607
+ const hidden = suggOnly.length - shownOnly.length;
608
+ if (hidden > 0)
609
+ outOnly +=
610
+ NL +
611
+ theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
612
+ }
613
+ process.stdout.write(outOnly);
614
+ const upOnly = layOnly.rows.length - 1 - layOnly.cursorRow + linesBelowOnly;
615
+ if (upOnly > 0)
616
+ process.stdout.write(ESC + '[' + upOnly + 'A');
617
+ process.stdout.write(CR);
618
+ if (layOnly.cursorCol > 0)
619
+ process.stdout.write(ESC + '[' + layOnly.cursorCol + 'C');
620
+ this.rendered = true;
621
+ this.cursorRowFromTop = top + layOnly.cursorRow;
622
+ return;
481
623
  }
624
+ let out = statusOut;
625
+ this._lastStatusBlock = statusBlock;
482
626
  const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
483
627
  out += lay.rows.map((r) => r.prefix + r.text).join(NL);
484
628
  // We draw the slash-command hints BELOW the input line. We then move the
@@ -496,13 +640,17 @@ export class LineEditor {
496
640
  out += NL + lines.join(NL);
497
641
  const hidden = sugg.length - shown.length;
498
642
  if (hidden > 0)
499
- out += NL + theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
643
+ out +=
644
+ NL +
645
+ theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
500
646
  }
501
647
  process.stdout.write(out);
502
648
  const lastRow = lay.rows.length - 1;
503
649
  // Move the cursor up: first to the input line within lay, then further by
504
650
  // the hint lines (if any) — the cursor must sit on the input.
505
- const linesBelow = shown.length ? shown.length + (sugg.length > shown.length ? 1 : 0) : 0;
651
+ const linesBelow = shown.length
652
+ ? shown.length + (sugg.length > shown.length ? 1 : 0)
653
+ : 0;
506
654
  const up = lastRow - lay.cursorRow + linesBelow;
507
655
  if (up > 0)
508
656
  process.stdout.write(ESC + '[' + up + 'A');
@@ -613,7 +761,12 @@ export class LineEditor {
613
761
  assistant(msg) {
614
762
  this.stop();
615
763
  const rendered = renderMarkdown(msg);
616
- this.printAbove(NL + theme.assistant(translate(this.locale)('editor.answer')) + NL + rendered + NL + theme.dim('─'.repeat(60)));
764
+ this.printAbove(NL +
765
+ theme.assistant(translate(this.locale)('editor.answer')) +
766
+ NL +
767
+ rendered +
768
+ NL +
769
+ theme.dim('─'.repeat(60)));
617
770
  }
618
771
  // Warning to the operator (e.g. the agent stopped suspiciously).
619
772
  // Printed above the input line without overwriting it.
@@ -657,7 +810,9 @@ export class LineEditor {
657
810
  _insert(text) {
658
811
  const chars = Array.from(this.buf);
659
812
  const ins = Array.from(String(text));
660
- const next = chars.slice(0, this.cursor).concat(ins, chars.slice(this.cursor));
813
+ const next = chars
814
+ .slice(0, this.cursor)
815
+ .concat(ins, chars.slice(this.cursor));
661
816
  this.buf = next.join('');
662
817
  this.cursor += ins.length;
663
818
  }
@@ -701,7 +856,11 @@ export class LineEditor {
701
856
  _insertFallback(raw) {
702
857
  const rep = pasteReplacement(raw);
703
858
  if (!rep) {
704
- const text = String(raw).split(CR + NL).join(NL).split(CR).join(NL);
859
+ const text = String(raw)
860
+ .split(CR + NL)
861
+ .join(NL)
862
+ .split(CR)
863
+ .join(NL);
705
864
  this._insert(text);
706
865
  return;
707
866
  }
@@ -1136,9 +1295,15 @@ export function selftest() {
1136
1295
  ];
1137
1296
  for (const t of cases) {
1138
1297
  const r = layoutInput(t.p, t.b, t.c, t.w);
1139
- console.log(JSON.stringify({ rows: r.rows.length, row: r.cursorRow, col: r.cursorCol }));
1298
+ console.log(JSON.stringify({
1299
+ rows: r.rows.length,
1300
+ row: r.cursorRow,
1301
+ col: r.cursorCol,
1302
+ }));
1140
1303
  }
1141
1304
  }
1142
- if (process.argv[1] && process.argv[1].endsWith('input.js') && process.argv.includes('--selftest')) {
1305
+ if (process.argv[1] &&
1306
+ process.argv[1].endsWith('input.js') &&
1307
+ process.argv.includes('--selftest')) {
1143
1308
  selftest();
1144
1309
  }
package/dist/mcp.js CHANGED
@@ -15,7 +15,10 @@ export function hardenPlaywrightArgs(command, args) {
15
15
  const hay = (String(command || '') + ' ' + args.join(' ')).toLowerCase();
16
16
  if (!hay.includes('playwright') || !hay.includes('mcp'))
17
17
  return args;
18
- if (args.some((a) => a === '--isolated' || a.startsWith('--user-data-dir') || a === '--extension' || a.startsWith('--cdp-endpoint'))) {
18
+ if (args.some((a) => a === '--isolated' ||
19
+ a.startsWith('--user-data-dir') ||
20
+ a === '--extension' ||
21
+ a.startsWith('--cdp-endpoint'))) {
19
22
  return args;
20
23
  }
21
24
  return [...args, '--isolated'];
@@ -161,7 +164,9 @@ export function renderMcpResult(result) {
161
164
  parts.push(String(b.text ?? ''));
162
165
  }
163
166
  else if (type === 'image') {
164
- parts.push('[image ' + String(b.mimeType ?? 'unknown') + ', written to disk by the MCP server]');
167
+ parts.push('[image ' +
168
+ String(b.mimeType ?? 'unknown') +
169
+ ', written to disk by the MCP server]');
165
170
  }
166
171
  else if (type === 'resource') {
167
172
  const res = (b.resource || {});
@@ -169,7 +174,11 @@ export function renderMcpResult(result) {
169
174
  parts.push(String(res.uri ?? '') + String.fromCharCode(10) + res.text);
170
175
  }
171
176
  else if (typeof res.blob === 'string') {
172
- parts.push('[binary resource ' + String(res.uri ?? '') + ', ' + String(res.mimeType ?? '') + ']');
177
+ parts.push('[binary resource ' +
178
+ String(res.uri ?? '') +
179
+ ', ' +
180
+ String(res.mimeType ?? '') +
181
+ ']');
173
182
  }
174
183
  else {
175
184
  parts.push(String(res.uri ?? ''));
@@ -187,7 +196,10 @@ export function renderMcpResult(result) {
187
196
  }
188
197
  }
189
198
  }
190
- const text = parts.filter((p) => p !== '').join(String.fromCharCode(10) + String.fromCharCode(10)).trim();
199
+ const text = parts
200
+ .filter((p) => p !== '')
201
+ .join(String.fromCharCode(10) + String.fromCharCode(10))
202
+ .trim();
191
203
  return formatResultString(text || '(MCP tool returned no content)', r.isError);
192
204
  }
193
205
  function formatResultString(result, isError) {
@@ -13,10 +13,9 @@ export function extractPathToken(text) {
13
13
  i++;
14
14
  continue;
15
15
  }
16
- let quote = '';
17
16
  let start = i;
18
17
  if (c === String.fromCharCode(39) || c === String.fromCharCode(34)) {
19
- quote = c;
18
+ const quote = c;
20
19
  start = i + 1;
21
20
  i = start;
22
21
  while (i < n && chars[i] !== quote)
@@ -63,7 +62,9 @@ function looksLikeToken(tok) {
63
62
  if (ext.length > 8)
64
63
  return false;
65
64
  for (const ch of ext) {
66
- const ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
65
+ const ok = (ch >= 'a' && ch <= 'z') ||
66
+ (ch >= 'A' && ch <= 'Z') ||
67
+ (ch >= '0' && ch <= '9');
67
68
  if (!ok)
68
69
  return false;
69
70
  }
@@ -54,7 +54,7 @@ async function copyDirJsFiles(from, to) {
54
54
  return copied;
55
55
  }
56
56
  // ---------- /self-review ----------
57
- export async function selfReview({ browser, focus, transcript }) {
57
+ export async function selfReview({ browser, focus, transcript, }) {
58
58
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
59
59
  const snapRoot = path.join(SNAP_ROOT);
60
60
  const snapDir = path.join(snapRoot, `run-${stamp}`);
@@ -125,7 +125,7 @@ export async function selfReview({ browser, focus, transcript }) {
125
125
  return { snapDir, reportPath, changed };
126
126
  }
127
127
  // ---------- /self-diff ----------
128
- export async function selfDiff({ name }) {
128
+ export async function selfDiff({ name, }) {
129
129
  const snapDir = path.join(SNAP_ROOT, name);
130
130
  const snapStat = await fs.stat(snapDir).catch(() => null);
131
131
  if (!snapStat)
@@ -154,7 +154,7 @@ export async function selfDiff({ name }) {
154
154
  }
155
155
  }
156
156
  // ---------- /self-apply ----------
157
- export async function selfApply({ name }) {
157
+ export async function selfApply({ name, }) {
158
158
  const snapDir = path.join(SNAP_ROOT, name);
159
159
  const snapStat = await fs.stat(snapDir).catch(() => null);
160
160
  if (!snapStat)
@@ -180,9 +180,9 @@ export async function selfApply({ name }) {
180
180
  console.log(theme.warn(`\nПерезапусти агента, чтобы изменения вступили в силу.\n`));
181
181
  }
182
182
  // ---------- /self-list ----------
183
- export async function selfList({ config } = {}) {
183
+ export async function selfList({ config: _config, } = {}) {
184
184
  const snapRoot = path.join(SNAP_ROOT);
185
- let entries = [];
185
+ let entries;
186
186
  try {
187
187
  entries = await fs.readdir(snapRoot);
188
188
  }
@@ -230,7 +230,7 @@ async function diffFiles(origDir, newDir) {
230
230
  }
231
231
  return changed;
232
232
  }
233
- function buildReviewPrompt({ focus, snapDir }) {
233
+ function buildReviewPrompt({ focus, snapDir, }) {
234
234
  const focusLine = focus
235
235
  ? `\nThe user asked you to focus especially on: ${focus}\n`
236
236
  : '';
package/dist/spinner.js CHANGED
@@ -108,7 +108,10 @@ export function createSpinner(locale = 'ru') {
108
108
  // so the countdown refreshes without resetting the dots.
109
109
  if (animating && dotTimer && spinner) {
110
110
  animBase = theme.brown(stripEllipsis(label));
111
- spinner.text = animBase + theme.brown(DOTS[dotPhase] + DOTS_PAD.slice(DOTS[dotPhase].length)) + HINT;
111
+ spinner.text =
112
+ animBase +
113
+ theme.brown(DOTS[dotPhase] + DOTS_PAD.slice(DOTS[dotPhase].length)) +
114
+ HINT;
112
115
  return;
113
116
  }
114
117
  startAnimated(label);
@@ -130,7 +133,10 @@ export function createSpinner(locale = 'ru') {
130
133
  toolResult: (result) => {
131
134
  stop();
132
135
  const text = typeof result === 'string' ? result : JSON.stringify(result);
133
- const preview = text.slice(0, 200).split(String.fromCharCode(10)).join(' ↵ ');
136
+ const preview = text
137
+ .slice(0, 200)
138
+ .split(String.fromCharCode(10))
139
+ .join(' ↵ ');
134
140
  console.log(theme.toolResult(' → ' + preview + String.fromCharCode(10)));
135
141
  },
136
142
  assistant: (msg) => {