zames_pro 2.79.0 → 2.80.0
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 +13 -0
- package/dist/attach-refs.js +9 -1
- package/dist/checkpoint.js +7 -2
- package/dist/commands.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/subagent.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.80.0] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **commands:** keep an unsupplied position as written in a custom command
|
|
15
|
+
- **subagent:** count a subagent run only once it actually starts
|
|
16
|
+
- **attach:** do not cut an inlined @file inside a surrogate pair
|
|
17
|
+
- **checkpoint:** a bare out-of-range rewind index no longer matches a stamp prefix
|
|
18
|
+
- **git:** split a space-separated paths argument in GitAdd
|
|
19
|
+
- **mcp:** keep an MCP tool result inside its size cap
|
|
20
|
+
- **hooks:** never cut a hook denial or notice inside a surrogate pair
|
|
21
|
+
- **sessions:** escape a literal underscore in the session file name
|
|
22
|
+
|
|
10
23
|
## [2.79.0] - 2026-10-19
|
|
11
24
|
|
|
12
25
|
### 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/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/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.0",
|
|
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": {
|