zames_pro 2.78.2 → 2.78.3
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 +17 -0
- package/dist/answer-parse.js +58 -0
- package/dist/backlog.js +4 -1
- package/dist/diagnostics.js +9 -3
- package/dist/gitTools.js +12 -5
- package/dist/input/suggest.js +14 -1
- package/dist/net-capture.js +27 -6
- package/dist/permissions.js +7 -0
- package/dist/shell.js +22 -1
- package/dist/tools.js +11 -3
- package/dist/transcript.js +15 -5
- package/dist/web.js +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.78.3] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **answer-parse:** a report narrating completed steps is not unfinished work
|
|
15
|
+
- **net-capture:** a short complete answer is not a finished-without-answer turn
|
|
16
|
+
- **git:** keep the empty-output sentinel out of the git context
|
|
17
|
+
- **tools:** make Read offset/limit 1-based like every line-numbered tool
|
|
18
|
+
- **shell:** scan a glued cd target (cd../.. escaped the sandbox)
|
|
19
|
+
- **web:** drop an unclosed script/style body in htmlToText
|
|
20
|
+
- **diagnostics:** recognize a coloured/timestamped clean banner
|
|
21
|
+
- **permissions:** fail closed on a rule with an invalid regex
|
|
22
|
+
- **transcript:** sanitize the session name in the file name
|
|
23
|
+
- **backlog:** strip a CR from a note title
|
|
24
|
+
- **shell:** escalate to SIGKILL when a killed job traps SIGTERM
|
|
25
|
+
- **input:** Tab on an empty buffer inserted a guessed command
|
|
26
|
+
|
|
10
27
|
## [2.78.2] - 2026-10-18
|
|
11
28
|
|
|
12
29
|
### Fixed
|
package/dist/answer-parse.js
CHANGED
|
@@ -111,6 +111,64 @@ export function looksLikeUnfinishedWork(text) {
|
|
|
111
111
|
if (/\b(done|finished|completed|готово|выполнено|завершено)\b/i.test(t)) {
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
114
|
+
// A report that NARRATES a completed step ("Tests pass. Now I will run
|
|
115
|
+
// the build.", "I have updated the README.", "Fixed the bug.") recounts
|
|
116
|
+
// work that is ALREADY done. The future-tense "now …" that follows must not
|
|
117
|
+
// turn such a report back into unfinished work: the loop re-asked the model
|
|
118
|
+
// up to 3 times on a complete answer. A bare promise ("Let me run the
|
|
119
|
+
// tests") has NO completed-step clause and is still caught below.
|
|
120
|
+
// The prefix and the verb are matched SEPARATELY (not as one combined
|
|
121
|
+
// regex): with a Cyrillic alternation glued to a `\b`-prefixed group, V8
|
|
122
|
+
// silently fails to find the match. A short English/Russian verb list is
|
|
123
|
+
// checked with plain `includes` on the lowercased text.
|
|
124
|
+
const lower = t.toLowerCase();
|
|
125
|
+
const completedWords = [
|
|
126
|
+
'passed',
|
|
127
|
+
'succeeded',
|
|
128
|
+
'failed',
|
|
129
|
+
'updated',
|
|
130
|
+
'fixed',
|
|
131
|
+
'added',
|
|
132
|
+
'removed',
|
|
133
|
+
'applied',
|
|
134
|
+
'committed',
|
|
135
|
+
'pushed',
|
|
136
|
+
'installed',
|
|
137
|
+
'verified',
|
|
138
|
+
'changed',
|
|
139
|
+
'modified',
|
|
140
|
+
'implemented',
|
|
141
|
+
'reviewed',
|
|
142
|
+
'created',
|
|
143
|
+
'built',
|
|
144
|
+
'pass.',
|
|
145
|
+
'pass,',
|
|
146
|
+
'pass ',
|
|
147
|
+
'have ',
|
|
148
|
+
'has ',
|
|
149
|
+
'проходят',
|
|
150
|
+
'прошёл',
|
|
151
|
+
'прошел',
|
|
152
|
+
'успешн',
|
|
153
|
+
'готов',
|
|
154
|
+
];
|
|
155
|
+
const completedRu = [
|
|
156
|
+
'исправил',
|
|
157
|
+
'добавил',
|
|
158
|
+
'обновил',
|
|
159
|
+
'создал',
|
|
160
|
+
'удалил',
|
|
161
|
+
'применил',
|
|
162
|
+
'запустил',
|
|
163
|
+
'проверил',
|
|
164
|
+
'изменил',
|
|
165
|
+
'сделал',
|
|
166
|
+
'выполнил',
|
|
167
|
+
];
|
|
168
|
+
if (completedWords.some((w) => lower.includes(w)) ||
|
|
169
|
+
completedRu.some((w) => lower.includes(w))) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
114
172
|
// `update`/`summarize`/`explain` are REPORT verbs (`I will update you on the
|
|
115
173
|
// results: …`) — they do not promise a tool call. A colon anywhere in the
|
|
116
174
|
// gap also means the sentence is explaining a fact, not about to act.
|
package/dist/backlog.js
CHANGED
|
@@ -30,7 +30,10 @@ export function parseBacklogNote(note) {
|
|
|
30
30
|
break;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// A note typed on Windows (or pasted from one) carries CRLF line endings;
|
|
34
|
+
// the title would keep the trailing `\r` and the heading would get a stray
|
|
35
|
+
// carriage return.
|
|
36
|
+
const bodyLines = body.split(NL).map((l) => l.replace(/\r$/, ''));
|
|
34
37
|
const title = bodyLines[0];
|
|
35
38
|
const rest = bodyLines.slice(1).join(NL).trim();
|
|
36
39
|
return { priority, title, note: rest || undefined };
|
package/dist/diagnostics.js
CHANGED
|
@@ -28,11 +28,17 @@ export function summarizeDiagnostics(raw, maxChars) {
|
|
|
28
28
|
// old 40-char cutoff — so every edit in watch mode appended a pointless
|
|
29
29
|
// diagnostics block to the tool result. Treat a ZERO-count banner as clean
|
|
30
30
|
// however long it is; a non-zero count still falls through to the lines.
|
|
31
|
-
|
|
31
|
+
// Strip ANSI colour codes and a leading `[12:00:00]`-style timestamp before
|
|
32
|
+
// the clean checks: a watch-mode banner is usually coloured and/or
|
|
33
|
+
// prefixed, and the anchored regexes missed it — every edit then
|
|
34
|
+
// appended a pointless diagnostics block.
|
|
35
|
+
const plain = text.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
36
|
+
const cleanCheck = plain.replace(/^\[[^\]]*\]\s*/, '');
|
|
37
|
+
if (/^(?:found\s+)?0\s+errors?\b/im.test(cleanCheck))
|
|
32
38
|
return null;
|
|
33
|
-
if (/^no errors?\.?$/im.test(
|
|
39
|
+
if (/^no errors?(?: found)?\.?$/im.test(cleanCheck))
|
|
34
40
|
return null;
|
|
35
|
-
if (/^0 problems?\b/im.test(
|
|
41
|
+
if (/^0 problems?\b/im.test(cleanCheck))
|
|
36
42
|
return null;
|
|
37
43
|
// Keep only error-looking lines when we can recognise them, so a long build
|
|
38
44
|
// log collapses to the actual problems. Fall back to the whole output.
|
package/dist/gitTools.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
2
|
const BS = String.fromCharCode(92);
|
|
3
|
+
// What runGitArgs answers for an EMPTY successful run. Callers that parse the
|
|
4
|
+
// output must not mistake it for real content (a clean tree is not "one
|
|
5
|
+
// modified file whose name is this string").
|
|
6
|
+
export const NO_OUTPUT_SENTINEL = '(command produced no output)';
|
|
3
7
|
// Run git with an argument ARRAY (no shell). ALL git invocations go through
|
|
4
8
|
// this: an argument from the model can never be seen by a shell, so $(...),
|
|
5
9
|
// backticks and a leading `-` (option injection) cannot turn a read-only call
|
|
@@ -22,7 +26,7 @@ export function runGitArgs(args, cwd, timeout = 15_000) {
|
|
|
22
26
|
const errStr = (stderr || '').toString();
|
|
23
27
|
if (!err) {
|
|
24
28
|
const combined = (out + errStr).trim();
|
|
25
|
-
resolve(combined ||
|
|
29
|
+
resolve(combined || NO_OUTPUT_SENTINEL);
|
|
26
30
|
return;
|
|
27
31
|
}
|
|
28
32
|
const parts = [];
|
|
@@ -49,7 +53,11 @@ export async function getGitContext(workdir) {
|
|
|
49
53
|
const remoteR = await runGitArgs(['remote'], workdir, 5000);
|
|
50
54
|
const aheadR = await runGitArgs(['rev-list', '--left-right', '--count', '@{upstream}...HEAD'], workdir, 5000);
|
|
51
55
|
const branch = branchR.trim() || '(detached HEAD)';
|
|
52
|
-
|
|
56
|
+
// The shared runner answers an EMPTY successful run with a sentinel. Storing
|
|
57
|
+
// it verbatim told the model the tree had a modified "file" that is really
|
|
58
|
+
// an internal string — every task started with a false dirty-tree premise.
|
|
59
|
+
const statusText = statusR.trim() === NO_OUTPUT_SENTINEL ? '' : statusR;
|
|
60
|
+
const statusLines = statusText
|
|
53
61
|
.split(String.fromCharCode(10))
|
|
54
62
|
.map((l) => l.trimEnd())
|
|
55
63
|
.filter(Boolean);
|
|
@@ -84,7 +92,7 @@ export function formatGitContext(ctx) {
|
|
|
84
92
|
else {
|
|
85
93
|
s += '\n- Remote: (none)';
|
|
86
94
|
}
|
|
87
|
-
if (ctx.changedFiles > 0) {
|
|
95
|
+
if (ctx.changedFiles > 0 && ctx.statusPreview.trim() !== NO_OUTPUT_SENTINEL) {
|
|
88
96
|
s += `\n- Uncommitted changes: ${ctx.changedFiles} file(s)
|
|
89
97
|
`;
|
|
90
98
|
// Choose a fence longer than any backtick run in the preview: a filename
|
|
@@ -234,8 +242,7 @@ export function createGitTools(workdir) {
|
|
|
234
242
|
// We stage everything only if the index is empty (as the description promises).
|
|
235
243
|
const staged = await runGitArgs(['diff', '--cached', '--name-only'], workdir, 10_000);
|
|
236
244
|
const stagedTrimmed = staged.trim();
|
|
237
|
-
const hasStaged = stagedTrimmed !== '' &&
|
|
238
|
-
stagedTrimmed !== '(command produced no output)';
|
|
245
|
+
const hasStaged = stagedTrimmed !== '' && stagedTrimmed !== NO_OUTPUT_SENTINEL;
|
|
239
246
|
const addResult = hasStaged
|
|
240
247
|
? '(index is not empty — add -A skipped)'
|
|
241
248
|
: await runGitArgs(['add', '-A'], workdir, 20_000);
|
package/dist/input/suggest.js
CHANGED
|
@@ -100,10 +100,23 @@ export function completeSlashCommand(buf, sugg, selected) {
|
|
|
100
100
|
if (sugg.length === 1)
|
|
101
101
|
return sugg[0].name + ' ';
|
|
102
102
|
let prefix = sugg[0].name;
|
|
103
|
+
const typed = buf.toLowerCase();
|
|
103
104
|
for (const c of sugg) {
|
|
104
|
-
while (
|
|
105
|
+
while (prefix.length > typed.length &&
|
|
106
|
+
!c.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
105
107
|
prefix = prefix.slice(0, -1);
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
// Stop shrinking at the TYPED text: with an empty buffer the list matches
|
|
111
|
+
// everything, and the shared prefix used to shrink down to a bare "/" (or a
|
|
112
|
+
// common first letter like "/h") — Tab then inserted something the operator
|
|
113
|
+
// never typed. A completion must EXTEND what is already there.
|
|
114
|
+
// With an EMPTY buffer everything matches and the shared prefix collapses
|
|
115
|
+
// to a bare "/" (or a common first letter like "/h"): Tab must not guess a
|
|
116
|
+
// command the operator has not started typing.
|
|
117
|
+
if (typed.length < 2)
|
|
118
|
+
return null;
|
|
119
|
+
if (!prefix.toLowerCase().startsWith(typed))
|
|
120
|
+
return null;
|
|
108
121
|
return prefix.length > buf.length ? prefix : null;
|
|
109
122
|
}
|
package/dist/net-capture.js
CHANGED
|
@@ -169,9 +169,12 @@ export function extractAnswer(body) {
|
|
|
169
169
|
// isFinishedWithoutAnswer === false): the finish loop kept polling for a
|
|
170
170
|
// complete answer and `ask()` gave up only at the askDeadlineMs watchdog — the
|
|
171
171
|
// operator watched a spinner for ~4 minutes with no tool call.
|
|
172
|
-
// A real answer
|
|
173
|
-
//
|
|
174
|
-
|
|
172
|
+
// A real answer is anything non-empty. The one case that must NOT count is a
|
|
173
|
+
// truncated RESPONSE head (an empty string or a bare `{`), which the measure
|
|
174
|
+
// below excludes explicitly.
|
|
175
|
+
const MIN_REAL_ANSWER_CHARS = 1;
|
|
176
|
+
/** A bare truncated head (whitespace or braces only) is not a real answer. */
|
|
177
|
+
const TRUNCATED_HEAD_RE = /^[\s{[]*$/;
|
|
175
178
|
// How many chars of RESPONSE text the body carries, or -1 when there is NO
|
|
176
179
|
// RESPONSE fragment at all. The fragment is SSE/JSON-encoded, so the content
|
|
177
180
|
// may contain `\"` escapes AND raw `"` inside the JSON (the capture is the RAW
|
|
@@ -184,7 +187,25 @@ function responseContentLength(body) {
|
|
|
184
187
|
const b = String(body || '');
|
|
185
188
|
if (!/"type":"RESPONSE"/.test(b))
|
|
186
189
|
return -1;
|
|
187
|
-
|
|
190
|
+
// `extractFromSse` reads only `data:` lines, so a plain-JSON body (or one
|
|
191
|
+
// captured without the SSE framing) measured as 0 even when it carried a
|
|
192
|
+
// real SHORT answer. Count the content strings from the raw body and decode
|
|
193
|
+
// them, so an escaped quote does not inflate the count.
|
|
194
|
+
let len = 0;
|
|
195
|
+
const re = /"type":"RESPONSE"[^}]*?"content":"((?:[^"\\]|\\.)*)"/g;
|
|
196
|
+
let m;
|
|
197
|
+
while ((m = re.exec(b)) !== null) {
|
|
198
|
+
let text = m[1];
|
|
199
|
+
try {
|
|
200
|
+
text = JSON.parse('"' + m[1] + '"');
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
203
|
+
// A bare truncated head (empty, or a lone `{`/`[`) is not an answer; any
|
|
204
|
+
// other non-empty text — however short — is.
|
|
205
|
+
if (!TRUNCATED_HEAD_RE.test(text))
|
|
206
|
+
len += text.length;
|
|
207
|
+
}
|
|
208
|
+
return len;
|
|
188
209
|
}
|
|
189
210
|
// True when the SSE body shows a turn that ENDED (`quasi_status: FINISHED`)
|
|
190
211
|
// but produced NO real answer text — the reasoning (THINK fragment) was
|
|
@@ -209,7 +230,7 @@ export function isFinishedWithoutAnswer(body) {
|
|
|
209
230
|
return false;
|
|
210
231
|
// A RESPONSE fragment with real content means the model DID answer.
|
|
211
232
|
const len = responseContentLength(b);
|
|
212
|
-
if (len >=
|
|
233
|
+
if (len >= MIN_REAL_ANSWER_CHARS)
|
|
213
234
|
return false;
|
|
214
235
|
// No RESPONSE fragment at all (len === -1) or only a truncated head:
|
|
215
236
|
// treat the absence of a real answer as "no answer produced".
|
|
@@ -234,7 +255,7 @@ export function isFinishedWithAnswer(body) {
|
|
|
234
255
|
// A truncated RESPONSE head (a lone `{`) is NOT a complete answer: the
|
|
235
256
|
// finish loop would return it as the model's text and the tool-call parser
|
|
236
257
|
// would fail on it. Require real content (see MIN_FINISHED_ANSWER_CHARS).
|
|
237
|
-
return responseContentLength(b) >=
|
|
258
|
+
return responseContentLength(b) >= MIN_REAL_ANSWER_CHARS;
|
|
238
259
|
}
|
|
239
260
|
// The CONTEXT size (in tokens) DeepSeek reports for the current answer.
|
|
240
261
|
//
|
package/dist/permissions.js
CHANGED
|
@@ -124,6 +124,13 @@ export function decidePermission(policy, tool, args) {
|
|
|
124
124
|
}
|
|
125
125
|
const paths = collectPathCandidates(tool, args);
|
|
126
126
|
for (const r of policy.rules) {
|
|
127
|
+
// An UNPARSABLE regex in any field of the rule makes the rule fail
|
|
128
|
+
// CLOSED: skipping it silently (the old behavior) turned a deny rule with
|
|
129
|
+
// a typo (`^(Bash$`) into "allow everything it was meant to block".
|
|
130
|
+
const bad = [r.tool, r.command, r.path].find((src) => src !== undefined && safeRegex(src) === null);
|
|
131
|
+
if (bad !== undefined) {
|
|
132
|
+
return { action: 'deny', reason: 'rule has an invalid regex: ' + bad };
|
|
133
|
+
}
|
|
127
134
|
if (r.tool) {
|
|
128
135
|
const re = safeRegex(r.tool);
|
|
129
136
|
if (!re || !re.test(tool))
|
package/dist/shell.js
CHANGED
|
@@ -25,7 +25,12 @@ export function assertCommandInsideRoot(root, command) {
|
|
|
25
25
|
}
|
|
26
26
|
// `(` and backticks open a subshell / command substitution where a `cd` is
|
|
27
27
|
// still executed; include them in the prefix so the scan sees it.
|
|
28
|
-
|
|
28
|
+
//
|
|
29
|
+
// `\s*` (not `\s+`): a shell accepts `cd` GLUED to its target — `cd..`,
|
|
30
|
+
// `cd$HOME`, `cd../..` — and runs it as a real directory change. Requiring
|
|
31
|
+
// whitespace skipped those forms, so a glued `cd../..` walked out of the
|
|
32
|
+
// root with the guard never firing.
|
|
33
|
+
const cdRe = /(?:^|[;&|(\s`])(?:cd|pushd)\s*([^;&|)]+)/gi;
|
|
29
34
|
let m;
|
|
30
35
|
while ((m = cdRe.exec(cmd))) {
|
|
31
36
|
const raw = m[1].trim().replace(/^["']|["']$/g, '');
|
|
@@ -217,6 +222,22 @@ export function killBackground(id) {
|
|
|
217
222
|
return `Failed to kill ${id}: ${e.message}`;
|
|
218
223
|
}
|
|
219
224
|
BG_PROCS.delete(id);
|
|
225
|
+
// SIGTERM is only a REQUEST. A shell that traps it (a wrapper script, a
|
|
226
|
+
// daemon that cleans up on TERM) ignores the signal, and the old code then
|
|
227
|
+
// forgot the id and reported "Killed" while the process kept running as an
|
|
228
|
+
// orphan. Give it a moment, then SIGKILL the group (the shell's own trap
|
|
229
|
+
// cannot block that). Detached from the caller so killBackground stays sync.
|
|
230
|
+
const pid = p.child.pid;
|
|
231
|
+
if (process.platform !== 'win32' && pid) {
|
|
232
|
+
const t = setTimeout(() => {
|
|
233
|
+
try {
|
|
234
|
+
process.kill(-pid, 'SIGKILL');
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
}, 1500);
|
|
238
|
+
if (typeof t.unref === 'function')
|
|
239
|
+
t.unref();
|
|
240
|
+
}
|
|
220
241
|
return `Killed background process ${id}.`;
|
|
221
242
|
}
|
|
222
243
|
/** Kill every background process — called on exit so no orphan survives. */
|
package/dist/tools.js
CHANGED
|
@@ -71,7 +71,8 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
71
71
|
const baseTools = [
|
|
72
72
|
{
|
|
73
73
|
name: 'Read',
|
|
74
|
-
description: 'Read a file. Optional: offset and limit (lines
|
|
74
|
+
description: 'Read a file. Optional: offset and limit (lines, BOTH 1-BASED: ' +
|
|
75
|
+
'offset=5 starts at line 5, limit=10 reads ten lines). ' +
|
|
75
76
|
'numbered=true adds cat -n style line numbers (1-based) for reference ' +
|
|
76
77
|
'only — do NOT copy the numbers into Edit old_string. ' +
|
|
77
78
|
'Lines longer than 2000 chars are truncated. ' +
|
|
@@ -117,8 +118,15 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
117
118
|
if (content === '')
|
|
118
119
|
return '(file is empty)';
|
|
119
120
|
const lines = content.split('\n');
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
// `offset`/`limit` are 1-BASED, like sed / `git blame -L` and the
|
|
122
|
+
// `numbered=true` output this tool prints. Treating them as 0-based
|
|
123
|
+
// array slices silently shifted every window by one line and made
|
|
124
|
+
// `offset: 1` skip the first line. A non-positive or non-numeric
|
|
125
|
+
// offset means "from the start".
|
|
126
|
+
const off = Math.max(1, Math.floor(Number(offset) || 1));
|
|
127
|
+
const start = off - 1;
|
|
128
|
+
const lim = Math.floor(Number(limit));
|
|
129
|
+
const end = Number.isFinite(lim) && lim > 0 ? start + lim : lines.length;
|
|
122
130
|
const slice = lines.slice(start, end);
|
|
123
131
|
const clip = (l) => l.length > 2000 ? clipLine(l, 2000) + ' …[line truncated]' : l;
|
|
124
132
|
if (!numbered)
|
package/dist/transcript.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
function transcriptErrorLine(e) {
|
|
4
|
+
return `transcript: failed to create file: ${e.message}`;
|
|
5
|
+
}
|
|
3
6
|
export class Transcript {
|
|
4
7
|
enabled;
|
|
5
8
|
file;
|
|
@@ -17,7 +20,14 @@ export class Transcript {
|
|
|
17
20
|
try {
|
|
18
21
|
fs.mkdirSync(dir, { recursive: true });
|
|
19
22
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
20
|
-
|
|
23
|
+
// `sessionName` is the LABEL of the working directory (dirLabel), so a
|
|
24
|
+
// name like `a/b` or `C:\x` would nest a subdirectory or break the
|
|
25
|
+
// path outright: the stream then failed with ENOENT and no transcript
|
|
26
|
+
// at all was written. Keep the name a plain file-name fragment.
|
|
27
|
+
const safeName = String(sessionName)
|
|
28
|
+
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_')
|
|
29
|
+
.trim() || 'session';
|
|
30
|
+
this.file = path.join(dir, `${safeName}-${stamp}.jsonl`);
|
|
21
31
|
this.stream = fs.createWriteStream(this.file, { flags: 'a' });
|
|
22
32
|
// Write errors (disk full, file deleted, etc.) arrive as an 'error'
|
|
23
33
|
// event; without a listener this is an uncaught exception.
|
|
@@ -27,10 +37,12 @@ export class Transcript {
|
|
|
27
37
|
});
|
|
28
38
|
}
|
|
29
39
|
catch (e) {
|
|
30
|
-
console.error(
|
|
40
|
+
console.error(transcriptErrorLine(e));
|
|
31
41
|
this.enabled = false;
|
|
32
42
|
}
|
|
33
43
|
}
|
|
44
|
+
/** Number of writes that hit backpressure (write() === false). */
|
|
45
|
+
backpressure = 0;
|
|
34
46
|
log(type, data = {}) {
|
|
35
47
|
const entry = {
|
|
36
48
|
ts: new Date().toISOString(),
|
|
@@ -60,8 +72,6 @@ export class Transcript {
|
|
|
60
72
|
}
|
|
61
73
|
catch { }
|
|
62
74
|
}
|
|
63
|
-
/** Number of writes that hit backpressure (write() === false). */
|
|
64
|
-
backpressure = 0;
|
|
65
75
|
// Flush the stream and resolve once the data reached the file. The callers
|
|
66
76
|
// run on process.exit paths (one-shot --task, browser error, clean exit), and
|
|
67
77
|
// end() without awaiting the callback lets the process die before the buffer
|
|
@@ -89,6 +99,6 @@ export class Transcript {
|
|
|
89
99
|
return null;
|
|
90
100
|
return ('transcript: ' +
|
|
91
101
|
this.backpressure +
|
|
92
|
-
' write(s) hit backpressure (slow disk?)
|
|
102
|
+
' write(s) hit backpressure (slow disk?)― events may be delayed');
|
|
93
103
|
}
|
|
94
104
|
}
|
package/dist/web.js
CHANGED
|
@@ -38,10 +38,17 @@ export function htmlToText(html) {
|
|
|
38
38
|
pendingHighSurrogate = null;
|
|
39
39
|
// Remove blocks we don't need
|
|
40
40
|
let s = html
|
|
41
|
+
// A tag that never CLOSES (a truncated body, or HTML the regex cannot
|
|
42
|
+
// pair) used to leak its raw JS/CSS into the text: also drop everything
|
|
43
|
+
// from an unclosed opening tag to the end of the document.
|
|
41
44
|
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
45
|
+
.replace(/<script\b[\s\S]*$/i, '')
|
|
42
46
|
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
47
|
+
.replace(/<style\b[\s\S]*$/i, '')
|
|
43
48
|
.replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
|
|
49
|
+
.replace(/<noscript\b[\s\S]*$/i, '')
|
|
44
50
|
.replace(/<svg[\s\S]*?<\/svg>/gi, '')
|
|
51
|
+
.replace(/<svg\b[\s\S]*$/i, '')
|
|
45
52
|
.replace(/<!--[\s\S]*?-->/g, '');
|
|
46
53
|
// Links: <a href="URL">text</a> → text (URL)
|
|
47
54
|
s = s.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.78.
|
|
3
|
+
"version": "2.78.3",
|
|
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": {
|