zames_pro 2.79.0 → 2.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/attach-refs.js +9 -1
- package/dist/checkpoint.js +7 -2
- package/dist/commands.js +5 -1
- package/dist/context.js +7 -1
- package/dist/diagnostics.js +5 -1
- package/dist/gitTools.js +47 -3
- package/dist/hooks.js +16 -3
- package/dist/mcp.js +9 -2
- package/dist/sessions.js +4 -1
- package/dist/statusline.js +27 -1
- package/dist/subagent.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.80.1] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **context:** strip a leading BOM before the SKILL.md frontmatter
|
|
15
|
+
- **statusline:** clip a long status badge to one short row
|
|
16
|
+
- **diagnostics:** keep the summary inside maxChars
|
|
17
|
+
|
|
18
|
+
## [2.80.0] - 2026-10-20
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **commands:** keep an unsupplied position as written in a custom command
|
|
23
|
+
- **subagent:** count a subagent run only once it actually starts
|
|
24
|
+
- **attach:** do not cut an inlined @file inside a surrogate pair
|
|
25
|
+
- **checkpoint:** a bare out-of-range rewind index no longer matches a stamp prefix
|
|
26
|
+
- **git:** split a space-separated paths argument in GitAdd
|
|
27
|
+
- **mcp:** keep an MCP tool result inside its size cap
|
|
28
|
+
- **hooks:** never cut a hook denial or notice inside a surrogate pair
|
|
29
|
+
- **sessions:** escape a literal underscore in the session file name
|
|
30
|
+
|
|
10
31
|
## [2.79.0] - 2026-10-19
|
|
11
32
|
|
|
12
33
|
### Fixed
|
package/dist/attach-refs.js
CHANGED
|
@@ -94,7 +94,15 @@ export async function inlineAtRefs(workdir, text) {
|
|
|
94
94
|
let body = raw;
|
|
95
95
|
let note = '';
|
|
96
96
|
if (body.length > AT_REF_FILE_CAP) {
|
|
97
|
-
|
|
97
|
+
let cut = AT_REF_FILE_CAP;
|
|
98
|
+
// Never end the inlined block on a lone HIGH surrogate: the cut is in
|
|
99
|
+
// UTF-16 units, so an emoji / astral CJK char on the boundary would
|
|
100
|
+
// reach the model as a broken code point (same rule as
|
|
101
|
+
// truncateToolResult / capReport / cutHookText).
|
|
102
|
+
const last = body.charCodeAt(cut - 1);
|
|
103
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
104
|
+
cut--;
|
|
105
|
+
body = body.slice(0, cut);
|
|
98
106
|
note = NL + NL + '(truncated at ' + AT_REF_FILE_CAP + ' chars)';
|
|
99
107
|
}
|
|
100
108
|
total += body.length;
|
package/dist/checkpoint.js
CHANGED
|
@@ -50,10 +50,15 @@ export function pickRecord(records, selector) {
|
|
|
50
50
|
return null;
|
|
51
51
|
if (/^\d+$/.test(sel)) {
|
|
52
52
|
const n = Number(sel);
|
|
53
|
-
// A small number is an index (1 = newest).
|
|
54
|
-
// still tried as a stamp prefix below, so `/rewind 1730000` works.
|
|
53
|
+
// A small number is an index (1 = newest).
|
|
55
54
|
if (n >= 1 && n <= records.length)
|
|
56
55
|
return records[n - 1];
|
|
56
|
+
// A stamp is epoch-ms (~13 digits), but older files and tests may hold
|
|
57
|
+
// a SHORT one, so the prefix form stays. What must NOT happen is a bare
|
|
58
|
+
// out-of-range INDEX matching a stamp by its first digit (`/rewind 9`
|
|
59
|
+
// with 5 checkpoints): the prefix form therefore needs TWO digits.
|
|
60
|
+
if (sel.length < 2)
|
|
61
|
+
return null;
|
|
57
62
|
}
|
|
58
63
|
return records.find((r) => String(r.stamp).startsWith(sel)) || null;
|
|
59
64
|
}
|
package/dist/commands.js
CHANGED
|
@@ -773,7 +773,11 @@ function replaceDollarTokens(s, parts, names) {
|
|
|
773
773
|
continue;
|
|
774
774
|
}
|
|
775
775
|
if (isAllDigits(tok)) {
|
|
776
|
-
|
|
776
|
+
const idx = Number(tok) - 1;
|
|
777
|
+
// A position that was NOT supplied stays as written: `$100` in prose
|
|
778
|
+
// is a PRICE, not "position 100", and replacing it with an empty
|
|
779
|
+
// string silently deleted the number from the prompt.
|
|
780
|
+
res += idx >= 0 && idx < parts.length ? parts[idx] : '$' + tok;
|
|
777
781
|
}
|
|
778
782
|
else {
|
|
779
783
|
const idx = names.indexOf(tok);
|
package/dist/context.js
CHANGED
|
@@ -305,7 +305,13 @@ function parseFrontmatter(raw) {
|
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
307
|
export function skillBody(raw) {
|
|
308
|
-
|
|
308
|
+
// A leading BOM (some Windows editors and generators write one) makes the
|
|
309
|
+
// text NOT start with `---`, so the frontmatter check below missed it and
|
|
310
|
+
// the model got the raw YAML mixed into the instructions. Drop it first.
|
|
311
|
+
const bom = String.fromCharCode(0xfeff);
|
|
312
|
+
const norm = String(raw ?? '')
|
|
313
|
+
.replace(new RegExp('^' + bom), '')
|
|
314
|
+
.replace(CRLF_RE, NL);
|
|
309
315
|
if (!norm.startsWith('---'))
|
|
310
316
|
return norm.trim();
|
|
311
317
|
const end = norm.indexOf(NL + '---', 3);
|
package/dist/diagnostics.js
CHANGED
|
@@ -47,10 +47,14 @@ export function summarizeDiagnostics(raw, maxChars) {
|
|
|
47
47
|
const chosen = errLines.length ? errLines : lines;
|
|
48
48
|
let out = chosen.join('\n');
|
|
49
49
|
if (out.length > maxChars) {
|
|
50
|
+
// The truncation MARKER is part of the cap, so the head must leave room
|
|
51
|
+
// for it (appending it to a maxChars-long head exceeded the budget).
|
|
52
|
+
const MARKER = '\n… [diagnostics truncated]';
|
|
53
|
+
const budget = Math.max(0, maxChars - MARKER.length);
|
|
50
54
|
// Never split a surrogate PAIR (emoji / astral CJK): the high half alone
|
|
51
55
|
// is a lone surrogate in the model's context. Same rule as
|
|
52
56
|
// truncateToolResult (agent-loop) and capReport (subagent).
|
|
53
|
-
let cut =
|
|
57
|
+
let cut = budget;
|
|
54
58
|
const c = out.charCodeAt(cut - 1);
|
|
55
59
|
if (c >= 0xd800 && c <= 0xdbff)
|
|
56
60
|
cut--;
|
package/dist/gitTools.js
CHANGED
|
@@ -44,6 +44,39 @@ export function runGitArgs(args, cwd, timeout = 15_000) {
|
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
+
// Split a `paths` argument into individual pathspecs. The parameter is
|
|
48
|
+
// declared as a STRING, so models send "a.txt b.txt" or a quoted path with a
|
|
49
|
+
// space ("my file.txt"); git needs each path as its OWN argument.
|
|
50
|
+
export function splitPaths(input) {
|
|
51
|
+
const out = [];
|
|
52
|
+
const s = String(input || '');
|
|
53
|
+
let cur = '';
|
|
54
|
+
let quote = null;
|
|
55
|
+
for (let i = 0; i < s.length; i++) {
|
|
56
|
+
const ch = s[i];
|
|
57
|
+
if (quote) {
|
|
58
|
+
if (ch === quote)
|
|
59
|
+
quote = null;
|
|
60
|
+
else
|
|
61
|
+
cur += ch;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === '"' || ch === "'") {
|
|
65
|
+
quote = ch;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === ' ' || ch === '\t' || ch === '\n') {
|
|
69
|
+
if (cur)
|
|
70
|
+
out.push(cur);
|
|
71
|
+
cur = '';
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
cur += ch;
|
|
75
|
+
}
|
|
76
|
+
if (cur)
|
|
77
|
+
out.push(cur);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
47
80
|
export async function getGitContext(workdir) {
|
|
48
81
|
const probe = await runGitArgs(['rev-parse', '--is-inside-work-tree'], workdir, 5000);
|
|
49
82
|
if (probe.trim() !== 'true')
|
|
@@ -215,16 +248,27 @@ export function createGitTools(workdir) {
|
|
|
215
248
|
if (err)
|
|
216
249
|
return err;
|
|
217
250
|
if (paths && String(paths).trim()) {
|
|
218
|
-
|
|
251
|
+
// `paths` is declared as a STRING, so the model sends
|
|
252
|
+
// "a.txt b.txt". Passing it as ONE pathspec made git answer
|
|
253
|
+
// `did not match any files` and staged nothing. Split on
|
|
254
|
+
// whitespace, honour quotes (a path may contain a space), and
|
|
255
|
+
// keep the array form working.
|
|
256
|
+
const list = (Array.isArray(paths) ? paths : splitPaths(String(paths)))
|
|
219
257
|
.map((v) => String(v).trim())
|
|
220
258
|
.filter(Boolean);
|
|
221
259
|
for (const p of list) {
|
|
222
260
|
if (p.startsWith('-'))
|
|
223
261
|
return `Error: invalid path: ${p}`;
|
|
224
262
|
}
|
|
225
|
-
|
|
263
|
+
const staged = await runGitArgs(['add', '--', ...list], workdir, 20_000);
|
|
264
|
+
// A successful `git add` prints NOTHING; the shared runner then
|
|
265
|
+
// answers the internal sentinel, which reads as a failure.
|
|
266
|
+
return staged.trim() === NO_OUTPUT_SENTINEL
|
|
267
|
+
? 'Staged: ' + list.join(' ')
|
|
268
|
+
: staged;
|
|
226
269
|
}
|
|
227
|
-
|
|
270
|
+
const all = await runGitArgs(['add', '-A'], workdir, 20_000);
|
|
271
|
+
return all.trim() === NO_OUTPUT_SENTINEL ? 'Staged all changes.' : all;
|
|
228
272
|
},
|
|
229
273
|
},
|
|
230
274
|
{
|
package/dist/hooks.js
CHANGED
|
@@ -18,6 +18,19 @@ const HOOK_DENIAL_CAP = 4000;
|
|
|
18
18
|
// Combined stdout of a PostToolUse / lifecycle hook. It lands in a tool result
|
|
19
19
|
// or in the chat, so it is a notice, not a log dump.
|
|
20
20
|
const HOOK_OUTPUT_CAP = 8000;
|
|
21
|
+
// Cut a hook string by UTF-16 units WITHOUT leaving a lone HIGH surrogate at
|
|
22
|
+
// the end: the text becomes a tool result or a chat notice, and a broken code
|
|
23
|
+
// point there is the same bug truncated elsewhere (same rule as
|
|
24
|
+
// truncateToolResult / capReport / summarizeDiagnostics).
|
|
25
|
+
function cutHookText(s, max) {
|
|
26
|
+
if (s.length <= max)
|
|
27
|
+
return s;
|
|
28
|
+
let cut = Math.max(0, max);
|
|
29
|
+
const last = s.charCodeAt(cut - 1);
|
|
30
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
31
|
+
cut--;
|
|
32
|
+
return s.slice(0, cut);
|
|
33
|
+
}
|
|
21
34
|
export function hooksConfigPath(workdir) {
|
|
22
35
|
return path.join(workdir, '.zames', 'hooks.json');
|
|
23
36
|
}
|
|
@@ -150,7 +163,7 @@ export async function runPreToolUse(hooks, tool, args, workdir) {
|
|
|
150
163
|
const reason = (r.stderr || r.stdout).trim();
|
|
151
164
|
// The denial text becomes a TOOL RESULT the model reads: a hook that
|
|
152
165
|
// dumps a log must not flood the context with one call.
|
|
153
|
-
const capped = reason
|
|
166
|
+
const capped = cutHookText(reason, HOOK_DENIAL_CAP);
|
|
154
167
|
const suffix = reason.length > HOOK_DENIAL_CAP
|
|
155
168
|
? '\n… [denial truncated: ' +
|
|
156
169
|
(reason.length - HOOK_DENIAL_CAP) +
|
|
@@ -198,14 +211,14 @@ async function runEventHooks(hooks, event, workdir, tool, args, result) {
|
|
|
198
211
|
// hundreds of KB and flood the context.
|
|
199
212
|
if (total >= HOOK_OUTPUT_CAP)
|
|
200
213
|
break;
|
|
201
|
-
const piece = out
|
|
214
|
+
const piece = cutHookText(out, HOOK_OUTPUT_CAP - total);
|
|
202
215
|
total += piece.length;
|
|
203
216
|
parts.push(piece);
|
|
204
217
|
}
|
|
205
218
|
const joined = parts.join(String.fromCharCode(10));
|
|
206
219
|
if (joined.length <= HOOK_OUTPUT_CAP)
|
|
207
220
|
return joined;
|
|
208
|
-
return (joined
|
|
221
|
+
return (cutHookText(joined, HOOK_OUTPUT_CAP) +
|
|
209
222
|
String.fromCharCode(10) +
|
|
210
223
|
'… [hook output truncated]');
|
|
211
224
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -183,7 +183,14 @@ export function truncateMcpResult(s, limit = MCP_RESULT_LIMIT) {
|
|
|
183
183
|
const str = String(s);
|
|
184
184
|
if (str.length <= limit)
|
|
185
185
|
return str;
|
|
186
|
-
|
|
186
|
+
// The MARKER is part of the result, so the head must leave room for it:
|
|
187
|
+
// appending it to a limit-long head produced a result WIDER than the cap
|
|
188
|
+
// (the cap exists to bound the context a single MCP call can consume).
|
|
189
|
+
// Reserve the WORST-CASE marker (a 10-digit count), so no result can
|
|
190
|
+
// exceed the cap whatever the omitted length turns out to be.
|
|
191
|
+
const MARKER = '\n[...truncated 9999999999 chars]';
|
|
192
|
+
const budget = Math.max(0, limit - MARKER.length);
|
|
193
|
+
let head = str.slice(0, budget);
|
|
187
194
|
// Never leave a lone HIGH surrogate at the cut: an emoji / astral CJK char
|
|
188
195
|
// sits on two UTF-16 units, and cutting between them hands the model a
|
|
189
196
|
// broken code point (same rule as truncateToolResult / capReport).
|
|
@@ -194,7 +201,7 @@ export function truncateMcpResult(s, limit = MCP_RESULT_LIMIT) {
|
|
|
194
201
|
}
|
|
195
202
|
// Prefer a line boundary so a cut does not split a line in half.
|
|
196
203
|
const nl = head.lastIndexOf(String.fromCharCode(10));
|
|
197
|
-
const cut = nl >
|
|
204
|
+
const cut = nl > budget * 0.5 ? head.slice(0, nl + 1) : head;
|
|
198
205
|
const omitted = str.length - cut.length;
|
|
199
206
|
return cut + `\n[...truncated ${omitted} chars]`;
|
|
200
207
|
}
|
package/dist/sessions.js
CHANGED
|
@@ -55,7 +55,10 @@ function safeName(id) {
|
|
|
55
55
|
const s = String(id);
|
|
56
56
|
let out = '';
|
|
57
57
|
for (const ch of s) {
|
|
58
|
-
|
|
58
|
+
// `_` ITSELF is the escape prefix: leaving a literal underscore in
|
|
59
|
+
// place made the encoding ambiguous (`a/b` -> `a_2fb` collided with the
|
|
60
|
+
// real id `a_2fb`), so one session silently overwrote the other.
|
|
61
|
+
if (/[a-zA-Z0-9.-]/.test(ch) && ch !== '_')
|
|
59
62
|
out += ch;
|
|
60
63
|
else
|
|
61
64
|
out += '_' + ch.codePointAt(0).toString(16).padStart(2, '0');
|
package/dist/statusline.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
// N37 statusLine hook. When `ui.statusLineCommand` is set, zames runs it on a
|
|
3
|
+
// timer with a JSON status object on stdin and shows the FIRST line of its
|
|
4
|
+
// stdout in the status line (like Claude Code). The command is deliberately
|
|
5
|
+
// run OUT of the render path: a per-render spawn would block typing, so the
|
|
6
|
+
// caller runs it on its own interval and only sets the resulting text.
|
|
7
|
+
//
|
|
8
|
+
// Best-effort: a missing/failing command yields an empty string (the badge is
|
|
9
|
+
// hidden), never an error in the UI.
|
|
10
|
+
// Longest status text that still fits a typical terminal row together with
|
|
11
|
+
// the rest of the status bar.
|
|
12
|
+
export const STATUS_BADGE_MAX = 80;
|
|
13
|
+
/** Clip a status badge to STATUS_BADGE_MAX with an ellipsis (never splits a
|
|
14
|
+
* surrogate pair). */
|
|
15
|
+
export function clipBadge(text, max = STATUS_BADGE_MAX) {
|
|
16
|
+
const s = String(text ?? '');
|
|
17
|
+
if (s.length <= max)
|
|
18
|
+
return s;
|
|
19
|
+
let cut = max - 1;
|
|
20
|
+
const c = s.charCodeAt(cut - 1);
|
|
21
|
+
if (c >= 0xd800 && c <= 0xdbff)
|
|
22
|
+
cut--;
|
|
23
|
+
return s.slice(0, cut) + '…';
|
|
24
|
+
}
|
|
2
25
|
/** Run the statusLine command and return its first stdout line (trimmed). */
|
|
3
26
|
export function runStatusLineCommand(workdir, command, info, timeoutMs = 4000) {
|
|
4
27
|
return new Promise((resolve) => {
|
|
@@ -24,7 +47,10 @@ export function runStatusLineCommand(workdir, command, info, timeoutMs = 4000) {
|
|
|
24
47
|
return;
|
|
25
48
|
done = true;
|
|
26
49
|
const first = String(out).split(/\r?\n/)[0].trim();
|
|
27
|
-
|
|
50
|
+
// The status line is rendered as ONE row, unwrapped. A command that
|
|
51
|
+
// prints a long single line must not push 400 raw columns into it
|
|
52
|
+
// (the row overflows and the cursor arithmetic drifts).
|
|
53
|
+
resolve(clipBadge(first));
|
|
28
54
|
};
|
|
29
55
|
// Only the FIRST LINE is shown, so there is no reason to keep collecting a
|
|
30
56
|
// big blob: a chatty command (a JSON dump, `ls -R`) would grow `out`
|
package/dist/subagent.js
CHANGED
|
@@ -76,7 +76,6 @@ export function createSubagentRunner(opts) {
|
|
|
76
76
|
' per task). Do the remaining work yourself with the other tools.',
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
used++;
|
|
80
79
|
// Resolve the parent chat id BEFORE anything else. The runner restores the
|
|
81
80
|
// parent chat in its finally block, and that restore is what keeps the
|
|
82
81
|
// subagent's isolated context from swallowing the parent's turn. If the id
|
|
@@ -98,6 +97,10 @@ export function createSubagentRunner(opts) {
|
|
|
98
97
|
'would be lost). Do the work yourself with the other tools.',
|
|
99
98
|
};
|
|
100
99
|
}
|
|
100
|
+
// Count the run only NOW: every early exit above (budget, unknown
|
|
101
|
+
// parent chat id) returned BEFORE anything ran, and charging those
|
|
102
|
+
// attempts burned the whole budget on runs that never started.
|
|
103
|
+
used++;
|
|
101
104
|
const subTools = buildTools(req.type === 'explore');
|
|
102
105
|
const taskText = subagentPersona(req.type) + '\n\n--- SUB-TASK ---\n' + req.prompt;
|
|
103
106
|
// Save the parent's send hooks: the nested loop OVERWRITES them on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.80.1",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — 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": {
|