zames_pro 2.82.137 → 2.82.138

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 (58) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/agent-loop.js +3 -6
  3. package/dist/answer-parse.js +10 -16
  4. package/dist/attach-refs.js +3 -3
  5. package/dist/attachments.js +7 -8
  6. package/dist/backlog.js +2 -5
  7. package/dist/banner.js +4 -5
  8. package/dist/browser-chats.js +4 -4
  9. package/dist/browser-dom.js +16 -15
  10. package/dist/browser-login.js +9 -5
  11. package/dist/browser-pure.js +5 -5
  12. package/dist/browser.js +10 -20
  13. package/dist/changelog.js +14 -20
  14. package/dist/checkpoint.js +6 -10
  15. package/dist/cli-args.js +4 -6
  16. package/dist/code-blocks.js +3 -6
  17. package/dist/commands.js +5 -9
  18. package/dist/config-command.js +6 -4
  19. package/dist/config-commands.js +2 -3
  20. package/dist/config-live.js +7 -13
  21. package/dist/config-menu.js +5 -4
  22. package/dist/config.js +5 -8
  23. package/dist/deepseek-ui.js +7 -7
  24. package/dist/diagnostics.js +5 -7
  25. package/dist/diff.js +8 -10
  26. package/dist/extraTools.js +3 -3
  27. package/dist/fsutil.js +0 -4
  28. package/dist/gitTools.js +5 -4
  29. package/dist/glm-browser.js +2 -10
  30. package/dist/glm-chats.js +0 -1
  31. package/dist/glm-net-capture.js +1 -1
  32. package/dist/glm-ui.js +2 -2
  33. package/dist/goal.js +1 -4
  34. package/dist/i18n/common.js +2 -2
  35. package/dist/i18n.js +1 -2
  36. package/dist/index.js +0 -11
  37. package/dist/input/suggest.js +4 -7
  38. package/dist/input.js +3 -8
  39. package/dist/markdown.js +0 -1
  40. package/dist/mcp.js +2 -5
  41. package/dist/net-capture.js +1 -1
  42. package/dist/path-token.js +7 -14
  43. package/dist/prompt.js +4 -3
  44. package/dist/qwen-browser.js +1 -8
  45. package/dist/qwen-chats.js +0 -1
  46. package/dist/qwen-net-capture.js +4 -7
  47. package/dist/qwen-ui.js +0 -3
  48. package/dist/run-task.js +4 -4
  49. package/dist/self-review.js +0 -5
  50. package/dist/send-runtime.js +6 -10
  51. package/dist/send-telemetry.js +5 -7
  52. package/dist/send-throttle.js +4 -5
  53. package/dist/sessions.js +6 -7
  54. package/dist/spinner.js +3 -4
  55. package/dist/statusline.js +5 -9
  56. package/dist/system-prompt.js +1 -1
  57. package/dist/tui/widgets.js +0 -1
  58. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.82.138] - 2026-10-25
11
+
12
+ ### Changed
13
+
14
+ - docs(types): fix a stale provider count
15
+ - docs(tui/widgets): prune a section divider
16
+ - docs(tui): prune section dividers
17
+ - docs(system-prompt): drop a backlog-id reference
18
+ - docs(statusline): prune and fix stale comments
19
+ - docs(status-command): prune and fix stale comments
20
+ - docs(spinner): prune and fix stale comments
21
+ - docs(shell): prune and fix stale comments
22
+ - docs(sessions): prune and fix stale comments
23
+ - docs(send-throttle): prune and fix stale comments
24
+ - docs(send-telemetry): prune and fix stale comments
25
+ - docs(send-runtime): prune and fix stale comments
26
+ - docs(self-review): prune section dividers
27
+ - docs(run-task): prune and fix stale comments
28
+ - docs(qwen-ui): prune and fix stale comments
29
+ - docs(qwen-net-capture): prune and fix stale comments
30
+ - docs(qwen-chats): prune and fix stale comments
31
+ - docs(qwen-browser): prune section dividers
32
+ - docs(prompt): prune and fix stale comments
33
+ - docs(permissions): prune and fix stale comments
34
+ - docs(path-token): prune and fix stale comments
35
+ - docs(net-capture): prune and fix stale comments
36
+ - docs(mcp): prune and fix stale comments
37
+ - docs(markdown): prune and fix stale comments
38
+ - docs(limits-command): prune and fix stale comments
39
+ - docs(input/suggest): prune and fix stale comments
40
+ - docs(input/layout): drop a backlog-id reference
41
+ - docs(input): prune backlog ids and section dividers
42
+ - docs(index): prune section dividers
43
+ - docs(i18n): prune and fix stale comments
44
+ - docs(i18n): drop backlog-id references from common.ts comments
45
+ - docs(goal): prune and fix stale comments
46
+ - docs(glm-ui): prune and fix stale comments
47
+ - docs(glm-net-capture): prune and fix stale comments
48
+ - docs(glm-chats): prune and fix stale comments
49
+ - docs(glm-browser): prune and fix stale comments
50
+ - docs(glm-browser): prune and fix stale comments
51
+ - docs(gitTools): prune and fix stale comments
52
+ - docs(fsutil): prune and fix stale comments
53
+ - docs(extraTools): prune and fix stale comments
54
+ - docs(diff): prune and fix stale comments
55
+ - docs(diagnostics): prune and fix stale comments
56
+ - docs(deepseek-ui): prune and fix stale comments
57
+ - docs(config): prune and fix stale comments
58
+ - docs(config-menu): prune and fix stale comments
59
+ - docs(config-live): prune and fix stale comments
60
+ - docs(config-commands): prune and fix stale comments
61
+ - docs(config-command): prune and fix stale comments
62
+ - docs(commands): prune and fix stale comments
63
+ - docs(code-blocks): prune and fix stale comments
64
+ - docs(cli-args): prune and fix stale comments
65
+ - docs(checkpoint): prune and fix stale comments
66
+ - docs(changelog): prune and fix stale comments
67
+ - docs(browser): prune and fix stale comments
68
+ - docs(browser-pure): prune and fix stale comments
69
+ - docs(browser-login): prune and fix stale comments
70
+ - docs(browser-dom): prune and fix stale comments
71
+ - docs(browser-chats): prune and fix stale comments
72
+ - docs(banner): prune and fix stale comments
73
+ - docs(backlog): prune and fix stale comments
74
+ - docs(attachments): prune and fix stale comments
75
+ - docs(attach-refs): prune and fix stale comments
76
+ - docs(attach-flow): prune and fix stale comments
77
+ - docs(answer-parse): prune and fix stale comments
78
+ - docs(agent-loop): prune and fix stale comments
79
+
10
80
  ## [2.82.137] - 2026-10-24
11
81
 
12
82
  ### Changed
@@ -233,14 +233,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
233
233
  // call but was not recognized (e.g. a DSML/XML form the strict parser
234
234
  // missed), the tool did NOT run and the echo is the only copy of the
235
235
  // call — it must be executed, not discarded as stale.
236
- // const ranToolLastTurn follows the previous turn's actual effect.
237
236
  let lastTurnRanTool = false;
238
237
  let justRanTool = false;
239
238
  let watchdogRetries = 0;
240
239
  const MAX_WATCHDOG_RETRIES = 3;
241
- // Retry budget for a browser.ask() TIMEOUT (not for content): the send did
242
- // not come back in time. This is separate from unparsedRetries because a
243
- // timeout is an infrastructure failure, not a model protocol violation.
244
240
  // Loop detection: the model sometimes repeats the SAME tool call with the
245
241
  // SAME arguments (e.g. Read the same file) turn after turn. There was no
246
242
  // explicit detector for that — the stale-answer guard only catches a
@@ -263,6 +259,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
263
259
  // Set when the argument-error series hits the cap; appended to the message
264
260
  // AFTER the tool results are formatted (they rebuild `message`).
265
261
  let argErrNudge = '';
262
+ // Retry budget for a browser.ask() TIMEOUT (not for content): the send did
263
+ // not come back in time. Separate from unparsedRetries because a timeout is
264
+ // an infrastructure failure, not a model protocol violation.
266
265
  let afterToolRetries = 0;
267
266
  // toolsRanInTask: how many tools actually ran in THIS task. The protocol
268
267
  // guard uses it to tell "started, then slipped into chat mode" (a reasoning
@@ -1065,8 +1064,6 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
1065
1064
  const poll = setInterval(syncToolAbort, 100);
1066
1065
  if (typeof poll.unref === 'function')
1067
1066
  poll.unref();
1068
- // Time the tool: the transcript carries durationMs so /cost can
1069
- // show where the time goes (frequent Read→Edit cycles vs slow Bash).
1070
1067
  const toolStart = Date.now();
1071
1068
  try {
1072
1069
  result = await tool.fn(call.args, { signal: toolAbort.signal });
@@ -1,12 +1,9 @@
1
1
  import { parseXmlToolCalls } from './xml-toolcall.js';
2
2
  import { normText } from './browser-pure.js';
3
- // Pure, state-free helpers extracted from agent-loop.ts : the
4
- // tool-call PARSER (JSON / permissive / XML-DSML repair) and the small answer
5
- // heuristics. None of them touch the browser or the loop state, so this
6
- // module is safe to import on its own. agent-loop.ts imports the ones the
7
- // loop needs and RE-EXPORTS the public ones, so the existing API (tests import
8
- // parseToolCall / responseLooksLikeToolCall / truncateToolResult from './agent-loop.js')
9
- // is unchanged.
3
+ // Pure, state-free helpers split out of agent-loop.ts: the tool-call PARSER
4
+ // (JSON / permissive / XML-DSML repair) and the small answer heuristics. None
5
+ // of them touch the browser or the loop state, so the module is safe to import
6
+ // on its own; tests and agent-loop.ts import the functions from here directly.
10
7
  // Cap a tool result for the model, but make the truncation EXPLICIT: a bare
11
8
  // slice() silently hid the tail and the model had no idea it was looking at a
12
9
  // partial output (it would act on a half-read file/log).
@@ -537,11 +534,9 @@ function parseToolCallPermissive(text) {
537
534
  const tool = toolMatch[1];
538
535
  // The model occasionally puts the argument keys INLINE with "tool" —
539
536
  // {"tool": "Bash", "command": "..."} — with no "args" wrapper at all.
540
- // The strict parser rejects it (no obj.args), and the permissive parser
541
- // used to bail out too (indexOf('"args"') === -1). Such a call was
542
- // reported as malformed and re-asked up to MAX_MALFORMED_RETRIES times;
543
- // after the budget ran out the run stopped with the model's text as the
544
- // final answer. Flatten the inline keys into args when "args" is absent.
537
+ // The strict parser rejects such a call (no obj.args); flatten the inline
538
+ // keys into args instead of letting the loop re-ask until it gives up and
539
+ // stops with the model's text as the final answer.
545
540
  if (text.indexOf('"args"') === -1) {
546
541
  const braceIdx = text.indexOf('{');
547
542
  if (braceIdx === -1)
@@ -730,16 +725,15 @@ function repairToolCallPreamble(text) {
730
725
  const m = t.match(/(?:^|[^A-Za-z0-9_])(?:\*\*)?(tool)(?:\*\*)?["'`\u2018\u2019\u201c\u201d]*\s*:/);
731
726
  if (!m || m.index === undefined)
732
727
  return null;
733
- // We look for the start from the first quote/bracket around the key, otherwise from the word tool.
734
728
  let start = m.index;
735
729
  const brace = t.indexOf('{', Math.max(0, start - 1));
736
730
  if (brace !== -1 && brace < start)
737
731
  start = brace;
738
732
  let frag = t.slice(start);
739
- // If the fragment does not start with `{` — we add it.
733
+ // The head lost its opening `{`, so rebuild one around the key below.
740
734
  if (!frag.startsWith('{')) {
741
- // The key may have lost its opening quote: tool": → "tool".
742
- // We trim the leading junk up to the word tool and normalize the key quotes.
735
+ // The key lost its opening quote (`tool":`), so drop the junk before it
736
+ // and re-add the quote.
743
737
  frag = frag.replace(/^[^A-Za-z0-9_]*/, '');
744
738
  frag = frag.replace(/^(?:\*\*)?(["'`\u2018\u2019\u201c\u201d]*)(tool)(?:\*\*)?["'`\u2018\u2019\u201c\u201d]*\s*:/, '"$2":');
745
739
  frag = '{' + frag;
@@ -2,7 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { looksLikeFilePath, isImageName } from './attachments.js';
4
4
  import { extractAtFileRefs } from './path-token.js';
5
- // Path resolution and `@file` reference inlining .
5
+ // Path resolution and `@file` reference inlining.
6
6
  // Extracted from src/index.ts so the logic is unit-testable without the whole
7
7
  // CLI. This module touches only the filesystem (no browser/terminal).
8
8
  const NL = String.fromCharCode(10);
@@ -81,12 +81,12 @@ async function isInside(workdir, target) {
81
81
  const rel = path.relative(root, real);
82
82
  return !rel.startsWith('..') && !path.isAbsolute(rel);
83
83
  }
84
- // Per-file and total caps for `@path` inlining . A reference should SAVE
84
+ // Per-file and total caps for `@path` inlining. A reference should SAVE
85
85
  // round-trips, not flood the context: a huge file is truncated with a note
86
86
  // telling the agent to read the rest itself.
87
87
  export const AT_REF_FILE_CAP = 60 * 1024;
88
88
  export const AT_REF_TOTAL_CAP = 200 * 1024;
89
- // Inline `@path` references found in a task text . Returns the augmented
89
+ // Inline `@path` references found in a task text. Returns the augmented
90
90
  // text plus the list of files actually inlined (for the operator notice).
91
91
  // Unknown/unreadable references are left in place untouched -- they may be
92
92
  // part of the prose; we never fail the task over a missing file.
@@ -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;