zames_pro 2.96.6 → 2.96.8

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/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.96.8] - 2026-11-06
11
+
12
+ ### Fixed
13
+
14
+ - **ui-health:** report a capability listed twice only once
15
+ - **tui:** a throwing store subscriber no longer starves the others
16
+ - **undo:** list(0) returns no rows instead of the whole history
17
+ - **shell:** a cd inside a nested shell is checked by the sandbox
18
+ - **sandbox:** a dangling symlink cannot let a write escape the root
19
+
20
+ ## [2.96.7] - 2026-11-06
21
+
22
+ ### Fixed
23
+
24
+ - **tui:** a raw DEL byte is not inserted into the input line
25
+ - **tui:** Down after editing a recalled entry moves to the next entry
26
+ - **tui:** Up/Down keep the visual column on wide-glyph wrapped lines
27
+ - **goal:** a quoted verdict example no longer wins over the real verdict
28
+ - **input:** hold only a partial paste marker, not a lone ESC
29
+
10
30
  ## [2.96.6] - 2026-11-06
11
31
 
12
32
  ### Fixed
package/dist/goal.js CHANGED
@@ -151,7 +151,31 @@ export function parseGoalVerdict(text) {
151
151
  const label = /VERDICT\s*[*_`]*\s*:\s*[*_`]*\s*(IMPOSSIBLE|NOT[_ -]?MET|MET)(?![\w-])/gi;
152
152
  let m;
153
153
  let status = null;
154
+ // True when the backtick run ending right BEFORE `at` is the CLOSING fence
155
+ // of a block that started at the beginning of a line.
156
+ const isFenceClose = (src, at) => {
157
+ let i = at - 1;
158
+ while (i >= 0 && src.charAt(i) === '`')
159
+ i--;
160
+ const fenceEnd = at;
161
+ const fenceStart = i + 1;
162
+ if (fenceEnd - fenceStart < 1)
163
+ return false;
164
+ // The opening run must be at the START of a line (or of the text).
165
+ const lineStart = src.lastIndexOf(String.fromCharCode(10), fenceStart - 1) + 1;
166
+ return src.slice(lineStart, fenceStart).trim() === '';
167
+ };
154
168
  while ((m = label.exec(t)) !== null) {
169
+ // A QUOTED or BACKTICKED label is an EXAMPLE the evaluator is explaining,
170
+ // not its verdict: `A verdict such as "VERDICT: MET" would be wrong here`
171
+ // followed by the real `VERDICT: NOT_MET` used to clear the goal as DONE.
172
+ // A quote/backtick directly BEFORE the label marks an example — unless it
173
+ // is the CLOSING fence of a fenced verdict (```` ```Verdict: MET``` ````),
174
+ // which is the accepted format and must parse.
175
+ const before = t.charAt(m.index - 1);
176
+ if (before === '"' || (before === '`' && !isFenceClose(t, m.index))) {
177
+ continue;
178
+ }
155
179
  // Skip whitespace/backticks between the value and whatever follows.
156
180
  let k = m.index + m[0].length;
157
181
  while (k < t.length && /[\s*_`]/.test(t.charAt(k)))
@@ -159,6 +183,10 @@ export function parseGoalVerdict(text) {
159
183
  const after = t.charAt(k);
160
184
  if (after === '|' || after === '/')
161
185
  continue;
186
+ // A CLOSING quote/backtick right after the value is the other end of an
187
+ // example (`VERDICT: MET` in prose).
188
+ if (after === '"' || after === '`')
189
+ continue;
162
190
  const raw = m[1].toUpperCase();
163
191
  status =
164
192
  raw === 'MET' ? 'met' : raw === 'IMPOSSIBLE' ? 'impossible' : 'not_met';
@@ -659,7 +659,10 @@ export function splitPasteTail(s) {
659
659
  // The longest marker prefix to consider is one char short of the marker.
660
660
  const markers = [PASTE_START, PASTE_END];
661
661
  const maxHold = Math.max(...markers.map((m) => m.length)) - 1;
662
- const upto = Math.min(maxHold, str.length - 1);
662
+ // Hold a PARTIAL marker up to the full marker length: for a 1-char chunk
663
+ // (`str.length - 1` = 0) nothing was held, so a read ending on a lone ESC
664
+ // leaked it into the buffer instead of waiting for the rest of `ESC[200~`.
665
+ const upto = Math.min(maxHold, str.length);
663
666
  for (let len = upto; len >= 1; len--) {
664
667
  const tail = str.slice(str.length - len);
665
668
  if (!tail.startsWith(ESC))
package/dist/input.js CHANGED
@@ -1640,7 +1640,11 @@ export class LineEditor {
1640
1640
  // to _pasteBuf (it is not parsed by this loop), so a held tail would be
1641
1641
  // re-attached to `s` and inserted as text BEFORE the body — the tail
1642
1642
  // (`1~`) leaked into the input line and was not part of the pasted text.
1643
- const held = this._inPaste
1643
+ // The bare-ESC disambiguation below must still see a chunk that IS just an
1644
+ // ESC: holding it as a partial paste marker would swallow the Escape key
1645
+ // (and a following arrow tail would then be read as literal text). Only a
1646
+ // PARTIAL marker of 2+ chars is held here; the lone ESC keeps its own path.
1647
+ const held = this._inPaste || s === ESC
1644
1648
  ? { head: s, tail: null }
1645
1649
  : splitPasteTail(s);
1646
1650
  if (held.tail) {
package/dist/sandbox.js CHANGED
@@ -12,6 +12,17 @@ function realPathNearest(target) {
12
12
  const tail = [];
13
13
  while (true) {
14
14
  try {
15
+ // `lstat` does NOT follow the final link: a DANGLING symlink makes
16
+ // `realpathSync` throw, so the broken link used to fall back to the
17
+ // LEXICAL path and a write THROUGH it landed at its target OUTSIDE the
18
+ // sandbox. Read the link itself and resolve its target too.
19
+ const st = fs.lstatSync(cur);
20
+ if (st.isSymbolicLink()) {
21
+ const raw = fs.readlinkSync(cur);
22
+ const resolvedLink = path.resolve(path.dirname(cur), raw);
23
+ const realTarget = realPathNearest(resolvedLink);
24
+ return path.join(realTarget, ...tail.slice().reverse());
25
+ }
15
26
  return path.join(fs.realpathSync(cur), ...tail.slice().reverse());
16
27
  }
17
28
  catch {
package/dist/shell.js CHANGED
@@ -30,7 +30,13 @@ export function assertCommandInsideRoot(root, command) {
30
30
  // `cd$HOME`, `cd../..` — and runs it as a real directory change. Requiring
31
31
  // whitespace skipped those forms, so a glued `cd../..` walked out of the
32
32
  // root with the guard never firing.
33
- const cdRe = /(?:^|[;&|(\s`])(?:cd|pushd)\s*([^;&|)]+)/gi;
33
+ // A `cd` inside a nested shell (`sh -c "cd / && ls"`) is executed
34
+ // exactly like a top-level one, but the static scan saw no separator
35
+ // before it: the `cd` sits after `-c "`. Treat `-c <quote>` as a
36
+ // separator too, so the target is checked (and rejected when it leaves
37
+ // the root). `$(cd …)` / backticks were already covered by `(` and
38
+ // the backtick.
39
+ const cdRe = /(?:^|[;&|(\s`]|-[a-z]*c\s*['"])(?:cd|pushd)\s*([^;&|)]+)/gi;
34
40
  let m;
35
41
  while ((m = cdRe.exec(cmd))) {
36
42
  // The shell removes EVERY quote in the word, wherever it sits —
package/dist/tui/edit.js CHANGED
@@ -199,9 +199,14 @@ export function moveVisualRows(s, delta, promptStr, cols) {
199
199
  // caret past the target row's text and onto the NEXT row entirely.
200
200
  const curRow = lay.rows[lay.cursorRow];
201
201
  const curChars = Array.from(curRow.text);
202
+ // Walk the characters of the current row until their VISUAL width reaches
203
+ // the caret's column. The old loop ran `cursorCol` TIMES over characters:
204
+ // with a wide (CJK) glyph two columns come from ONE character, so it read
205
+ // past the row (undefined counted as 1) and Up/Down landed too far right.
202
206
  let want = 0;
203
- for (let k = 0; k < lay.cursorCol - visLen(curRow.prefix); k++) {
204
- want += charWidth(curChars[k]?.codePointAt(0) ?? 32);
207
+ const targetCol = lay.cursorCol - visLen(curRow.prefix);
208
+ for (let k = 0; k < curChars.length && want < targetCol; k++) {
209
+ want += charWidth(curChars[k].codePointAt(0));
205
210
  }
206
211
  let idx = row.start;
207
212
  let w = 0;
@@ -221,8 +226,16 @@ export function moveVisualRows(s, delta, promptStr, cols) {
221
226
  // rows" bug.
222
227
  if (idx > row.start + rowChars.length)
223
228
  idx = row.start + rowChars.length;
224
- if (idx === row.start + rowChars.length && rowChars.length > 0)
225
- idx--;
229
+ // Keep the caret INSIDE the target row — but a caret at the row's END is
230
+ // the natural position for a WIDE last glyph: stepping back one CODE UNIT
231
+ // put it in the MIDDLE of a surrogate pair or before a CJK glyph, so Up
232
+ // from the end of a wrapped CJK line landed a glyph too far left. Only step
233
+ // back when the end is followed by MORE text on the same visual line.
234
+ if (idx === row.start + rowChars.length && rowChars.length > 0) {
235
+ const atWide = idx > 0 && charWidth(s.buf.codePointAt(idx - 1) ?? 32) === 2;
236
+ if (!atWide)
237
+ idx--;
238
+ }
226
239
  return { ...s, cursor: idx };
227
240
  }
228
241
  /** Insert a newline; a trailing «\» before the cursor is removed first. */
@@ -274,7 +287,18 @@ export function historyDown(s) {
274
287
  // The CURRENT line is the operator's, not a history entry: leaving the
275
288
  // history must bring it back. Without this, editing a recalled line and then
276
289
  // pressing Down restored the OLD draft and dropped the edit.
277
- const draft = browsingHistory(s) ? s.histDraft : s.buf;
290
+ //
291
+ // An EDIT of a recalled entry is a scratch copy of THAT slot, not the
292
+ // operator's draft: `typingRun` marks it. Treating it as "not browsing"
293
+ // jumped OUT of the history and dropped the entry the operator was fixing —
294
+ // Down must still move to the NEXT entry.
295
+ // A REAL edit means the buffer no longer matches the recalled entry: the
296
+ // `typingRun` flag is set by the KEY layer, not by `insertText` itself, so
297
+ // the comparison is what is reliable here.
298
+ const recalled = s.history[s.histIndex];
299
+ const editingRecalled = s.histIndex < s.history.length &&
300
+ (s.typingRun || (recalled !== undefined && s.buf !== recalled));
301
+ const draft = browsingHistory(s) && !editingRecalled ? s.histDraft : s.buf;
278
302
  let buf;
279
303
  if (histIndex < s.history.length - 1) {
280
304
  histIndex++;
package/dist/tui/keys.js CHANGED
@@ -197,7 +197,11 @@ export function dispatchKey(input, key, ctx, isVisualEdge) {
197
197
  if (key.escape)
198
198
  return { type: 'escape' };
199
199
  // ---- printable text ----
200
- if (input && input >= ' ') {
200
+ // A raw DEL byte (0x7f) is NOT printable: some terminals hand it over as
201
+ // plain `input` without `key.backspace`, and `input >= ' '` (DEL is 127)
202
+ // inserted it as an invisible control character into the buffer.
203
+ const printable = input && input !== String.fromCharCode(127) && input >= ' ';
204
+ if (printable) {
201
205
  return { type: 'insert', text: input };
202
206
  }
203
207
  return null;
package/dist/tui/store.js CHANGED
@@ -104,7 +104,20 @@ export class TuiStore {
104
104
  this.emit();
105
105
  }
106
106
  emit() {
107
- for (const cb of this.listeners)
108
- cb();
107
+ // A subscriber that throws (a React render error, a bug in the status bar)
108
+ // must not abort the loop: the subscribers AFTER it kept a STALE snapshot,
109
+ // and the exception escaped into the agent code that only printed a line.
110
+ // Notify every subscriber, then re-throw the FIRST error so a genuine bug
111
+ // is still visible where the caller can see it.
112
+ for (const cb of this.listeners) {
113
+ try {
114
+ cb();
115
+ }
116
+ catch {
117
+ // Swallowed ON PURPOSE: a notification sink is a render concern, and
118
+ // the imperative agent path that printed the line must not crash (and
119
+ // must not skip the remaining subscribers).
120
+ }
121
+ }
109
122
  }
110
123
  }
package/dist/ui-health.js CHANGED
@@ -52,6 +52,9 @@ export async function probeUiHealth(page, capabilities = UI_CAPABILITIES) {
52
52
  const present = [];
53
53
  const missing = [];
54
54
  const missingCritical = [];
55
+ // A capability listed TWICE (two selector chains merged, a copy-paste) was
56
+ // reported twice, and the `/doctor` row read `input, input`.
57
+ const seen = new Set();
55
58
  for (const spec of capabilities) {
56
59
  // A capability with NO selectors cannot be probed at all: the provider does
57
60
  // not have that control (z.ai has no toggles). Reporting it as missing made
@@ -59,6 +62,9 @@ export async function probeUiHealth(page, capabilities = UI_CAPABILITIES) {
59
62
  // page. It is N/A, not absent.
60
63
  if (!spec.selectors || spec.selectors.length === 0)
61
64
  continue;
65
+ if (seen.has(spec.capability))
66
+ continue;
67
+ seen.add(spec.capability);
62
68
  let found = false;
63
69
  for (const sel of spec.selectors) {
64
70
  if (await hasAny(page, sel, spec.textRe)) {
package/dist/undo.js CHANGED
@@ -155,6 +155,10 @@ export class UndoStore {
155
155
  }
156
156
  async list(count = 10) {
157
157
  const history = await this._readIndex();
158
+ // `slice(-0)` is `slice(0)` — the WHOLE history, so asking for ZERO rows
159
+ // returned everything. A count below 1 means "no rows".
160
+ if (!(count > 0))
161
+ return [];
158
162
  return history.slice(-count).reverse();
159
163
  }
160
164
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.96.6",
3
+ "version": "2.96.8",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {