zames_pro 2.72.8 → 2.73.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 +84 -0
- package/README.md +111 -19
- package/dist/agent-loop.js +15 -5
- package/dist/browser-dom.js +21 -0
- package/dist/browser-pure.js +6 -6
- package/dist/browser.js +155 -12
- package/dist/commands.js +7 -0
- package/dist/config-command.js +118 -0
- package/dist/config.js +56 -1
- package/dist/context.js +47 -19
- package/dist/deepseek-ui.js +33 -0
- package/dist/i18n/common.js +0 -16
- package/dist/i18n/config.js +14 -18
- package/dist/i18n/deepseek.js +39 -39
- package/dist/i18n/doctor.js +0 -4
- package/dist/i18n/help.js +8 -8
- package/dist/i18n/prompt.js +0 -4
- package/dist/i18n/qwen.js +85 -0
- package/dist/i18n/self-review.js +0 -5
- package/dist/i18n.js +2 -0
- package/dist/index.js +273 -137
- package/dist/input.js +23 -0
- package/dist/provider-handoff.js +73 -0
- package/dist/providers.js +50 -0
- package/dist/qwen-browser.js +1668 -0
- package/dist/qwen-chats.js +183 -0
- package/dist/qwen-net-capture.js +221 -0
- package/dist/qwen-ui.js +160 -0
- package/dist/run-task.js +2 -1
- package/dist/sessions.js +56 -7
- package/dist/subagent.js +28 -3
- package/dist/system-prompt.js +15 -4
- package/dist/tools.js +19 -6
- package/dist/ui-health.js +27 -21
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +8 -1
- package/scripts/release.mts +9 -7
package/dist/sessions.js
CHANGED
|
@@ -9,6 +9,40 @@ import { writeJsonAtomic } from './fsutil.js';
|
|
|
9
9
|
const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
|
|
10
10
|
const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
|
|
11
11
|
export const SESSIONS_FORMAT_VERSION = 1;
|
|
12
|
+
// Subagent chats are real DeepSeek chats, but they are INTERNAL: they exist
|
|
13
|
+
// only to isolate a sub-task's context and their report is already folded back
|
|
14
|
+
// into the parent chat. They must never be resumable. DeepSeek's own sidebar
|
|
15
|
+
// (`/chats`, which scrapes the DOM) still shows them under auto-generated
|
|
16
|
+
// titles, so we cannot hide them there; but we CAN hide them from `/sessions`
|
|
17
|
+
// (our own store) by keeping an id denylist here and filtering every read path.
|
|
18
|
+
const INTERNAL_FILE = path.join(SESSIONS_DIR, 'internal.json');
|
|
19
|
+
function readInternal() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = JSON.parse(fs.readFileSync(INTERNAL_FILE, 'utf-8'));
|
|
22
|
+
return new Set(Array.isArray(raw) ? raw.filter((x) => typeof x === 'string') : []);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return new Set();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Remember a chat id as internal (a subagent chat) so it is never resumed. */
|
|
29
|
+
export function markInternalChat(id) {
|
|
30
|
+
if (!id)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
ensureDir();
|
|
34
|
+
const set = readInternal();
|
|
35
|
+
set.add(String(id));
|
|
36
|
+
writeJsonAtomic(INTERNAL_FILE, Array.from(set));
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
/** True when `id` is a known internal (subagent) chat. */
|
|
41
|
+
export function isInternalChat(id) {
|
|
42
|
+
if (!id)
|
|
43
|
+
return false;
|
|
44
|
+
return readInternal().has(String(id));
|
|
45
|
+
}
|
|
12
46
|
function ensureDir() {
|
|
13
47
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
14
48
|
}
|
|
@@ -17,7 +51,7 @@ function safeName(id) {
|
|
|
17
51
|
}
|
|
18
52
|
// Save/update a session. workdir is needed so that when launched from the same
|
|
19
53
|
// project we restore exactly its last chat.
|
|
20
|
-
export function saveSession({ id, title = '', workdir = '', todos, }) {
|
|
54
|
+
export function saveSession({ id, title = '', workdir = '', todos, provider, }) {
|
|
21
55
|
if (!id)
|
|
22
56
|
return null;
|
|
23
57
|
try {
|
|
@@ -36,6 +70,7 @@ export function saveSession({ id, title = '', workdir = '', todos, }) {
|
|
|
36
70
|
createdAt: prev.createdAt || now,
|
|
37
71
|
updatedAt: now,
|
|
38
72
|
version: SESSIONS_FORMAT_VERSION,
|
|
73
|
+
provider: provider || prev.provider || 'deepseek',
|
|
39
74
|
};
|
|
40
75
|
// Keep the previous checklist unless a new one is explicitly passed —
|
|
41
76
|
// saveLastChat() calls this for title/workdir only and must not wipe it.
|
|
@@ -73,17 +108,30 @@ function writeIndex({ id, workdir }) {
|
|
|
73
108
|
}
|
|
74
109
|
// The last session for the working directory. If there is none —
|
|
75
110
|
// we return the last session overall (useful when launched from a new place).
|
|
76
|
-
|
|
111
|
+
// The last session for the working directory, restricted to `provider` when
|
|
112
|
+
// given. A chat id only exists on ITS backend, so restoring a DeepSeek chat
|
|
113
|
+
// while the active provider is Qwen (or vice versa) would fail to open — the
|
|
114
|
+
// filter makes auto-restore and /last pick the right one (and fall back to the
|
|
115
|
+
// global last only within the same provider).
|
|
116
|
+
export function loadLastSession(workdir = '', provider) {
|
|
77
117
|
try {
|
|
78
118
|
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
|
|
79
119
|
const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {};
|
|
80
120
|
const candidates = [byWorkdir[workdir], index.last].filter((x) => Boolean(x));
|
|
121
|
+
const internal = readInternal();
|
|
81
122
|
for (const id of candidates) {
|
|
82
123
|
// The session file may have been deleted — the index is then stale and
|
|
83
124
|
// the session cannot be restored. We try the next candidate.
|
|
125
|
+
if (internal.has(id))
|
|
126
|
+
continue;
|
|
84
127
|
const s = readSession(id);
|
|
85
|
-
if (s)
|
|
86
|
-
|
|
128
|
+
if (!s)
|
|
129
|
+
continue;
|
|
130
|
+
// Older files have no provider: treat them as deepseek (the only backend
|
|
131
|
+
// that existed when they were written).
|
|
132
|
+
if (provider && (s.provider || 'deepseek') !== provider)
|
|
133
|
+
continue;
|
|
134
|
+
return s;
|
|
87
135
|
}
|
|
88
136
|
return null;
|
|
89
137
|
}
|
|
@@ -102,18 +150,19 @@ export function readSession(id) {
|
|
|
102
150
|
return null;
|
|
103
151
|
}
|
|
104
152
|
}
|
|
105
|
-
// List all sessions, newest first.
|
|
153
|
+
// List all sessions, newest first. Internal (subagent) chats are excluded.
|
|
106
154
|
export function listSessions() {
|
|
107
155
|
try {
|
|
108
156
|
ensureDir();
|
|
157
|
+
const internal = readInternal();
|
|
109
158
|
const files = fs
|
|
110
159
|
.readdirSync(SESSIONS_DIR)
|
|
111
|
-
.filter((f) => f.endsWith('.json') && f !== 'last.json');
|
|
160
|
+
.filter((f) => f.endsWith('.json') && f !== 'last.json' && f !== 'internal.json');
|
|
112
161
|
const out = [];
|
|
113
162
|
for (const f of files) {
|
|
114
163
|
try {
|
|
115
164
|
const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8'));
|
|
116
|
-
if (data && data.id)
|
|
165
|
+
if (data && data.id && !internal.has(String(data.id)))
|
|
117
166
|
out.push(data);
|
|
118
167
|
}
|
|
119
168
|
catch { }
|
package/dist/subagent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadHooks, runLifecycleHooks } from './hooks.js';
|
|
2
|
+
import { markInternalChat } from './sessions.js';
|
|
2
3
|
// Subagent runner (BACKLOG N36). A `Task` tool call from the model is handled
|
|
3
4
|
// by the agent loop as a SEAM (like onAutoCompact): it calls back into the
|
|
4
5
|
// caller, which owns the whole lifecycle. This module builds that callback.
|
|
@@ -69,7 +70,27 @@ export function createSubagentRunner(opts) {
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
used++;
|
|
72
|
-
|
|
73
|
+
// Resolve the parent chat id BEFORE anything else. The runner restores the
|
|
74
|
+
// parent chat in its finally block, and that restore is what keeps the
|
|
75
|
+
// subagent's isolated context from swallowing the parent's turn. If the id
|
|
76
|
+
// cannot be resolved we must NOT run the nested loop: without it the finally
|
|
77
|
+
// would skip openChat and the parent would continue INSIDE the subagent
|
|
78
|
+
// chat. A couple of quick retries cover the "id not known yet" window on a
|
|
79
|
+
// freshly opened chat; after that we fail safe and tell the model.
|
|
80
|
+
let parentChatId = null;
|
|
81
|
+
for (let attempt = 0; attempt < 3 && !parentChatId; attempt++) {
|
|
82
|
+
parentChatId = await browser.getCurrentChatId().catch(() => null);
|
|
83
|
+
if (!parentChatId)
|
|
84
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
85
|
+
}
|
|
86
|
+
if (!parentChatId) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
text: 'Cannot start a subagent: the current chat id is unknown, so the ' +
|
|
90
|
+
'parent chat could not be restored afterwards (context isolation ' +
|
|
91
|
+
'would be lost). Do the work yourself with the other tools.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
73
94
|
const subTools = buildTools(req.type === 'explore');
|
|
74
95
|
const taskText = subagentPersona(req.type) + '\n\n--- SUB-TASK ---\n' + req.prompt;
|
|
75
96
|
// Save the parent's send hooks: the nested loop OVERWRITES them on
|
|
@@ -104,8 +125,12 @@ export function createSubagentRunner(opts) {
|
|
|
104
125
|
selfImprovement: false,
|
|
105
126
|
// No recursion: the subagent cannot spawn its own subagents.
|
|
106
127
|
onSubagent: null,
|
|
107
|
-
// Do not persist the subagent chat as a session
|
|
108
|
-
|
|
128
|
+
// Do not persist the subagent chat as a session, but REMEMBER its id
|
|
129
|
+
// as internal so it is hidden from /sessions and never resumed.
|
|
130
|
+
onChatReady: (chatId) => {
|
|
131
|
+
if (chatId)
|
|
132
|
+
markInternalChat(chatId);
|
|
133
|
+
},
|
|
109
134
|
// Keep the PARENT's UI alive during the nested run. The nested loop
|
|
110
135
|
// OVERWRITES browser.onSendStart/… with its own callbacks; without
|
|
111
136
|
// forwarding the parent's hooks the spinner and the throttle countdown
|
package/dist/system-prompt.js
CHANGED
|
@@ -34,14 +34,18 @@ its OWN separate chat with an ISOLATED context and returns ONLY a final
|
|
|
34
34
|
report. The subagent does NOT see this conversation, so the prompt you pass
|
|
35
35
|
must be fully self-contained (goal, exact questions, file paths, context).
|
|
36
36
|
|
|
37
|
-
Use it to PROTECT this chat's context
|
|
37
|
+
Use it to PROTECT this chat's context — that is its ONLY purpose. zames
|
|
38
|
+
runs subagents SEQUENTIALLY (one browser, one shared send slot), so a Task
|
|
39
|
+
call does NOT speed anything up; it only keeps this chat's context small.
|
|
38
40
|
|
|
39
41
|
- Broad exploration whose raw hits would flood this chat ("find every use of
|
|
40
42
|
X", "map how module Y works end to end"): delegate it with
|
|
41
43
|
subagent_type "explore" (read-only) and keep only the report.
|
|
42
44
|
- A self-contained research or verification sub-task you can specify in a
|
|
43
|
-
paragraph
|
|
44
|
-
-
|
|
45
|
+
paragraph.
|
|
46
|
+
- A long investigation, or several unrelated ones: call Task again for each.
|
|
47
|
+
EVERY call opens a FRESH isolated chat (a new subagent) — there is no
|
|
48
|
+
"reuse" of a previous subagent, so pass everything it needs in the prompt.
|
|
45
49
|
|
|
46
50
|
Do NOT use it for:
|
|
47
51
|
- work that depends on this conversation's history (the subagent cannot see
|
|
@@ -90,10 +94,17 @@ export function renderContextSection(context) {
|
|
|
90
94
|
'\n';
|
|
91
95
|
}
|
|
92
96
|
if (context.memory.length) {
|
|
97
|
+
// Point at the DEEPEST memory file (last in the chain: the loader pushes
|
|
98
|
+
// globals first, then the dir chain root->workdir). Before, this used
|
|
99
|
+
// memory[0] — the GLOBAL ~/.zames/MEMORY.md whenever it existed — so
|
|
100
|
+
// project-specific facts leaked into the global file and then loaded for
|
|
101
|
+
// every unrelated project. Fall back to the global file only when no
|
|
102
|
+
// project-level MEMORY.md exists.
|
|
103
|
+
const memoryTarget = context.memory[context.memory.length - 1].path;
|
|
93
104
|
out +=
|
|
94
105
|
'\nWhen you learn a durable fact about this project or the operator preferences (build quirks, ' +
|
|
95
106
|
'conventions, gotchas), append a short bullet to ' +
|
|
96
|
-
|
|
107
|
+
memoryTarget +
|
|
97
108
|
' via Edit/Write so it survives ' +
|
|
98
109
|
'into future sessions. Keep entries concise.\n';
|
|
99
110
|
}
|
package/dist/tools.js
CHANGED
|
@@ -279,12 +279,14 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
279
279
|
name: 'Task',
|
|
280
280
|
description: 'Delegate a self-contained sub-task to a SUBAGENT that works in its own ' +
|
|
281
281
|
'separate chat with an isolated context, then returns ONLY a final ' +
|
|
282
|
-
'report. Use it to keep heavy research out of the main context
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'
|
|
286
|
-
'
|
|
287
|
-
'
|
|
282
|
+
'report. Use it to keep heavy research out of the main context — it ' +
|
|
283
|
+
'does NOT run in parallel (one browser, one send slot), so it saves ' +
|
|
284
|
+
'context, not time. subagent_type: "explore" (read-only: search/read, ' +
|
|
285
|
+
'cannot modify) or "general" (full tools). Each call opens a FRESH ' +
|
|
286
|
+
'subagent chat; nothing is reused between calls. The subagent does NOT ' +
|
|
287
|
+
'see this conversation, so `prompt` must be fully self-contained: state ' +
|
|
288
|
+
'the goal, the exact questions, and any file paths/context it needs. ' +
|
|
289
|
+
'Returns the subagent report as text.',
|
|
288
290
|
parameters: {
|
|
289
291
|
description: 'string',
|
|
290
292
|
prompt: 'string',
|
|
@@ -324,6 +326,17 @@ export const MUTATING_TOOLS = new Set([
|
|
|
324
326
|
export function filterToolsForReadOnly(tools) {
|
|
325
327
|
return tools.filter((t) => !MUTATING_TOOLS.has(t.name));
|
|
326
328
|
}
|
|
329
|
+
// A skill may declare `allowed-tools:` in its SKILL.md frontmatter (the Claude
|
|
330
|
+
// Code contract): the skill then runs with ONLY those tools. We keep the
|
|
331
|
+
// `respond` seam so the run can still finish, and match names case-insensitively
|
|
332
|
+
// (frontmatter is human-written). PURE; unit-tested.
|
|
333
|
+
export function filterToolsByAllowList(tools, allowed) {
|
|
334
|
+
if (!allowed || !allowed.length)
|
|
335
|
+
return tools;
|
|
336
|
+
const allow = new Set(allowed.map((n) => n.toLowerCase()));
|
|
337
|
+
allow.add('respond');
|
|
338
|
+
return tools.filter((t) => allow.has(t.name.toLowerCase()));
|
|
339
|
+
}
|
|
327
340
|
// MCP tool names are opaque (server + '__' + tool), so the exact-name
|
|
328
341
|
// MUTATING_TOOLS set cannot classify them. Instead we match the common
|
|
329
342
|
// mutating VERBS in the tool name. It is a heuristic deny-list, not a proof:
|
package/dist/ui-health.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
// UI health check — a cheap "can the agent still drive this page?" probe.
|
|
2
2
|
//
|
|
3
|
-
// WHY: every selector in
|
|
4
|
-
// not control. When
|
|
5
|
-
// task hanging on "input field not found" (or a silently mis-clicked
|
|
6
|
-
// minutes into a run. This module turns that into an explicit answer
|
|
7
|
-
// the task: it checks the capabilities the agent ALWAYS needs and
|
|
8
|
-
// missing ones. The page object is injected (the browser owns
|
|
9
|
-
// the decision logic here stays pure and unit-testable.
|
|
3
|
+
// WHY: every selector in the provider's *-ui.ts is an assumption about a page
|
|
4
|
+
// we do not control. When the site ships a redesign, the FIRST symptom used to
|
|
5
|
+
// be a task hanging on "input field not found" (or a silently mis-clicked
|
|
6
|
+
// send), minutes into a run. This module turns that into an explicit answer
|
|
7
|
+
// BEFORE the task: it checks the capabilities the agent ALWAYS needs and
|
|
8
|
+
// reports the missing ones. The page object is injected (the browser owns
|
|
9
|
+
// Playwright), so the decision logic here stays pure and unit-testable.
|
|
10
10
|
//
|
|
11
11
|
// Only ALWAYS-PRESENT capabilities are probed. The Stop button and the answer
|
|
12
12
|
// nodes are TRANSIENT (Stop exists only during generation, answers only after
|
|
13
13
|
// a turn), so a presence probe on a healthy idle page would report false
|
|
14
14
|
// negatives; they are deliberately excluded.
|
|
15
15
|
import { INPUT_SELECTORS, SEND_SELECTORS, TOGGLE_SELECTORS, NEW_CHAT_SELECTOR, NEW_CHAT_TEXT_RE, } from './deepseek-ui.js';
|
|
16
|
-
// Critical: without the input the prompt
|
|
17
|
-
// cannot be submitted — a task would hang on
|
|
18
|
-
// toggles/newChat degrade gracefully (the toggles
|
|
19
|
-
// state, newChat falls back to navigating to the
|
|
16
|
+
// DeepSeek's default capability list. Critical: without the input the prompt
|
|
17
|
+
// cannot be typed, without send it cannot be submitted — a task would hang on
|
|
18
|
+
// `ds.input_missing`. Advisory: toggles/newChat degrade gracefully (the toggles
|
|
19
|
+
// fall back to the configured state, newChat falls back to navigating to the
|
|
20
|
+
// base URL).
|
|
20
21
|
export const UI_CAPABILITIES = [
|
|
21
22
|
{ capability: 'input', selectors: INPUT_SELECTORS, critical: true },
|
|
22
23
|
{ capability: 'send', selectors: SEND_SELECTORS, critical: true },
|
|
23
24
|
{ capability: 'toggles', selectors: TOGGLE_SELECTORS, critical: false },
|
|
24
|
-
{
|
|
25
|
+
{
|
|
26
|
+
capability: 'newChat',
|
|
27
|
+
selectors: [NEW_CHAT_SELECTOR],
|
|
28
|
+
critical: false,
|
|
29
|
+
textRe: NEW_CHAT_TEXT_RE,
|
|
30
|
+
},
|
|
25
31
|
];
|
|
26
32
|
async function hasAny(page, sel, textRe) {
|
|
27
33
|
try {
|
|
@@ -35,21 +41,21 @@ async function hasAny(page, sel, textRe) {
|
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
/**
|
|
38
|
-
* Probe the always-present
|
|
39
|
-
* reported as every capability missing, which the caller surfaces
|
|
40
|
-
* operator instead of letting the next task hang.
|
|
44
|
+
* Probe the always-present controls of a provider's chat page. Never throws: a
|
|
45
|
+
* dead page is reported as every capability missing, which the caller surfaces
|
|
46
|
+
* to the operator instead of letting the next task hang.
|
|
47
|
+
*
|
|
48
|
+
* `capabilities` is a parameter so each provider (deepseek-ui.ts, qwen-ui.ts)
|
|
49
|
+
* can supply its own selectors while the probe logic stays shared.
|
|
41
50
|
*/
|
|
42
|
-
export async function probeUiHealth(page) {
|
|
51
|
+
export async function probeUiHealth(page, capabilities = UI_CAPABILITIES) {
|
|
43
52
|
const present = [];
|
|
44
53
|
const missing = [];
|
|
45
54
|
const missingCritical = [];
|
|
46
|
-
for (const spec of
|
|
55
|
+
for (const spec of capabilities) {
|
|
47
56
|
let found = false;
|
|
48
|
-
// The new-chat control is matched by an EXACT text label on a broad
|
|
49
|
-
// element set (it is a <div> on the current build, not a <button>).
|
|
50
|
-
const textRe = spec.capability === 'newChat' ? NEW_CHAT_TEXT_RE : undefined;
|
|
51
57
|
for (const sel of spec.selectors) {
|
|
52
|
-
if (await hasAny(page, sel, textRe)) {
|
|
58
|
+
if (await hasAny(page, sel, spec.textRe)) {
|
|
53
59
|
found = true;
|
|
54
60
|
break;
|
|
55
61
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Terminal coding agent that drives chat
|
|
3
|
+
"version": "2.73.0",
|
|
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": {
|
|
7
7
|
"zames": "dist/index.js"
|
|
@@ -56,7 +56,11 @@
|
|
|
56
56
|
"browser-automation",
|
|
57
57
|
"llm",
|
|
58
58
|
"no-api-key",
|
|
59
|
-
"automation"
|
|
59
|
+
"automation",
|
|
60
|
+
"qwen",
|
|
61
|
+
"qwen-chat",
|
|
62
|
+
"multi-provider",
|
|
63
|
+
"provider"
|
|
60
64
|
],
|
|
61
65
|
"license": "MIT",
|
|
62
66
|
"engines": {
|
|
@@ -30,8 +30,15 @@ const FLOORS = {
|
|
|
30
30
|
'undo.ts': 85,
|
|
31
31
|
'checkpoint.ts': 80,
|
|
32
32
|
'permissions.ts': 85,
|
|
33
|
-
|
|
33
|
+
// changelog.ts is mostly comments + type/interface declarations (lines
|
|
34
|
+
// 1-30) that carry no runtime statements, so its LINE % caps well below the
|
|
35
|
+
// other modules even with every executable branch tested. The floor reflects
|
|
36
|
+
// that ceiling (a real drop below it still fails).
|
|
37
|
+
'changelog.ts': 78,
|
|
34
38
|
'attach-refs.ts': 85,
|
|
39
|
+
// C3: the pure /config subcommand dispatcher — silent routing regressions
|
|
40
|
+
// (a wrong subcommand branch) are invisible without tests.
|
|
41
|
+
'config-command.ts': 85,
|
|
35
42
|
// N12: web.ts holds the SSRF guard and the DuckDuckGo/HTML parsers — a
|
|
36
43
|
// regression there is silent. Floors sit below the current values.
|
|
37
44
|
'web.ts': 40,
|
package/scripts/release.mts
CHANGED
|
@@ -160,19 +160,21 @@ if (dryRun) {
|
|
|
160
160
|
process.exit(0)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// 5. Insert the section
|
|
163
|
+
// 5. Insert the version section in place of the Unreleased BODY. The
|
|
164
|
+
// Unreleased body is auto-synced from commits (see changelog:sync), so it is
|
|
165
|
+
// the same content as the new section — leaving it in place duplicated every
|
|
166
|
+
// entry (the real "### Fixed" block twice). Replace the whole body up to the
|
|
167
|
+
// next version heading with the new section.
|
|
164
168
|
const changelogPath = path.join(root, 'CHANGELOG.md')
|
|
165
169
|
const changelog = fs.readFileSync(changelogPath, 'utf-8')
|
|
166
170
|
const anchor = '## [Unreleased]'
|
|
167
171
|
const idx = changelog.indexOf(anchor)
|
|
168
172
|
if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
|
|
169
173
|
const afterAnchor = idx + anchor.length
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
section +
|
|
175
|
-
changelog.slice(afterAnchor).replace(/^\n+/, NL)
|
|
174
|
+
const rest = changelog.slice(afterAnchor)
|
|
175
|
+
const nextHeading = rest.search(/\n## \[/)
|
|
176
|
+
const tail = nextHeading < 0 ? NL : rest.slice(nextHeading)
|
|
177
|
+
const updated = changelog.slice(0, afterAnchor) + NL + NL + section + tail
|
|
176
178
|
fs.writeFileSync(changelogPath, updated)
|
|
177
179
|
|
|
178
180
|
// 6. Bump package.json, preserving its exact formatting (single-line replace
|