zames_pro 2.82.137 → 2.82.139

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +86 -63
  3. package/dist/agent-loop.js +3 -6
  4. package/dist/answer-parse.js +10 -16
  5. package/dist/attach-refs.js +3 -3
  6. package/dist/attachments.js +7 -8
  7. package/dist/backlog.js +2 -5
  8. package/dist/banner.js +4 -5
  9. package/dist/browser-chats.js +4 -4
  10. package/dist/browser-dom.js +16 -15
  11. package/dist/browser-login.js +9 -5
  12. package/dist/browser-pure.js +5 -5
  13. package/dist/browser.js +10 -20
  14. package/dist/changelog.js +14 -20
  15. package/dist/checkpoint.js +6 -10
  16. package/dist/cli-args.js +4 -6
  17. package/dist/code-blocks.js +3 -6
  18. package/dist/commands.js +5 -9
  19. package/dist/config-command.js +6 -4
  20. package/dist/config-commands.js +2 -3
  21. package/dist/config-live.js +7 -13
  22. package/dist/config-menu.js +5 -4
  23. package/dist/config.js +5 -8
  24. package/dist/deepseek-ui.js +7 -7
  25. package/dist/diagnostics.js +5 -7
  26. package/dist/diff.js +8 -10
  27. package/dist/extraTools.js +3 -3
  28. package/dist/fsutil.js +0 -4
  29. package/dist/gitTools.js +5 -4
  30. package/dist/glm-browser.js +2 -10
  31. package/dist/glm-chats.js +0 -1
  32. package/dist/glm-net-capture.js +1 -1
  33. package/dist/glm-ui.js +2 -2
  34. package/dist/goal.js +1 -4
  35. package/dist/i18n/common.js +2 -2
  36. package/dist/i18n.js +1 -2
  37. package/dist/index.js +0 -11
  38. package/dist/input/suggest.js +4 -7
  39. package/dist/input.js +3 -8
  40. package/dist/markdown.js +0 -1
  41. package/dist/mcp.js +2 -5
  42. package/dist/net-capture.js +1 -1
  43. package/dist/path-token.js +7 -14
  44. package/dist/prompt.js +4 -3
  45. package/dist/qwen-browser.js +1 -8
  46. package/dist/qwen-chats.js +0 -1
  47. package/dist/qwen-net-capture.js +4 -7
  48. package/dist/qwen-ui.js +0 -3
  49. package/dist/run-task.js +4 -4
  50. package/dist/self-review.js +0 -5
  51. package/dist/send-runtime.js +6 -10
  52. package/dist/send-telemetry.js +5 -7
  53. package/dist/send-throttle.js +4 -5
  54. package/dist/sessions.js +6 -7
  55. package/dist/spinner.js +3 -4
  56. package/dist/statusline.js +5 -9
  57. package/dist/system-prompt.js +1 -1
  58. package/dist/tui/widgets.js +0 -1
  59. package/llms-full.txt +44 -32
  60. package/llms.txt +1 -1
  61. package/package.json +1 -1
  62. package/scripts/postinstall.mjs +5 -0
@@ -284,13 +284,8 @@ export function hasClipboardTool() {
284
284
  }
285
285
  return false;
286
286
  }
287
- // Read an image from the OS clipboard. Tries, in order, every known tool for
288
- // the current platform. Returns the raw bytes plus a short note about which
289
- // tool was used (or the reason nothing was found) — the caller shows that note
290
- // to the user so a failed paste is never silent.
291
- // True when running inside WSL. There the X11 clipboard is a SEPARATE
292
- // clipboard from the Windows one: the user copies an image in Windows, so we
293
- // must read it through powershell.exe, not xclip.
287
+ // True when the Windows filesystem is visible from here, i.e. a
288
+ // powershell.exe call can reach the Windows clipboard.
294
289
  export function windowsReachable() {
295
290
  if (process.platform === 'win32')
296
291
  return true;
@@ -328,6 +323,11 @@ const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
328
323
  '$d=[System.Windows.Forms.Clipboard]::GetDataObject(); ' +
329
324
  'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
330
325
  '$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
326
+ // Read an image from the OS clipboard, trying every known tool for the current
327
+ // platform. The returned `via` names the tool that worked, or why nothing was
328
+ // found — the caller shows it to the operator so a failed paste is never
329
+ // silent. On WSL the Windows clipboard comes FIRST: it is a DIFFERENT clipboard
330
+ // from X11, and the user copies the image in Windows, not in the X session.
331
331
  export async function readClipboardImageDetailed() {
332
332
  if (process.platform === 'linux' ||
333
333
  process.platform === 'freebsd' ||
@@ -410,7 +410,6 @@ export async function readClipboardImageDetailed() {
410
410
  }
411
411
  return { data: null, via: 'unsupported-platform' };
412
412
  }
413
- // Backwards-compatible wrapper used elsewhere.
414
413
  export async function readClipboardImage() {
415
414
  return (await readClipboardImageDetailed()).data;
416
415
  }
package/dist/backlog.js CHANGED
@@ -39,11 +39,8 @@ export function parseBacklogNote(note) {
39
39
  break;
40
40
  }
41
41
  }
42
- // A note typed on Windows (or pasted from one) carries CRLF line endings;
43
- // the title would keep the trailing `\r` and the heading would get a stray
44
- // carriage return.
45
- // Normalize BOTH line endings first: a lone `\r` (a classic-Mac paste) is
46
- // a line break too, and `body.split(NL)` kept it inside the title.
42
+ // Normalize CRLF AND a lone `\r` (a classic-Mac paste is a line break too):
43
+ // left in place, the `\r` stayed in the title and reached the heading.
47
44
  const bodyLines = body.split(/\r\n?|\n/).map((l) => l.replace(/\r$/, ''));
48
45
  const title = bodyLines[0];
49
46
  const rest = bodyLines.slice(1).join(NL).trim();
package/dist/banner.js CHANGED
@@ -13,7 +13,7 @@ function width(s) {
13
13
  * widest line (clamped to `maxWidth`), padded by one space each side. Lines
14
14
  * longer than the inner width are truncated with an ellipsis.
15
15
  */
16
- function box(lines, maxWidth, border = '#c8b06a') {
16
+ function box(lines, maxWidth) {
17
17
  // A caller may pass 0 ("auto") or a tiny width on a narrow terminal. With
18
18
  // `maxWidth - 4` going negative the inner width collapsed to the 10-column
19
19
  // floor while every content line was clipped to `inner - 2`, so the title,
@@ -25,22 +25,21 @@ function box(lines, maxWidth, border = '#c8b06a') {
25
25
  const top = theme.prompt('╭' + '─'.repeat(inner) + '╮');
26
26
  const bottom = theme.prompt('╰' + '─'.repeat(inner) + '╯');
27
27
  const body = lines.map((l) => {
28
- // Truncate to the inner width so a long path never breaks the frame.
29
28
  const maxContent = Math.max(0, inner - 2);
30
29
  const clipped = clip(l, maxContent);
31
30
  const w = width(clipped);
32
31
  const pad = Math.max(0, inner - 1 - w);
33
32
  return (theme.prompt('│') + ' ' + clipped + ' '.repeat(pad) + theme.prompt('│'));
34
33
  });
35
- void border;
36
34
  return [top, ...body, bottom].join('\n');
37
35
  }
38
36
  /** Truncate an (ANSI-carrying) string to `max` visible columns. */
39
37
  function clip(s, max) {
40
38
  if (width(s) <= max)
41
39
  return s;
42
- // The banner content has at most a couple of ANSI runs; drop the tail by
43
- // visible width while keeping escape sequences intact.
40
+ // The content carries a couple of ANSI runs, so the tail is dropped by
41
+ // VISIBLE width and escape sequences are copied whole — a plain slice would
42
+ // cut an escape in half and leak its remainder as text.
44
43
  let out = '';
45
44
  let visible = 0;
46
45
  let i = 0;
@@ -1,4 +1,4 @@
1
- // PURE parsing of the DeepSeek history/message payloads . These
1
+ // PURE parsing of the DeepSeek history/message payloads. These
2
2
  // helpers are extracted from DeepSeekBrowser.fetchChatMessages: they used to be
3
3
  // copy-pasted INLINE twice (the in-page evaluate AND the Playwright-request
4
4
  // fallback), so the same role/fragment extraction lived in two places and could
@@ -69,9 +69,9 @@ export function parseHistoryMessages(messages) {
69
69
  return { list, usage };
70
70
  }
71
71
  /**
72
- * Pull `chat_messages` out of a history_messages JSON response, tolerating the
73
- * two shapes the endpoint has shipped (`data.biz_data.chat_messages`); returns
74
- * null when the shape does not match, so the caller can report a clear error.
72
+ * Pull `chat_messages` out of a history_messages JSON response; returns null
73
+ * when the shape does not match, so the caller can report a clear error
74
+ * instead of silently restoring an empty dialogue.
75
75
  */
76
76
  export function extractChatMessages(json) {
77
77
  const j = json;
@@ -1,4 +1,4 @@
1
- // Pure in-page DOM helpers extracted from DeepSeekBrowser .
1
+ // Pure in-page DOM helpers extracted from DeepSeekBrowser.
2
2
  //
3
3
  // Every function here runs INSIDE page.evaluate, so it MUST be fully
4
4
  // self-contained: Playwright serializes the function SOURCE and evaluates it in
@@ -79,9 +79,10 @@ export function readMfaAccountInDom() {
79
79
  return '';
80
80
  }
81
81
  /**
82
- * The answer as RENDERED on the page, ALWAYS from the DOM (never the network
83
- * capture). Reasoning blocks (think class) are skipped and must never be
84
- * picked up as the answer.
82
+ * The answer as RENDERED on the page. The freshness checks in `_askOnce` MUST
83
+ * compare rendered text, not the network capture, so they can see that the DOM
84
+ * actually changed. Reasoning blocks (think class) are skipped and must never
85
+ * be picked up as the answer.
85
86
  */
86
87
  export function readLastAnswerInDom(opts) {
87
88
  const sels = opts.sels;
@@ -159,10 +160,9 @@ export function continueVisibleCheapInDom(nameRe) {
159
160
  return false;
160
161
  }
161
162
  /**
162
- * Find the Stop button in the DeepSeek UI. We can't rely on the class alone:
163
- * during generation the send button (the same circle button) changes its icon
164
- * to a "square" (stop) while keeping the classes, so a square icon (svg rect)
165
- * also means Stop.
163
+ * Find the Stop button. The class alone is not enough: during generation the
164
+ * send button (the same circle button) swaps its icon to a "square" while
165
+ * keeping its classes, so a square icon (svg rect) also means Stop.
166
166
  */
167
167
  export function stopButtonVisibleInDom(btnRe) {
168
168
  const re = new RegExp(btnRe, 'i');
@@ -186,9 +186,10 @@ export function stopButtonVisibleInDom(btnRe) {
186
186
  return false;
187
187
  }
188
188
  /**
189
- * Raw-DOM Stop click with the full pointer/mouse sequence (older builds
190
- * without a proper role, or a click intercepted by an overlay). Returns true
191
- * when a square-icon button was found and clicked.
189
+ * Raw-DOM Stop click with the full pointer/mouse sequence. The label check is
190
+ * skipped here on purpose — the square-icon button may carry no label at all,
191
+ * and a plain .click() was swallowed by an overlay. Returns true when a
192
+ * square-icon button was found and clicked.
192
193
  */
193
194
  export function clickStopInDom(btnRe) {
194
195
  const re = new RegExp(btnRe, 'i');
@@ -310,9 +311,8 @@ export function readElementText(el) {
310
311
  return el.innerText || el.textContent || '';
311
312
  }
312
313
  /**
313
- * Lowercased tagName of an element. Runs bound to the element (Playwright's
314
- * Locator.evaluate), used to tell a native input/textarea from a
315
- * contenteditable box.
314
+ * Lowercased tagName of an element, used to tell a native input/textarea from
315
+ * a contenteditable box. Runs bound to the element.
316
316
  */
317
317
  export function elementTagNameInDom(el) {
318
318
  return el.tagName.toLowerCase();
@@ -387,7 +387,8 @@ export function submitLoginFormInDom(opts) {
387
387
  return true;
388
388
  }
389
389
  }
390
- // Fallback: the primary-looking button in the scope.
390
+ // No labelled submit control matched, so a React-rendered form is the likely
391
+ // case: click the primary-looking button instead.
391
392
  const primary = btns.find((b) => {
392
393
  const cls = (b.className || '').toString();
393
394
  return /primary|submit|ds-button--primary/i.test(cls);
@@ -16,7 +16,9 @@
16
16
  // 2. autoLogin() with saved creds -> done (this is the FIRST step after
17
17
  // the probe; credentials are permanent)
18
18
  // 3. guest is allowed and no login was requested -> work as a guest
19
- // 4. not interactive -> LoginRequiredError (fast, no window)
19
+ // 4. not interactive -> LoginRequiredError (fast, no window,
20
+ // so a cold login fails instead of
21
+ // hanging)
20
22
  // 5. no display -> fall back to the in-TUI forms
21
23
  // 6. headless -> relaunchSafe(headed), sign in
22
24
  // 7. wait until probeLogin() === 'in' (poll), the operator closes the window
@@ -57,8 +59,9 @@ export async function interactiveBrowserLogin(deps) {
57
59
  // 4) Non-interactive: fail fast instead of hanging on a window nobody sees.
58
60
  if (!deps.interactive)
59
61
  throw new LoginRequiredError(deps.provider);
60
- // 5) No display: the in-TUI form is the accepted fallback (DESIGN-NOTES
61
- // "Login stays inside the TUI").
62
+ // 5) No display: the in-TUI form is the accepted fallback — a captcha cannot
63
+ // be typed in a terminal, but a password/code form still can. Without one
64
+ // there is nothing to offer, so fail like a non-interactive run.
62
65
  if (!deps.canShowWindow) {
63
66
  if (deps.tuiLogin) {
64
67
  const ok = await deps.tuiLogin().catch(() => false);
@@ -109,8 +112,9 @@ async function runHeadedLogin(deps) {
109
112
  const r = await deps.probeLogin().catch(() => 'unknown');
110
113
  if (r === 'in')
111
114
  return true;
112
- // A closed window is the operator's "I am done" signal: the browser object
113
- // may still hold the session cookies, so verify once more and accept.
115
+ // A closed window is the operator's "I am done" signal. The session may
116
+ // have been established while the page was closing, so probe ONCE more —
117
+ // skipping that probe threw LoginRequiredError on a signed-in operator.
114
118
  if (deps.windowClosed()) {
115
119
  deps.notice('login.window_closed');
116
120
  const again = await deps
@@ -14,7 +14,7 @@ export function cleanAnswer(raw) {
14
14
  return '';
15
15
  return raw;
16
16
  }
17
- // Pure, state-free helpers extracted from browser.ts . Nothing
17
+ // Pure, state-free helpers extracted from browser.ts. Nothing
18
18
  // here touches the DeepSeekBrowser instance state, so this module is safe to
19
19
  // import anywhere (including hot-reloaded modules) without pulling in the
20
20
  // live Playwright context. browser.ts re-exports the public names, so the
@@ -318,15 +318,16 @@ export function normText(s) {
318
318
  .replace(/\s+/g, ' ')
319
319
  .trim();
320
320
  }
321
- // The "too frequent" error: distinct from others so ask() waits a long time
322
- // (DeepSeek limits reset over minutes) and retries the send itself.
321
+ // Distinct from the other failures so ask() waits a long time (DeepSeek limits
322
+ // reset over minutes) and then retries the send itself.
323
323
  export class RateLimitError extends Error {
324
324
  constructor(detail) {
325
325
  super('Messages too frequent. Try again later. ' + detail);
326
326
  this.name = 'RateLimitError';
327
327
  }
328
328
  }
329
- // DeepSeek transient server error (Server busy).
329
+ // A transient DeepSeek overload: clears in seconds, so ask() retries it quickly
330
+ // instead of waiting out the rate-limit pause.
330
331
  export class ServerBusyError extends Error {
331
332
  constructor(detail) {
332
333
  super('Server busy. Try again later. ' + detail);
@@ -357,7 +358,6 @@ export class GenerationIncompleteError extends Error {
357
358
  this.name = 'GenerationIncompleteError';
358
359
  }
359
360
  }
360
- // ---------- profile cleanup ----------
361
361
  export async function cleanSingletonFiles(dir = USER_DATA_DIR) {
362
362
  const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
363
363
  for (const name of names) {
package/dist/browser.js CHANGED
@@ -18,7 +18,7 @@ import { translate, DEFAULT_LOCALE } from './i18n.js';
18
18
  // here preserves the existing public API (test/*.test.ts import them from
19
19
  // browser.ts).
20
20
  export { STOP_NAME_RE, CONTINUE_NAME_RE };
21
- // The pure, state-free helpers live in browser-pure.ts : they are
21
+ // The pure, state-free helpers live in browser-pure.ts: they are
22
22
  // imported for the class below and RE-EXPORTED here, so the public API of this
23
23
  // module is unchanged (tests and other modules import these names from
24
24
  // './browser.js').
@@ -278,12 +278,8 @@ export class DeepSeekBrowser {
278
278
  catch { }
279
279
  }
280
280
  }
281
- // A lightweight IN-PAGE marker so the operator, looking at the browser
282
- // window, can tell that the agent is generating (the terminal spinner is not
283
- // visible there). We prefix the tab title with a "⏳"; the ORIGINAL title is
284
- // saved ONCE on the window and restored when generation ends, so DeepSeek's
285
- // own chat title is never lost. Best-effort: a page error must never break
286
- // the send loop.
281
+ // Best-effort: a page error while marking the tab title must never break the
282
+ // send loop.
287
283
  async _setBusyTitle(busy) {
288
284
  try {
289
285
  await this.page.evaluate(setBusyTitleInDom, busy);
@@ -1313,8 +1309,8 @@ export class DeepSeekBrowser {
1313
1309
  const raw = await this._readLastAnswerTextDom().catch(() => '');
1314
1310
  return this._cleanAnswer(raw);
1315
1311
  }
1316
- // The pure filter lives in browser-pure.ts (cleanAnswer); kept as a method
1317
- // for the existing `this._cleanAnswer(...)` call sites.
1312
+ // The filter itself is pure (cleanAnswer in browser-pure.ts); the method is
1313
+ // kept so the existing `this._cleanAnswer(...)` call sites stay unchanged.
1318
1314
  _cleanAnswer(raw) {
1319
1315
  return cleanAnswer(raw);
1320
1316
  }
@@ -2752,17 +2748,11 @@ export class DeepSeekBrowser {
2752
2748
  // bottom. Used by /resume and /resume-id so the operator sees the restored
2753
2749
  // context in the terminal instead of just "Chat opened.".
2754
2750
  //
2755
- // Best-effort DOM scraping: DeepSeek renders every message as a markdown
2756
- // block (assistant) or a plain bubble (user). Role detection relies on the
2757
- // ds-assistant-message-* class; anything that is not an assistant block is
2758
- // treated as a user message. The model's reasoning (.ds-think-content) is
2759
- // skipped, exactly like in _readLastAnswerText.
2760
- // Read the WHOLE visible dialogue of the currently open chat, top to
2761
- // bottom. Used by /resume and /resume-id so the operator sees the restored
2762
- // context in the terminal instead of just "Chat opened.".
2763
- //
2764
- // The in-page scraper (scrapeChatMessages, browser-chats.ts) is a named
2765
- // export so the class body stays thin; it runs inside the page.
2751
+ // Best-effort DOM scraping (the in-page scrapeChatMessages in
2752
+ // browser-chats.ts, kept as a named export so the class stays thin): role
2753
+ // detection relies on the ds-assistant-message-* class, anything that is not
2754
+ // an assistant block is a user message, and the model's reasoning
2755
+ // (.ds-think-content) is skipped exactly like in _readLastAnswerText.
2766
2756
  async readChatMessages() {
2767
2757
  if (!this.page)
2768
2758
  return [];
package/dist/changelog.js CHANGED
@@ -1,15 +1,12 @@
1
- // Conventional-Commits -> CHANGELOG draft. PURE and
2
- // unit-tested: the draft is built from git log OUTSIDE the release flow
3
- // (scripts/changelog-draft.mts), so a misparse never touches CHANGELOG.md.
4
- // We do NOT auto-write the file: a human (or the agent) reviews the draft
5
- // first -- the changelog must stay a deliberate document, not a byproduct.
6
- // Parse one commit SUBJECT into a conventional commit, or null when it
7
- // does not follow the format (a free-form subject is skipped from the draft).
8
- // Supported forms:
9
- // feat: add X feat(cli): add X
10
- // fix!: change Y feat!: breaking
11
- // Explicit breaking markers: `!` after the type/scope. A BREAKING CHANGE
12
- // body footer is beyond a subject-only parser (git log %s only) -- out of scope.
1
+ // Conventional-Commits -> CHANGELOG draft. PURE and unit-tested: the draft is
2
+ // built from git log by scripts/changelog-draft.mts (and the release script),
3
+ // and `syncUnreleased` rewrites the Unreleased section only on the explicit
4
+ // --write flag — the changelog must stay a deliberate document, not a
5
+ // byproduct of whatever landed on the branch.
6
+ // Parse one commit SUBJECT into a conventional commit, or null when it does
7
+ // not follow the format (a free-form subject is skipped from the draft).
8
+ // `feat: add X`, `feat(cli): add X` and `fix!: change Y` are all understood. A
9
+ // `BREAKING CHANGE:` body footer is out of scope: git log %s carries no body.
13
10
  export function parseConventionalCommit(subject) {
14
11
  const s = String(subject ?? '').trim();
15
12
  if (!s)
@@ -37,11 +34,9 @@ const KIND_BY_TYPE = {
37
34
  };
38
35
  // Types that never belong in a user-facing changelog (build/CI/internal).
39
36
  const SKIP_TYPES = new Set(['chore', 'ci', 'test', 'build', 'docs', 'internal']);
40
- // Group parsed commits by changelog kind. Each entry is a short
41
- // imperative line; a scope is prefixed as `**scope:** ` and a breaking marker
42
- // is appended as `(BREAKING)`. Order within a group is preserved (commit
43
- // order), and duplicate lines are dropped so a rebase that repeats a subject
44
- // does not flip the draft twice.
37
+ // Group parsed commits by changelog kind. A scope is prefixed as `**scope:** `
38
+ // and a breaking marker appended as `(BREAKING)`. Duplicate lines are dropped
39
+ // so a rebase that repeats a subject does not list it twice.
45
40
  export function groupCommits(forCommit) {
46
41
  const groups = {
47
42
  Added: [],
@@ -63,9 +58,8 @@ export function groupCommits(forCommit) {
63
58
  }
64
59
  return groups;
65
60
  }
66
- // Render the groups as a changelog body (the parts under a version
67
- // heading), with the standard Keep-a-Changelog order and empty groups
68
- // omitted. Returns '' when there is nothing to report.
61
+ // Render the groups as a changelog body, in the standard Keep-a-Changelog
62
+ // order and with empty groups omitted. Returns '' when there is nothing to report.
69
63
  export function renderChangelogDraft(groups) {
70
64
  const order = ['Added', 'Fixed', 'Changed', 'Removed'];
71
65
  const out = [];
@@ -4,7 +4,7 @@ import os from 'os';
4
4
  import { execFile } from 'child_process';
5
5
  import { randomBytes } from 'crypto';
6
6
  import { writeJsonAtomic } from './fsutil.js';
7
- // Checkpoints / rewind .
7
+ // Checkpoints / rewind.
8
8
  //
9
9
  // Undo (src/undo.ts) reverts ONE file write. A checkpoint snapshots the WHOLE
10
10
  // working tree at the start of a task, so a risky task can be rolled back with
@@ -21,7 +21,6 @@ const CHECKPOINT_DIR = path.join(os.homedir(), '.zames', 'checkpoints');
21
21
  // dir. Everything else is part of the snapshot. Patterns are anchored to the
22
22
  // archive root ('./x') so a nested `src/tmp` is NOT accidentally excluded.
23
23
  export const CHECKPOINT_EXCLUDES = ['node_modules', '.git', 'dist', 'tmp'];
24
- // ---------- pure helpers (unit-tested without tar) ----------
25
24
  export function tarCreateArgs(archive, _dir) {
26
25
  const args = ['-czf', archive];
27
26
  for (const e of CHECKPOINT_EXCLUDES)
@@ -40,15 +39,14 @@ export function rotateRecords(records, max) {
40
39
  return records.slice();
41
40
  return records.slice(records.length - max);
42
41
  }
43
- /**
44
- * Resolve a `/rewind` selector against records sorted NEWEST FIRST.
45
- * `1` = most recent; a non-numeric selector is matched as a stamp prefix.
46
- */
47
42
  /** True for a selector that is exactly a full epoch-ms stamp (13 digits). */
48
43
  export function isFullStampSelector(selector) {
49
44
  return /^\d{13}$/.test(String(selector ?? '').trim());
50
45
  }
51
- const isFullStamp = isFullStampSelector;
46
+ /**
47
+ * Resolve a `/rewind` selector against records sorted NEWEST FIRST.
48
+ * `1` = most recent; a longer number is matched as a stamp prefix.
49
+ */
52
50
  export function pickRecord(records, selector,
53
51
  /** The FULL record set, when `records` is only a display window. */
54
52
  allRecords) {
@@ -62,7 +60,7 @@ allRecords) {
62
60
  // happened to share its first digits with a NEWER one was resolved to that
63
61
  // WRONG record. A full stamp is therefore matched against the full set
64
62
  // FIRST, and only then treated as an index.
65
- if (isFullStamp(sel)) {
63
+ if (isFullStampSelector(sel)) {
66
64
  const pool = allRecords && allRecords.length ? allRecords : records;
67
65
  const exact = pool.find((r) => String(r.stamp) === sel);
68
66
  if (exact)
@@ -85,7 +83,6 @@ allRecords) {
85
83
  pool.find((r) => String(r.stamp).startsWith(sel)) ||
86
84
  null);
87
85
  }
88
- // ---------- tar / fs plumbing ----------
89
86
  function runTar(args, cwd, timeout = 60_000) {
90
87
  return new Promise((resolve) => {
91
88
  // `execFile` throws SYNCHRONOUSLY on a spawn error (ENOENT for the shell,
@@ -137,7 +134,6 @@ async function copyTree(from, to) {
137
134
  await fs.copyFile(src, dst);
138
135
  }
139
136
  }
140
- // ---------- store ----------
141
137
  export class CheckpointStore {
142
138
  enabled;
143
139
  maxBackups;
package/dist/cli-args.js CHANGED
@@ -1,4 +1,4 @@
1
- // PURE CLI argument parsing . index.ts used to hold `args` as a
1
+ // PURE CLI argument parsing. index.ts used to hold `args` as a
2
2
  // module-level constant plus three helpers closing over it, which made the
3
3
  // parsing impossible to unit-test in isolation (index.ts is the entry point and
4
4
  // is not covered). `createCliArgs(argv)` binds the argv ONCE and returns the
@@ -23,11 +23,9 @@ export function createCliArgs(argv) {
23
23
  const args = Array.isArray(argv) ? argv : [];
24
24
  const getArg = (flag, fallback = null) => {
25
25
  const i = args.indexOf(flag);
26
- // `args[i + 1]` truthiness means an empty string or a missing value falls
27
- // back — the historical behavior (an empty `--task ''` is not a task).
28
- // NOTE: this does NOT special-case a following FLAG, so `--task --debug`
29
- // returns `--debug` (the original inline helper did the same); we preserve
30
- // it rather than "fix" a long-standing quirk in a refactor.
26
+ // Truthiness, not `!== undefined`: an EMPTY value (`--task ''`) must
27
+ // fall back too - an empty task is not a task. A following FLAG is not
28
+ // special-cased either (see the CliArgs doc above).
31
29
  return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
32
30
  };
33
31
  const hasFlag = (flag) => args.includes(flag);
@@ -1,9 +1,6 @@
1
- // PURE extraction of fenced code blocks from a Markdown answer.
2
- //
3
- // The TUI cannot offer a real "copy button", so copying a code block is bound
4
- // to a key / command. That needs the RAW fenced text (the highlighted render is
5
- // full of ANSI and must NOT be copied), which is what this module returns.
6
- // Kept pure so it is unit-tested without a terminal.
1
+ // PURE extraction of fenced code blocks from a Markdown answer. It returns the
2
+ // RAW fenced text: the highlighted render is full of ANSI and must never be
3
+ // what gets copied out of the TUI.
7
4
  /**
8
5
  * Extract every fenced code block (``` or ~~~) from `md`, in order. Handles a
9
6
  * longer closing fence than the opening one and an unterminated final block
package/dist/commands.js CHANGED
@@ -3,9 +3,9 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import { isImageName } from './attachments.js';
5
5
  import { translate } from './i18n.js';
6
- // Helpers for the extra slash commands (/diff, /cost, /export, /doctor,
7
- // /review, /add-dir). Pure functions, unit-tested without a
8
- // live agent/browser. index.ts only renders their output.
6
+ // Helpers for the slash commands (/diff, /cost, /export, /doctor, /review,
7
+ // /add-dir, /queue, /goal, custom commands). Pure functions, unit-tested
8
+ // without a live agent/browser; index.ts only renders their output.
9
9
  const NL = String.fromCharCode(10);
10
10
  const CR = String.fromCharCode(13);
11
11
  // ---------- /diff ----------
@@ -27,8 +27,8 @@ export function formatDiff(diffText, opts = {}, t = translate('en')) {
27
27
  const rest = lines.length - maxLines;
28
28
  return head + NL + t('diff.more_lines', { n: String(rest) });
29
29
  }
30
- // Git arguments for the `/diff` command. An ARRAY (no shell): the caller runs
31
- // them through runGitArgs(), so nothing is interpreted by a shell. Pure.
30
+ // Git arguments for the `/diff` command. An ARRAY, not a shell string: the
31
+ // caller runs them through runGitArgs(), so nothing is interpreted by a shell.
32
32
  export function diffGitArgs(staged = false) {
33
33
  return staged ? ['diff', '--staged'] : ['diff'];
34
34
  }
@@ -1038,10 +1038,6 @@ export function mergeMessages(msgs) {
1038
1038
  list.length +
1039
1039
  ' messages while you were working. They are listed below in order — ' +
1040
1040
  'treat them as one instruction.';
1041
- // The parts are OPERATOR text: a line that reads `---` (a Markdown rule, a
1042
- // pasted diff) would spoof a message boundary and the model would see an
1043
- // extra message. Neutralize such a line inside a part; the separator below
1044
- // stays the only one.
1045
1041
  // The message text is OPERATOR input: a line that reads `---` (a Markdown
1046
1042
  // rule, a pasted diff) or repeats the header sentence spoofs a message
1047
1043
  // BOUNDARY / count — the model then sees an extra message that never
@@ -1,4 +1,4 @@
1
- // The /config subcommand DISPATCH . The PURE text rendering lives
1
+ // The /config subcommand DISPATCH. The PURE text rendering lives
2
2
  // in config-commands.ts; this module owns the parsing/routing of the text
3
3
  // subcommands (list/get/set/reset/lang/path) and prints through injected
4
4
  // callbacks. Everything that touches live state (the runtime config object,
@@ -26,7 +26,7 @@ export async function handleConfigCommand(input, deps) {
26
26
  ? all
27
27
  : ['/config', ...all];
28
28
  const sub = (parts[1] || '').toLowerCase();
29
- // With no subcommand - the menu (or a text list in non-TTY).
29
+ // Bare `/config` opens the menu (a text list in non-TTY, via openMenu).
30
30
  if (!sub || sub === 'menu') {
31
31
  await openMenu();
32
32
  return;
@@ -39,7 +39,8 @@ export async function handleConfigCommand(input, deps) {
39
39
  print(theme.system(t('cfg.paths', { global: homeConfigPath, project: projectConfigPath })));
40
40
  return;
41
41
  }
42
- // /config lang <ru|en> - quick access to ui.locale
42
+ // `/config lang <ru|en>` is a shortcut for `set ui.locale`; `язык` is an
43
+ // accepted alias because the operator types the command in their own language.
43
44
  if (sub === 'lang' || sub === 'language' || sub === 'язык') {
44
45
  const val = parts[2];
45
46
  if (!val) {
@@ -123,7 +124,8 @@ export async function handleConfigCommand(input, deps) {
123
124
  print(theme.assistant(t('cfg.reset', { v: key })));
124
125
  return;
125
126
  }
126
- // Unknown subcommand - show the list.
127
+ // An unknown subcommand reports the key AND shows the list, so the operator
128
+ // sees the valid keys without a second command.
127
129
  printErr(theme.error(t('cfg.unknown_key', { v: sub })));
128
130
  showList();
129
131
  }
@@ -1,4 +1,4 @@
1
- // PURE rendering for the /config command's TEXT output . These
1
+ // PURE rendering for the /config command's TEXT output. These
2
2
  // helpers format the schema into lines; they touch no browser/terminal state,
3
3
  // so they are unit-tested without a live agent (the /config dispatch in
4
4
  // index.ts only prints what these return). The interactive menu lives in
@@ -6,8 +6,7 @@
6
6
  import { theme } from './theme.js';
7
7
  /**
8
8
  * The value shown for a field in the LIST: the default marker, on/off for a
9
- * boolean, a fixed mask for a password, otherwise the JSON form. Mirrors the
10
- * long-used inline logic so the output is byte-for-byte identical.
9
+ * boolean, a fixed mask for a password, otherwise the JSON form.
11
10
  */
12
11
  export function configListValue(field, value, t) {
13
12
  if (value === undefined)
@@ -4,9 +4,9 @@
4
4
  // construction, so a change must be pushed onto the live instance too or it
5
5
  // is silently a no-op until a restart. The path->target mapping is PURE and
6
6
  // unit-tested here; index.ts wires it to the live instances.
7
- /** config path -> field name on the browser instance. Both backends
8
- * share these names; a field that is absent on the active backend
9
- * (e.g. DeepSeek-only thinkingExtraMs on Qwen) is skipped, not thrown. */
7
+ /** config path -> field name on the browser instance. All three backends
8
+ * share these names; a field that is absent on the active backend is skipped
9
+ * rather than throwing, so one map serves every provider. */
10
10
  export const BROWSER_LIVE_CONFIG = {
11
11
  'browser.answerTimeoutMs': 'answerTimeoutMs',
12
12
  'browser.askRetries': 'askRetries',
@@ -30,11 +30,9 @@ export function applyBrowserLiveConfig(path, value, browser) {
30
30
  const field = BROWSER_LIVE_CONFIG[path];
31
31
  if (!field || !(field in browser))
32
32
  return false;
33
- // A timing value must be a FINITE NON-NEGATIVE number. `applyBrowserLiveConfig`
34
- // wrote anything it was given, so a NaN/negative value reached a live field
35
- // (a timer with NaN fires immediately), and `answerTimeoutMs = 0` SKIPPED
36
- // the derived `askDeadlineMs` update, leaving a deadline that no longer
37
- // matches the timeout. Reject the bad value instead of corrupting the field.
33
+ // A timing value must be a FINITE NON-NEGATIVE number: a NaN reached a live
34
+ // field (a timer with NaN fires immediately) and a negative one is nonsense.
35
+ // Reject the value instead of corrupting the field.
38
36
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
39
37
  return false;
40
38
  }
@@ -45,11 +43,7 @@ export function applyBrowserLiveConfig(path, value, browser) {
45
43
  // give up before the browser's own timeout ever fired. Re-derive it the same
46
44
  // way the constructor does.
47
45
  if (field === 'answerTimeoutMs' && 'askDeadlineMs' in browser) {
48
- // ANY accepted number re-derives the deadline; the old `value > 0` guard
49
- // left the deadline at its stale value when the timeout was set to 0.
50
- if (typeof value === 'number' && Number.isFinite(value)) {
51
- browser.askDeadlineMs = value + 60_000;
52
- }
46
+ browser.askDeadlineMs = value + 60_000;
53
47
  }
54
48
  return true;
55
49
  }
@@ -11,9 +11,9 @@
11
11
  import { theme } from './theme.js';
12
12
  import { visLen } from './input.js';
13
13
  const ESC = String.fromCharCode(27);
14
- // Strip ANSI sequences to compute the visible length of a line. (A module-
15
- // level name distinct from input.ts's internal helper; the old regex had a
16
- // stray backtick and never matched a real CSI, so visCols was over-counted.)
14
+ // Strip ANSI sequences to compute the visible length of a line. A local copy
15
+ // (input.ts keeps its own private helper): the regex must match a real CSI,
16
+ // otherwise visCols over-counts and the erase moves to the wrong row.
17
17
  function stripAnsiLocal(s) {
18
18
  return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
19
19
  }
@@ -197,7 +197,8 @@ export function runConfigMenu(opts) {
197
197
  lines.push(...tail);
198
198
  // Erase the previous block and print the new one. We count VISUAL lines
199
199
  // (accounting for wrapping by terminal width), otherwise on narrow
200
- // terminals you get "garbage" from unerased tails.
200
+ // terminals the unerased tails leave garbage behind.
201
+ //
201
202
  // Reserve the last column. A line whose visible width reaches exactly the
202
203
  // terminal width hits the autowrap edge: depending on the terminal it
203
204
  // either consumes two rows for the following `\n` or keeps a pending wrap,