zames_pro 2.71.0 → 2.72.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 +46 -0
- package/dist/agent-loop.js +253 -924
- package/dist/answer-parse.js +765 -0
- package/dist/browser-chats.js +161 -0
- package/dist/browser-dom.js +416 -0
- package/dist/browser-pure.js +249 -0
- package/dist/browser.js +75 -713
- package/dist/changelog.js +29 -0
- package/dist/cli-args.js +48 -0
- package/dist/compact.js +9 -0
- package/dist/config.js +6 -0
- package/dist/diagnostics.js +71 -0
- package/dist/hooks.js +30 -2
- package/dist/i18n/common.js +4 -0
- package/dist/index.js +49 -24
- package/dist/input.js +25 -1
- package/dist/run-task.js +31 -0
- package/dist/shell.js +102 -1
- package/dist/statusline.js +52 -0
- package/dist/subagent.js +40 -3
- package/dist/tools.js +81 -8
- package/package.json +10 -8
- package/scripts/changelog-draft.mts +38 -1
- package/scripts/{eslint-ts6.mjs → eslint-ts6.mts} +11 -4
- package/scripts/release.mts +36 -7
- package/scripts/{render-demo.mjs → render-demo.mts} +12 -11
- package/scripts/{self-smoke.mjs → self-smoke.mts} +41 -4
- /package/scripts/{clean-dist.mjs → clean-dist.mts} +0 -0
- /package/scripts/{coverage-gate.mjs → coverage-gate.mts} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/** Run the statusLine command and return its first stdout line (trimmed). */
|
|
3
|
+
export function runStatusLineCommand(workdir, command, info, timeoutMs = 4000) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
let child;
|
|
6
|
+
try {
|
|
7
|
+
child = spawn(command, {
|
|
8
|
+
cwd: workdir,
|
|
9
|
+
shell: process.platform === 'win32'
|
|
10
|
+
? process.env.ComSpec || true
|
|
11
|
+
: '/bin/sh',
|
|
12
|
+
windowsHide: true,
|
|
13
|
+
env: { ...process.env },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
resolve('');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let out = '';
|
|
21
|
+
let done = false;
|
|
22
|
+
const finish = () => {
|
|
23
|
+
if (done)
|
|
24
|
+
return;
|
|
25
|
+
done = true;
|
|
26
|
+
const first = String(out).split(/\r?\n/)[0].trim();
|
|
27
|
+
resolve(first);
|
|
28
|
+
};
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
try {
|
|
31
|
+
child.kill();
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
finish();
|
|
35
|
+
}, timeoutMs);
|
|
36
|
+
child.stdout?.on('data', (c) => {
|
|
37
|
+
out += c.toString();
|
|
38
|
+
});
|
|
39
|
+
child.on('error', () => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
finish();
|
|
42
|
+
});
|
|
43
|
+
child.on('close', () => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
finish();
|
|
46
|
+
});
|
|
47
|
+
if (child.stdin) {
|
|
48
|
+
child.stdin.on('error', () => { });
|
|
49
|
+
child.stdin.end(JSON.stringify(info) + String.fromCharCode(10));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
package/dist/subagent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadHooks, runLifecycleHooks } from './hooks.js';
|
|
1
2
|
// Subagent runner (BACKLOG N36). A `Task` tool call from the model is handled
|
|
2
3
|
// by the agent loop as a SEAM (like onAutoCompact): it calls back into the
|
|
3
4
|
// caller, which owns the whole lifecycle. This module builds that callback.
|
|
@@ -13,6 +14,19 @@
|
|
|
13
14
|
// browser.ts, which owns the live Playwright context) — out of scope here.
|
|
14
15
|
/** Hard cap on iterations for one subagent run. */
|
|
15
16
|
export const SUBAGENT_MAX_ITER = 30;
|
|
17
|
+
// The report is fed back into the PARENT chat as a tool result, so a runaway
|
|
18
|
+
// subagent must not be able to blow up the parent's context with one answer.
|
|
19
|
+
// The tail is trimmed with a marker, not silently dropped.
|
|
20
|
+
export const SUBAGENT_REPORT_CAP = 20_000;
|
|
21
|
+
/** Cap a subagent report before it becomes a parent-context tool result. */
|
|
22
|
+
export function capReport(text, cap = SUBAGENT_REPORT_CAP) {
|
|
23
|
+
if (text.length <= cap)
|
|
24
|
+
return text;
|
|
25
|
+
return (text.slice(0, cap) +
|
|
26
|
+
'\n… [report truncated: ' +
|
|
27
|
+
(text.length - cap) +
|
|
28
|
+
' chars omitted]');
|
|
29
|
+
}
|
|
16
30
|
/**
|
|
17
31
|
* The subagent persona, prepended to the task message. It is deliberately NOT
|
|
18
32
|
* injected as a replacement system prompt: the nested runAgentLoop builds the
|
|
@@ -43,7 +57,7 @@ export function subagentPersona(type) {
|
|
|
43
57
|
* returns only the report.
|
|
44
58
|
*/
|
|
45
59
|
export function createSubagentRunner(opts) {
|
|
46
|
-
const { browser, workdir, locale, runAgentLoop, buildTools, transcript = null, maxSubagents, askDeadlineMs, maxAfterToolRetries,
|
|
60
|
+
const { browser, workdir, locale, runAgentLoop, buildTools, transcript = null, maxSubagents, askDeadlineMs, maxAfterToolRetries, onAskPermission, } = opts;
|
|
47
61
|
let used = 0;
|
|
48
62
|
return async (req) => {
|
|
49
63
|
if (used >= maxSubagents) {
|
|
@@ -84,11 +98,24 @@ export function createSubagentRunner(opts) {
|
|
|
84
98
|
locale,
|
|
85
99
|
askDeadlineMs,
|
|
86
100
|
maxAfterToolRetries,
|
|
87
|
-
|
|
101
|
+
// A subagent NEVER touches BACKLOG.md, even in the operator's dev
|
|
102
|
+
// mode: its sub-task is scoped, and a general subagent appending
|
|
103
|
+
// "improvement" notes would be unrelated, unsupervised edits.
|
|
104
|
+
selfImprovement: false,
|
|
88
105
|
// No recursion: the subagent cannot spawn its own subagents.
|
|
89
106
|
onSubagent: null,
|
|
90
107
|
// Do not persist the subagent chat as a session.
|
|
91
108
|
onChatReady: () => { },
|
|
109
|
+
// Keep the PARENT's UI alive during the nested run. The nested loop
|
|
110
|
+
// OVERWRITES browser.onSendStart/… with its own callbacks; without
|
|
111
|
+
// forwarding the parent's hooks the spinner and the throttle countdown
|
|
112
|
+
// would go blank for the whole subagent run (a long subagent looked
|
|
113
|
+
// like a frozen, dead agent). The nested loop, not this runner, owns
|
|
114
|
+
// the assignment, so we hand it the functions it should install.
|
|
115
|
+
onThinking: savedHooks.onSendStart ?? undefined,
|
|
116
|
+
onSendPause: savedHooks.onSendPause ?? undefined,
|
|
117
|
+
onSendState: savedHooks.onSendState ?? undefined,
|
|
118
|
+
onNotice: savedHooks.onNotice ?? undefined,
|
|
92
119
|
onAskPermission: onAskPermission
|
|
93
120
|
? (info) => onAskPermission({
|
|
94
121
|
tool: info.tool,
|
|
@@ -112,8 +139,18 @@ export function createSubagentRunner(opts) {
|
|
|
112
139
|
browser.onSendState = savedHooks.onSendState;
|
|
113
140
|
browser.onNotice = savedHooks.onNotice;
|
|
114
141
|
}
|
|
142
|
+
// SubagentStop hook (N32): fires after the nested loop finishes, so a hook
|
|
143
|
+
// can clean up or notify. Best-effort; its stdout is appended to the report.
|
|
144
|
+
let hookOut = '';
|
|
145
|
+
try {
|
|
146
|
+
hookOut = await runLifecycleHooks(loadHooks(workdir), 'SubagentStop', workdir, { type: req.type, ok: failed ? 'false' : 'true' });
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
115
149
|
if (failed)
|
|
116
150
|
return { ok: false, text: failed };
|
|
117
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
text: capReport(hookOut ? report + '\n\n' + hookOut : report),
|
|
154
|
+
};
|
|
118
155
|
};
|
|
119
156
|
}
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { assertCommandInsideRoot, runFile, runShell } from './shell.js';
|
|
3
|
+
import { assertCommandInsideRoot, runFile, runShell, startBackground, pollBackground, killBackground, } from './shell.js';
|
|
4
|
+
import { isImageName, guessMime, formatSize } from './attachments.js';
|
|
4
5
|
import { safePath } from './sandbox.js';
|
|
5
6
|
import { createGitTools } from './gitTools.js';
|
|
6
7
|
import { createWebTools } from './web.js';
|
|
@@ -26,6 +27,17 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
26
27
|
// that omits `path` (or sends it as null) becomes String(undefined) ===
|
|
27
28
|
// "undefined" and the tool silently creates a file literally named
|
|
28
29
|
// "undefined" in the working directory. `req()` rejects that up front.
|
|
30
|
+
// A NUL byte is the classic binary signal (text files essentially never
|
|
31
|
+
// contain one). We only scan the head: a huge archive must not be walked
|
|
32
|
+
// byte-by-byte just to decide it is not text.
|
|
33
|
+
const isBinaryBuffer = (buf) => {
|
|
34
|
+
const n = Math.min(buf.length, 8000);
|
|
35
|
+
for (let i = 0; i < n; i++) {
|
|
36
|
+
if (buf[i] === 0)
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
29
41
|
const req = (v, name) => {
|
|
30
42
|
if (v === undefined || v === null) {
|
|
31
43
|
throw new Error(`Missing required argument: ${name}`);
|
|
@@ -42,7 +54,10 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
42
54
|
description: 'Read a file. Optional: offset and limit (lines). ' +
|
|
43
55
|
'numbered=true adds cat -n style line numbers (1-based) for reference ' +
|
|
44
56
|
'only — do NOT copy the numbers into Edit old_string. ' +
|
|
45
|
-
'Lines longer than 2000 chars are truncated.'
|
|
57
|
+
'Lines longer than 2000 chars are truncated. ' +
|
|
58
|
+
'Binary files (images, archives) are not decoded: an image returns its ' +
|
|
59
|
+
'path/size as a marker (the operator can paste it to attach it), and ' +
|
|
60
|
+
'other binaries return a short hex header instead of mojibake.',
|
|
46
61
|
parameters: {
|
|
47
62
|
path: 'string',
|
|
48
63
|
offset: 'number?',
|
|
@@ -51,7 +66,34 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
51
66
|
},
|
|
52
67
|
fn: async ({ path: p, offset, limit, numbered }) => {
|
|
53
68
|
const file = safe(req(p, 'path'));
|
|
54
|
-
|
|
69
|
+
// Binary guard (N35): reading an image/archive as utf-8 produced
|
|
70
|
+
// mojibake the model tried to reason about. Detect a binary buffer
|
|
71
|
+
// BEFORE decoding and answer with useful metadata instead.
|
|
72
|
+
const buf = await fs.readFile(file);
|
|
73
|
+
if (isImageName(file)) {
|
|
74
|
+
return ('[image: ' +
|
|
75
|
+
p +
|
|
76
|
+
'] (' +
|
|
77
|
+
guessMime(file) +
|
|
78
|
+
', ' +
|
|
79
|
+
formatSize(buf.length) +
|
|
80
|
+
') — this is an image; its pixels cannot be read as text. To have ' +
|
|
81
|
+
'the model SEE it, the operator can paste/drag the file into the ' +
|
|
82
|
+
'chat (it becomes an [image#N] attachment).');
|
|
83
|
+
}
|
|
84
|
+
if (isBinaryBuffer(buf)) {
|
|
85
|
+
const head = buf.subarray(0, 16);
|
|
86
|
+
return ('(binary file: ' +
|
|
87
|
+
p +
|
|
88
|
+
', ' +
|
|
89
|
+
formatSize(buf.length) +
|
|
90
|
+
', ' +
|
|
91
|
+
guessMime(file) +
|
|
92
|
+
')\nFirst bytes (hex): ' +
|
|
93
|
+
Buffer.from(head).toString('hex') +
|
|
94
|
+
'\nUse Bash (e.g. `file`, `xxd`, `unzip -l`) to inspect it.');
|
|
95
|
+
}
|
|
96
|
+
const content = buf.toString('utf-8');
|
|
55
97
|
if (content === '')
|
|
56
98
|
return '(file is empty)';
|
|
57
99
|
const lines = content.split('\n');
|
|
@@ -126,13 +168,43 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
126
168
|
{
|
|
127
169
|
name: 'Bash',
|
|
128
170
|
description: 'Run a shell command in the working directory (cmd.exe on Windows, sh on Linux/macOS). ' +
|
|
129
|
-
'Do not use for long-running processes (servers) — they will hit the timeout. ' +
|
|
130
171
|
'Do not use for commands that require interactive input. ' +
|
|
172
|
+
'For a long-running process (dev server, watch, long build) set ' +
|
|
173
|
+
'run_in_background=true: it starts the command and returns an id ' +
|
|
174
|
+
'immediately; poll its output with BashOutput and stop it with BashOutput kill. ' +
|
|
131
175
|
'For git use the Git* tools. For the web use WebFetch / WebSearch.',
|
|
132
|
-
parameters: {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
parameters: {
|
|
177
|
+
command: 'string',
|
|
178
|
+
timeout: 'number?',
|
|
179
|
+
run_in_background: 'boolean?',
|
|
180
|
+
},
|
|
181
|
+
fn: async ({ command, timeout, run_in_background }, ctx) => {
|
|
182
|
+
const cmd = req(command, 'command');
|
|
183
|
+
assertCommandInsideRoot(root, cmd);
|
|
184
|
+
if (run_in_background) {
|
|
185
|
+
const id = startBackground(workdir, cmd);
|
|
186
|
+
return ('Started in background with id ' +
|
|
187
|
+
id +
|
|
188
|
+
'. Poll it with BashOutput (id="' +
|
|
189
|
+
id +
|
|
190
|
+
'") and stop it with BashOutput (id="' +
|
|
191
|
+
id +
|
|
192
|
+
'", kill=true).');
|
|
193
|
+
}
|
|
194
|
+
return runShell(workdir, cmd, timeout, ctx?.signal);
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'BashOutput',
|
|
199
|
+
description: 'Read the accumulated stdout+stderr of a process started with Bash ' +
|
|
200
|
+
'run_in_background=true, and optionally kill it. id — the id returned ' +
|
|
201
|
+
'by Bash. kill=true terminates the process (and its children).',
|
|
202
|
+
parameters: { id: 'string', kill: 'boolean?' },
|
|
203
|
+
fn: async ({ id, kill }) => {
|
|
204
|
+
const pid = req(id, 'id');
|
|
205
|
+
if (kill)
|
|
206
|
+
return killBackground(pid);
|
|
207
|
+
return pollBackground(pid);
|
|
136
208
|
},
|
|
137
209
|
},
|
|
138
210
|
{
|
|
@@ -243,6 +315,7 @@ export const MUTATING_TOOLS = new Set([
|
|
|
243
315
|
'MultiEdit',
|
|
244
316
|
'ApplyPatch',
|
|
245
317
|
'Bash',
|
|
318
|
+
'BashOutput',
|
|
246
319
|
'GitAdd',
|
|
247
320
|
'GitCommit',
|
|
248
321
|
'GitPush',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.72.1",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — 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": {
|
|
@@ -17,21 +17,23 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node ./dist/index.js",
|
|
19
19
|
"dev": "ZAMES_NET_DEBUG=1 tsx ./src/index.ts --dev --headed",
|
|
20
|
-
"clean": "
|
|
21
|
-
"build": "
|
|
22
|
-
"coverage:gate": "
|
|
20
|
+
"clean": "tsx scripts/clean-dist.mts",
|
|
21
|
+
"build": "tsx scripts/clean-dist.mts && tsc -p tsconfig.build.json",
|
|
22
|
+
"coverage:gate": "tsx scripts/coverage-gate.mts",
|
|
23
23
|
"changelog:draft": "tsx scripts/changelog-draft.mts",
|
|
24
|
+
"changelog:sync": "tsx scripts/changelog-draft.mts --write",
|
|
25
|
+
"changelog:check": "tsx scripts/changelog-draft.mts --check",
|
|
24
26
|
"release": "tsx scripts/release.mts",
|
|
25
27
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"lint": "
|
|
27
|
-
"lint:fix": "
|
|
28
|
+
"lint": "tsx scripts/eslint-ts6.mts",
|
|
29
|
+
"lint:fix": "tsx scripts/eslint-ts6.mts --fix",
|
|
28
30
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json,md}\"",
|
|
29
31
|
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
30
32
|
"prepublishOnly": "npm run build:readme && npm run build",
|
|
31
33
|
"test": "tsx --test test/*.test.ts",
|
|
32
34
|
"test:watch": "tsx --test --watch test/*.test.ts",
|
|
33
|
-
"self-smoke": "tsx scripts/self-smoke.
|
|
34
|
-
"render-demo": "
|
|
35
|
+
"self-smoke": "tsx scripts/self-smoke.mts",
|
|
36
|
+
"render-demo": "tsx scripts/render-demo.mts",
|
|
35
37
|
"build:readme": "tsx scripts/build-readme.mts",
|
|
36
38
|
"postinstall": "node scripts/postinstall.mjs",
|
|
37
39
|
"prepare": "husky"
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
2
5
|
import {
|
|
3
6
|
parseConventionalCommit,
|
|
4
7
|
groupCommits,
|
|
5
8
|
renderChangelogDraft,
|
|
9
|
+
syncUnreleased,
|
|
6
10
|
} from '../src/changelog.js'
|
|
7
11
|
|
|
8
12
|
// Draft a CHANGELOG section from conventional commits since a ref (BACKLOG
|
|
@@ -13,6 +17,8 @@ import {
|
|
|
13
17
|
// Usage:
|
|
14
18
|
// npm run changelog:draft # since the latest tag, or an explicit ref
|
|
15
19
|
// npm run changelog:draft -- v2.63.0
|
|
20
|
+
// npm run changelog:draft -- --write # update the Unreleased section in place
|
|
21
|
+
// npm run changelog:draft -- --check # exit 1 if Unreleased is out of date
|
|
16
22
|
|
|
17
23
|
function git(args: string[]): string {
|
|
18
24
|
const r = spawnSync('git', args, { encoding: 'utf-8' })
|
|
@@ -20,7 +26,10 @@ function git(args: string[]): string {
|
|
|
20
26
|
return (r.stdout || '').toString()
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
const
|
|
29
|
+
const argv = process.argv.slice(2)
|
|
30
|
+
const write = argv.includes('--write')
|
|
31
|
+
const check = argv.includes('--check')
|
|
32
|
+
const argRef = argv.find((a) => !a.startsWith('-'))
|
|
24
33
|
|
|
25
34
|
// Resolve the start ref: an explicit arg, otherwise the latest tag,
|
|
26
35
|
// otherwise the root commit.
|
|
@@ -46,6 +55,34 @@ const parsed = subjects
|
|
|
46
55
|
.filter((c): c is NonNullable<typeof c> => c !== null)
|
|
47
56
|
const body = renderChangelogDraft(groupCommits(parsed))
|
|
48
57
|
|
|
58
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
59
|
+
const changelogPath = path.join(root, 'CHANGELOG.md')
|
|
60
|
+
|
|
61
|
+
// D2: sync the `## [Unreleased]` section in CHANGELOG.md from the conventional
|
|
62
|
+
// commits since the last tag. `--write` edits the file; `--check` fails when
|
|
63
|
+
// the committed section is out of date (for CI); neither just prints the body.
|
|
64
|
+
if (write || check) {
|
|
65
|
+
const onDisk = fs.readFileSync(changelogPath, 'utf-8')
|
|
66
|
+
const nextText = syncUnreleased(onDisk, body)
|
|
67
|
+
if (write) {
|
|
68
|
+
if (nextText !== onDisk) {
|
|
69
|
+
fs.writeFileSync(changelogPath, nextText)
|
|
70
|
+
console.log('changelog: updated the Unreleased section')
|
|
71
|
+
} else {
|
|
72
|
+
console.log('changelog: Unreleased already up to date')
|
|
73
|
+
}
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
if (nextText !== onDisk) {
|
|
77
|
+
console.error(
|
|
78
|
+
'changelog: the Unreleased section is out of date — run `npm run changelog:sync`',
|
|
79
|
+
)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
console.log('changelog: Unreleased is up to date')
|
|
83
|
+
process.exit(0)
|
|
84
|
+
}
|
|
85
|
+
|
|
49
86
|
if (!body) {
|
|
50
87
|
console.error('no conventional commits since ' + ref)
|
|
51
88
|
process.exit(0)
|
|
@@ -11,6 +11,12 @@ import Module from 'module'
|
|
|
11
11
|
import path from 'path'
|
|
12
12
|
import { fileURLToPath } from 'url'
|
|
13
13
|
|
|
14
|
+
// Module._resolveFilename is a private Node field (not in @types/node), so it
|
|
15
|
+
// is reached through a small structural type instead of `any`.
|
|
16
|
+
type ModuleWithResolve = typeof Module & {
|
|
17
|
+
_resolveFilename: (request: string, ...rest: unknown[]) => string
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
const require = createRequire(import.meta.url)
|
|
15
21
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
16
22
|
const ts6 = path.join(here, '..', 'node_modules', 'typescript-6')
|
|
@@ -26,16 +32,17 @@ try {
|
|
|
26
32
|
|
|
27
33
|
// Patch Module._resolveFilename so any require('typescript') resolves to the
|
|
28
34
|
// TS 6 package instead of the TS 7 the project depends on.
|
|
29
|
-
const
|
|
30
|
-
|
|
35
|
+
const M = Module as ModuleWithResolve
|
|
36
|
+
const origResolve = M._resolveFilename
|
|
37
|
+
M._resolveFilename = (request: string, ...rest: unknown[]) => {
|
|
31
38
|
if (request === 'typescript') {
|
|
32
39
|
try {
|
|
33
40
|
return require.resolve(path.join(ts6, 'lib', 'typescript.js'))
|
|
34
41
|
} catch {
|
|
35
|
-
return origResolve
|
|
42
|
+
return origResolve(request, ...rest)
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
|
-
return origResolve
|
|
45
|
+
return origResolve(request, ...rest)
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
const { ESLint } = await import('eslint')
|
package/scripts/release.mts
CHANGED
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
// npm run release patch --dry-run # print the plan, change nothing
|
|
24
24
|
//
|
|
25
25
|
// Run via tsx so it can reuse src/changelog.ts (the conventional-commit
|
|
26
|
-
// parser) — a plain .mjs cannot import the TypeScript sources.
|
|
26
|
+
// parser) — a plain .mjs cannot import the TypeScript sources. The other
|
|
27
|
+
// helper scripts are .mts for the same reason; postinstall stays .mjs because
|
|
28
|
+
// it runs before tsx is guaranteed to be installed.
|
|
27
29
|
|
|
28
30
|
const NL = String.fromCharCode(10)
|
|
29
31
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -43,7 +45,11 @@ function fail(msg: string): never {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function run(cmd: string, args: string[]): void {
|
|
46
|
-
const r = spawnSync(cmd, args, {
|
|
48
|
+
const r = spawnSync(cmd, args, {
|
|
49
|
+
cwd: root,
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
shell: process.platform === 'win32',
|
|
52
|
+
})
|
|
47
53
|
if (r.status !== 0) fail(cmd + ' ' + args.join(' ') + ' exited ' + r.status)
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -100,8 +106,16 @@ if (git(['tag', '--list', tag]).trim()) {
|
|
|
100
106
|
// last tag. English commit subjects become the section body; the operator can
|
|
101
107
|
// still reword the section afterwards.
|
|
102
108
|
const latestTag = git(['describe', '--tags', '--abbrev=0'], true).trim()
|
|
103
|
-
const ref =
|
|
104
|
-
|
|
109
|
+
const ref =
|
|
110
|
+
latestTag ||
|
|
111
|
+
git(['rev-list', '--max-parents=0', 'HEAD']).trim().split(NL).pop() ||
|
|
112
|
+
'HEAD'
|
|
113
|
+
const subjects = git([
|
|
114
|
+
'log',
|
|
115
|
+
'--no-merges',
|
|
116
|
+
'--pretty=format:%s',
|
|
117
|
+
ref + '..HEAD',
|
|
118
|
+
])
|
|
105
119
|
.split(NL)
|
|
106
120
|
.map((s) => s.trim())
|
|
107
121
|
.filter(Boolean)
|
|
@@ -129,7 +143,14 @@ const sectionBody = body
|
|
|
129
143
|
.join(NL)
|
|
130
144
|
const date = new Date().toISOString().slice(0, 10)
|
|
131
145
|
const section =
|
|
132
|
-
'## [' +
|
|
146
|
+
'## [' +
|
|
147
|
+
next +
|
|
148
|
+
'] - ' +
|
|
149
|
+
date +
|
|
150
|
+
NL +
|
|
151
|
+
NL +
|
|
152
|
+
(sectionBody || '### Changed' + NL + NL + '- Internal improvements.') +
|
|
153
|
+
NL
|
|
133
154
|
|
|
134
155
|
if (dryRun) {
|
|
135
156
|
console.log('release(dry-run): ' + pkg.version + ' -> ' + next)
|
|
@@ -148,14 +169,22 @@ if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
|
|
|
148
169
|
const afterAnchor = idx + anchor.length
|
|
149
170
|
const updated =
|
|
150
171
|
changelog.slice(0, afterAnchor) +
|
|
151
|
-
NL +
|
|
172
|
+
NL +
|
|
173
|
+
NL +
|
|
174
|
+
section +
|
|
152
175
|
changelog.slice(afterAnchor).replace(/^\n+/, NL)
|
|
153
176
|
fs.writeFileSync(changelogPath, updated)
|
|
154
177
|
|
|
155
178
|
// 6. Bump package.json, preserving its exact formatting (single-line replace
|
|
156
179
|
// of the version value only).
|
|
157
180
|
const pkgText = fs.readFileSync(pkgPath, 'utf-8')
|
|
158
|
-
fs.writeFileSync(
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
pkgPath,
|
|
183
|
+
pkgText.replace(
|
|
184
|
+
'"version": "' + pkg.version + '"',
|
|
185
|
+
'"version": "' + next + '"',
|
|
186
|
+
),
|
|
187
|
+
)
|
|
159
188
|
|
|
160
189
|
// 7. Commit + tag. The push is intentionally NOT done here.
|
|
161
190
|
git(['add', 'package.json', 'CHANGELOG.md'])
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// theme palette (src/theme.ts) with the DejaVu Sans Mono font that the
|
|
8
8
|
// container actually ships, then screenshots it with headless Chromium.
|
|
9
9
|
//
|
|
10
|
-
// Run:
|
|
10
|
+
// Run: npm run render-demo (tsx scripts/render-demo.mts)
|
|
11
11
|
//
|
|
12
12
|
// Isolation: chromium.launch() uses a THROWAWAY profile, never ~/.zames/profile
|
|
13
13
|
// (see AGENTS.md — never touch the live agent's browser/profile).
|
|
@@ -33,9 +33,9 @@ const C = {
|
|
|
33
33
|
toggleOn: '#6fd0b0',
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const esc = (s) =>
|
|
36
|
+
const esc = (s: unknown): string =>
|
|
37
37
|
String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
38
|
-
const span = (color, text, bold = false) =>
|
|
38
|
+
const span = (color: string, text: string, bold = false): string =>
|
|
39
39
|
'<span style="color:' +
|
|
40
40
|
color +
|
|
41
41
|
(bold ? ';font-weight:700' : '') +
|
|
@@ -44,13 +44,18 @@ const span = (color, text, bold = false) =>
|
|
|
44
44
|
'</span>'
|
|
45
45
|
|
|
46
46
|
// One rendered session, line by line (mirrors the real banner + answer flow).
|
|
47
|
-
const lines = []
|
|
48
|
-
const push = (html) =>
|
|
47
|
+
const lines: string[] = []
|
|
48
|
+
const push = (html: string): void => {
|
|
49
|
+
lines.push('<div class="l">' + html + '</div>')
|
|
50
|
+
}
|
|
49
51
|
const blank = () => push(' ')
|
|
50
52
|
|
|
51
53
|
push(span(C.system, 'Working directory: ') + span(C.dir, '/home/dev/zames_pro'))
|
|
52
54
|
push(span(C.system, 'Version: ') + span(C.user, '2.64.1'))
|
|
53
|
-
push(
|
|
55
|
+
push(
|
|
56
|
+
span(C.system, 'Transcript: ') +
|
|
57
|
+
span(C.dim, '~/.zames/logs/zames_pro-2026-10-07.log'),
|
|
58
|
+
)
|
|
54
59
|
blank()
|
|
55
60
|
push(
|
|
56
61
|
span(
|
|
@@ -78,11 +83,7 @@ push(
|
|
|
78
83
|
span(C.user, 'Show the version from package.json.'),
|
|
79
84
|
)
|
|
80
85
|
blank()
|
|
81
|
-
push(
|
|
82
|
-
span(C.tool, '🔧 Read') +
|
|
83
|
-
' ' +
|
|
84
|
-
span(C.dim, '{"path":"package.json"}'),
|
|
85
|
-
)
|
|
86
|
+
push(span(C.tool, '🔧 Read') + ' ' + span(C.dim, '{"path":"package.json"}'))
|
|
86
87
|
push(
|
|
87
88
|
span(C.toolResult, ' → ') +
|
|
88
89
|
span(
|
|
@@ -48,8 +48,13 @@ const { createSubagentRunner } = await import('../src/subagent.ts')
|
|
|
48
48
|
const { loadConfig } = await import('../src/config.ts')
|
|
49
49
|
const { Transcript } = await import('../src/transcript.ts')
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
interface SmokeResult {
|
|
52
|
+
name: string
|
|
53
|
+
ok: boolean
|
|
54
|
+
note?: string
|
|
55
|
+
}
|
|
56
|
+
const results: SmokeResult[] = []
|
|
57
|
+
const record = (name: string, ok: boolean, note?: string): void => {
|
|
53
58
|
results.push({ name, ok, note })
|
|
54
59
|
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
55
60
|
}
|
|
@@ -149,7 +154,11 @@ async function main() {
|
|
|
149
154
|
askDeadlineMs: cfg.browser.askDeadlineMs,
|
|
150
155
|
maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
|
|
151
156
|
})
|
|
152
|
-
record(
|
|
157
|
+
record(
|
|
158
|
+
'follow-up answered',
|
|
159
|
+
outC.trim().length > 0,
|
|
160
|
+
JSON.stringify(outC.slice(0, 80)),
|
|
161
|
+
)
|
|
153
162
|
const onSubagent = createSubagentRunner({
|
|
154
163
|
browser,
|
|
155
164
|
workdir: WORK,
|
|
@@ -193,13 +202,41 @@ async function main() {
|
|
|
193
202
|
/HELLO_SMOKE/i.test(outD),
|
|
194
203
|
JSON.stringify(outD.slice(0, 120)),
|
|
195
204
|
)
|
|
205
|
+
|
|
206
|
+
// Scenario E — background Bash + BashOutput (N33): start a process that
|
|
207
|
+
// keeps running, poll its output, then kill it.
|
|
208
|
+
const outE = await runAgentLoop({
|
|
209
|
+
browser,
|
|
210
|
+
tools,
|
|
211
|
+
task:
|
|
212
|
+
'Do exactly this, then report: (1) Start the Bash command ' +
|
|
213
|
+
'`echo BG_MARKER_7 && sleep 120` in the BACKGROUND (run_in_background=true). ' +
|
|
214
|
+
'(2) Poll it once with BashOutput. (3) Kill it with BashOutput kill=true. ' +
|
|
215
|
+
'Then call respond with one short line containing BG_MARKER_7.',
|
|
216
|
+
workdir: WORK,
|
|
217
|
+
maxIterations: 20,
|
|
218
|
+
freshChat: true,
|
|
219
|
+
sendSystemPrompt: true,
|
|
220
|
+
transcript,
|
|
221
|
+
onToolCall: (n) => console.log(' tool: ' + n),
|
|
222
|
+
onAssistantMessage: (m) => console.log(' answer: ' + m),
|
|
223
|
+
onWarning: (m) => console.log(' warn: ' + m),
|
|
224
|
+
locale: 'ru',
|
|
225
|
+
askDeadlineMs: cfg.browser.askDeadlineMs,
|
|
226
|
+
maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
|
|
227
|
+
})
|
|
228
|
+
record(
|
|
229
|
+
'background bash: started, polled and killed',
|
|
230
|
+
/BG_MARKER_7/.test(outE),
|
|
231
|
+
JSON.stringify(outE.slice(0, 120)),
|
|
232
|
+
)
|
|
196
233
|
}
|
|
197
234
|
|
|
198
235
|
let failed = false
|
|
199
236
|
try {
|
|
200
237
|
await main()
|
|
201
238
|
} catch (e) {
|
|
202
|
-
record('run', false, e.message)
|
|
239
|
+
record('run', false, (e as Error).message)
|
|
203
240
|
failed = true
|
|
204
241
|
} finally {
|
|
205
242
|
await browser.close().catch(() => {})
|
|
File without changes
|
|
File without changes
|