zames_pro 2.96.7 → 2.96.9
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 +20 -0
- package/dist/context-limit.js +5 -1
- package/dist/model-command.js +7 -1
- package/dist/resume-command.js +7 -2
- package/dist/sandbox.js +11 -0
- package/dist/shell.js +7 -1
- package/dist/spinner.js +4 -1
- package/dist/statusline.js +9 -0
- package/dist/tui/store.js +15 -2
- package/dist/ui-health.js +6 -0
- package/dist/undo.js +4 -0
- package/package.json +1 -1
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.9] - 2026-11-06
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **statusline:** a backgrounded child no longer holds the badge until the timeout
|
|
15
|
+
- **resume:** a junk session record is skipped instead of crashing /sessions
|
|
16
|
+
- **model:** a model id with a newline is flattened to one line
|
|
17
|
+
- **context-limit:** a non-finite retry budget gives up instead of retrying forever
|
|
18
|
+
- **spinner:** a dots-only phrase keeps a label instead of going empty
|
|
19
|
+
|
|
20
|
+
## [2.96.8] - 2026-11-06
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **ui-health:** report a capability listed twice only once
|
|
25
|
+
- **tui:** a throwing store subscriber no longer starves the others
|
|
26
|
+
- **undo:** list(0) returns no rows instead of the whole history
|
|
27
|
+
- **shell:** a cd inside a nested shell is checked by the sandbox
|
|
28
|
+
- **sandbox:** a dangling symlink cannot let a write escape the root
|
|
29
|
+
|
|
10
30
|
## [2.96.7] - 2026-11-06
|
|
11
31
|
|
|
12
32
|
### Fixed
|
package/dist/context-limit.js
CHANGED
|
@@ -9,7 +9,11 @@ export async function recoverFromContextLimit(deps, original) {
|
|
|
9
9
|
notice(t(keys.giveUp, { provider }));
|
|
10
10
|
return { resend: false, error: original };
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
// A NON-FINITE budget makes `attempt > maxRetries` false forever (`x > NaN`
|
|
13
|
+
// is false), so a chat that can never accept another message was
|
|
14
|
+
// re-compacted on every send with no budget at all. Require a real number.
|
|
15
|
+
const budget = Number.isFinite(maxRetries) && maxRetries >= 0 ? maxRetries : 0;
|
|
16
|
+
if (!recover || attempt > budget)
|
|
13
17
|
return giveUp();
|
|
14
18
|
notice(t(keys.compact, { provider }));
|
|
15
19
|
let recovered = false;
|
package/dist/model-command.js
CHANGED
|
@@ -12,7 +12,13 @@ export function parseModelIntent(provider, arg, t = translate('en')) {
|
|
|
12
12
|
message: t('model.unsupported', { provider }),
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
// A model id is a SINGLE token: the operator's text can carry a newline
|
|
16
|
+
// (a paste, a wrapped line), and it was stored and echoed verbatim — the
|
|
17
|
+
// newline broke the status line and the `/model set` report into fake
|
|
18
|
+
// rows. Collapse whitespace; an id is never allowed to contain a break.
|
|
19
|
+
const model = String(arg ?? '')
|
|
20
|
+
.replace(/\s+/g, ' ')
|
|
21
|
+
.trim();
|
|
16
22
|
return model ? { kind: 'set', model } : { kind: 'list' };
|
|
17
23
|
}
|
|
18
24
|
/** The text for an EMPTY model list (a provider that answered with nothing). */
|
package/dist/resume-command.js
CHANGED
|
@@ -14,8 +14,13 @@ export function filterSessions(sessions, filter) {
|
|
|
14
14
|
.toLowerCase();
|
|
15
15
|
if (!q)
|
|
16
16
|
return sessions.slice();
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Session files are hand-editable and are written by OLDER builds: an array
|
|
18
|
+
// element can be `null`, and a field can be a number. Reading them blindly
|
|
19
|
+
// threw a raw TypeError out of `/sessions`.
|
|
20
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
21
|
+
return sessions.filter((s) => !!s &&
|
|
22
|
+
(text(s.title).toLowerCase().includes(q) ||
|
|
23
|
+
text(s.workdir).toLowerCase().includes(q)));
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* A chat id only exists on the backend that created it, so opening a session
|
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
|
-
|
|
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/spinner.js
CHANGED
|
@@ -15,7 +15,10 @@ export function randomThinkingPhrase(locale = DEFAULT_LOCALE) {
|
|
|
15
15
|
}
|
|
16
16
|
// Strip the trailing ellipsis from the phrase — dots are animated separately.
|
|
17
17
|
export function stripEllipsis(phrase) {
|
|
18
|
-
|
|
18
|
+
const out = phrase.replace(/[.…]+\s*$/, '');
|
|
19
|
+
// A phrase that is ONLY dots (an odd translation) must not become an EMPTY
|
|
20
|
+
// label: the spinner would then animate bare dots with no text at all.
|
|
21
|
+
return out === '' ? phrase.trim() : out;
|
|
19
22
|
}
|
|
20
23
|
// Dot animation shared by BOTH UIs (the ora spinner and the LineEditor): start
|
|
21
24
|
// from an empty string (0 dots), then grow. The width is padded to the maximum
|
package/dist/statusline.js
CHANGED
|
@@ -94,6 +94,15 @@ export function runStatusLineCommand(workdir, command, info, timeoutMs = 4000) {
|
|
|
94
94
|
clearTimeout(timer);
|
|
95
95
|
finish();
|
|
96
96
|
});
|
|
97
|
+
// A NEWLINE already ends the FIRST line, and that is all the badge shows.
|
|
98
|
+
// A backgrounded grandchild keeps the pipe OPEN (`my-status &`), so `end`
|
|
99
|
+
// never fired and the badge waited out the whole timeout on every tick.
|
|
100
|
+
child.stdout?.on('data', (c) => {
|
|
101
|
+
if (String(c).includes(String.fromCharCode(10))) {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
finish();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
97
106
|
child.on('close', () => {
|
|
98
107
|
clearTimeout(timer);
|
|
99
108
|
finish();
|
package/dist/tui/store.js
CHANGED
|
@@ -104,7 +104,20 @@ export class TuiStore {
|
|
|
104
104
|
this.emit();
|
|
105
105
|
}
|
|
106
106
|
emit() {
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
3
|
+
"version": "2.96.9",
|
|
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": {
|