zames_pro 2.96.0 → 2.96.2
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 +28 -0
- package/dist/backlog.js +17 -0
- package/dist/changelog.js +21 -23
- package/dist/commands.js +8 -3
- package/dist/dev-watch.js +6 -0
- package/dist/info-commands.js +11 -4
- package/dist/limits-command.js +13 -5
- package/dist/release-policy.js +6 -2
- package/dist/restore-history.js +10 -1
- package/dist/shell.js +14 -7
- package/dist/status-command.js +46 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **info:** keep a /chat and /pwd value on one line
|
|
13
|
+
- **status:** flatten a status/doctor value and drop a broken count
|
|
14
|
+
- **limits:** never render a negative or NaN counter in /limits
|
|
15
|
+
- **restore-history:** keep a plain answer that opens with 'thinking for'
|
|
16
|
+
- **commands:** keep a project file/skill/command name on one /context row
|
|
17
|
+
|
|
18
|
+
## [2.96.2] - 2026-11-06
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **info:** keep a /chat and /pwd value on one line
|
|
23
|
+
- **status:** flatten a status/doctor value and drop a broken count
|
|
24
|
+
- **limits:** never render a negative or NaN counter in /limits
|
|
25
|
+
- **restore-history:** keep a plain answer that opens with 'thinking for'
|
|
26
|
+
- **commands:** keep a project file/skill/command name on one /context row
|
|
27
|
+
|
|
28
|
+
## [2.96.1] - 2026-11-06
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **release-policy:** one pair of parens in the BREAKING note
|
|
33
|
+
- **changelog:** a group heading must start a line, not sit inside a bullet
|
|
34
|
+
- **backlog:** ignore a fenced section header when appending
|
|
35
|
+
- **dev-watch:** follow index.ts's hot-reloadable list (restore-history, input/layout)
|
|
36
|
+
- **shell:** block a recursive rm of ./ and ../ (trailing slash)
|
|
37
|
+
|
|
10
38
|
## [2.96.0] - 2026-11-05
|
|
11
39
|
|
|
12
40
|
### Added
|
package/dist/backlog.js
CHANGED
|
@@ -191,8 +191,25 @@ export function appendBacklogItem(text, item) {
|
|
|
191
191
|
}
|
|
192
192
|
const lines = src.split(NL);
|
|
193
193
|
const headerRe = new RegExp('^##[ ]+' + prio + '(?![0-9])');
|
|
194
|
+
// A fenced code block is MARKDOWN, not structure: a note that documents the
|
|
195
|
+
// file format contains `## P1` inside ```…```, and treating it as a section
|
|
196
|
+
// header inserted the new item INTO that fence (parseBacklogItems skips
|
|
197
|
+
// fenced headings, so the item then became invisible to /improve). Track the
|
|
198
|
+
// fence exactly like the parser does.
|
|
199
|
+
let fence = '';
|
|
194
200
|
let at = -1;
|
|
195
201
|
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
const fm = /^\s*(`{3,}|~{3,})/.exec(lines[i]);
|
|
203
|
+
if (fm) {
|
|
204
|
+
const mark = fm[1][0];
|
|
205
|
+
if (!fence)
|
|
206
|
+
fence = mark;
|
|
207
|
+
else if (mark === fence)
|
|
208
|
+
fence = '';
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (fence)
|
|
212
|
+
continue;
|
|
196
213
|
if (headerRe.test(lines[i])) {
|
|
197
214
|
// Insert right after the header and any blank lines that follow it, so
|
|
198
215
|
// the newest note sits at the top of its priority block.
|
package/dist/changelog.js
CHANGED
|
@@ -91,31 +91,29 @@ export function syncUnreleased(changelog, body) {
|
|
|
91
91
|
const rest = changelog.slice(afterAnchor);
|
|
92
92
|
const next = rest.search(/\n## \[/);
|
|
93
93
|
const tail = next < 0 ? '' : rest.slice(next);
|
|
94
|
-
|
|
94
|
+
// A `## ` that does not START a line is FREE TEXT inside a bullet (a commit
|
|
95
|
+
// description that documents a markdown heading, `ignore a ## P<n> header`).
|
|
96
|
+
// Matching it ANYWHERE treated that text as a group heading: the bullet was
|
|
97
|
+
// split and a FAKE `### P<n>` section appeared in the file. Every group
|
|
98
|
+
// heading is therefore recognized on WHOLE LINES only.
|
|
99
|
+
const lines = String(body || '')
|
|
95
100
|
.replace(/^## \[Unreleased\]\s*/m, '')
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
// The draft renders groups as `## Added` (top-level), but INSIDE the file
|
|
110
|
-
// a version's groups are `### Added` (a subsection of the version heading).
|
|
111
|
-
// `## [` (the next version) is left alone. The FIRST group follows the
|
|
112
|
-
// version heading directly (no bullet before it), so the blank-line rule
|
|
113
|
-
// above never fired for it — `## [Unreleased]` came out as a bare `#`
|
|
114
|
-
// followed by `# Added`. The heading owns a blank line, always.
|
|
101
|
+
.split(nl);
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
// A heading glued to the previous bullet (`- one## Added`) does not render
|
|
105
|
+
// as a heading at all: break it onto its own line, keeping the bullet.
|
|
106
|
+
const glued = /^(?!#)(\S.*[^\s`])(## .*)$/.exec(line);
|
|
107
|
+
if (glued)
|
|
108
|
+
out.push(glued[1], '', glued[2]);
|
|
109
|
+
else
|
|
110
|
+
out.push(line);
|
|
111
|
+
}
|
|
112
|
+
const cleanBody = out
|
|
113
|
+
.join(nl)
|
|
115
114
|
.replace(/^## (?!\[)/gm, '### ')
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.replace(/^\n+(?=### )/gm, '');
|
|
115
|
+
.replace(/^\n+(?=### )/, '')
|
|
116
|
+
.trim();
|
|
119
117
|
const section = cleanBody ? nl + nl + cleanBody + nl : nl;
|
|
120
118
|
return changelog.slice(0, afterAnchor) + section + tail;
|
|
121
119
|
}
|
package/dist/commands.js
CHANGED
|
@@ -64,11 +64,16 @@ export function formatDiffStat(statText, opts = {}, t = translate('en')) {
|
|
|
64
64
|
/** One renderable line per loaded context source (file + char count). */
|
|
65
65
|
export function formatContextSources(agents, memory, skills, commands, opts = {}, t = translate('en')) {
|
|
66
66
|
const lines = [];
|
|
67
|
+
// Paths, skill and command names come from the operator's PROJECT: a
|
|
68
|
+
// newline in one of them started a NEW row of the report (a crafted
|
|
69
|
+
// `/cmd\nFAKE` printed a bare `FAKE` line), so every value is flattened to
|
|
70
|
+
// one line first — the same rule as renderDoctor/formatJobLine.
|
|
71
|
+
const oneLine = (v) => String(v ?? '').replace(/\s+/g, ' ');
|
|
67
72
|
lines.push(t('context.title'));
|
|
68
73
|
if (typeof opts.systemPromptChars === 'number') {
|
|
69
74
|
lines.push(t('context.system_prompt', { n: String(opts.systemPromptChars) }));
|
|
70
75
|
}
|
|
71
|
-
const fileLine = (p, content) => ' ' + p + ' (' + content.length + ' chars)';
|
|
76
|
+
const fileLine = (p, content) => ' ' + oneLine(p) + ' (' + content.length + ' chars)';
|
|
72
77
|
lines.push(t('context.agents'));
|
|
73
78
|
if (agents.length)
|
|
74
79
|
for (const a of agents)
|
|
@@ -84,13 +89,13 @@ export function formatContextSources(agents, memory, skills, commands, opts = {}
|
|
|
84
89
|
lines.push(t('context.skills'));
|
|
85
90
|
if (skills.length)
|
|
86
91
|
for (const s of skills)
|
|
87
|
-
lines.push(' ' + s.name + (s.path ? ' ' + s.path : ''));
|
|
92
|
+
lines.push(' ' + oneLine(s.name) + (s.path ? ' ' + oneLine(s.path) : ''));
|
|
88
93
|
else
|
|
89
94
|
lines.push(' ' + t('context.none'));
|
|
90
95
|
lines.push(t('context.commands'));
|
|
91
96
|
if (commands.length)
|
|
92
97
|
for (const c of commands)
|
|
93
|
-
lines.push(' /' + c.name);
|
|
98
|
+
lines.push(' /' + oneLine(c.name));
|
|
94
99
|
else
|
|
95
100
|
lines.push(' ' + t('context.none'));
|
|
96
101
|
return lines.join(NL);
|
package/dist/dev-watch.js
CHANGED
|
@@ -28,6 +28,12 @@ export const HOT_RELOADABLE = new Set([
|
|
|
28
28
|
'config',
|
|
29
29
|
'fsutil',
|
|
30
30
|
'backlog',
|
|
31
|
+
// index.ts reloads these too; leaving them out of THIS list made the dev
|
|
32
|
+
// watcher report a false "restart zames" warning on every edit to them.
|
|
33
|
+
'restore-history',
|
|
34
|
+
// A NESTED module: only its basename is known here, which is what the
|
|
35
|
+
// watcher compares (src/input/layout.ts).
|
|
36
|
+
'layout',
|
|
31
37
|
'commands',
|
|
32
38
|
'run-task',
|
|
33
39
|
'attach-refs',
|
package/dist/info-commands.js
CHANGED
|
@@ -10,6 +10,11 @@ import { theme } from './theme.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export function createInfoCommands(deps) {
|
|
12
12
|
const { t } = deps;
|
|
13
|
+
// A chat id comes from the page URL and the working directory from the
|
|
14
|
+
// process cwd: a newline in either would start a LINE of its own in the
|
|
15
|
+
// terminal, so a value is flattened before it is printed (the same rule
|
|
16
|
+
// `/status` and `/doctor` already follow).
|
|
17
|
+
const oneLine = (v) => String(v ?? '').replace(/\s+/g, ' ');
|
|
13
18
|
const printLines = (lines, print) => {
|
|
14
19
|
for (const line of lines)
|
|
15
20
|
print(theme.system(line));
|
|
@@ -24,17 +29,19 @@ export function createInfoCommands(deps) {
|
|
|
24
29
|
handler: async (ctx) => {
|
|
25
30
|
const id = deps.currentChatId();
|
|
26
31
|
if (id) {
|
|
27
|
-
ctx.print(theme.system(t('chats.current_id', { v: id })));
|
|
28
|
-
ctx.print(theme.system(`URL: ${deps.chatUrl(id)}`));
|
|
32
|
+
ctx.print(theme.system(t('chats.current_id', { v: oneLine(id) })));
|
|
33
|
+
ctx.print(theme.system(`URL: ${oneLine(deps.chatUrl(id))}`));
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
31
36
|
const live = await deps.resolveChatId();
|
|
32
|
-
ctx.print(theme.system(live
|
|
37
|
+
ctx.print(theme.system(live
|
|
38
|
+
? t('chats.current_id', { v: oneLine(live) })
|
|
39
|
+
: t('chats.not_created')));
|
|
33
40
|
},
|
|
34
41
|
},
|
|
35
42
|
{
|
|
36
43
|
name: 'pwd',
|
|
37
|
-
handler: (ctx) => ctx.print(theme.system(deps.workdir())),
|
|
44
|
+
handler: (ctx) => ctx.print(theme.system(oneLine(deps.workdir()))),
|
|
38
45
|
},
|
|
39
46
|
{
|
|
40
47
|
name: 'transcript',
|
package/dist/limits-command.js
CHANGED
|
@@ -5,9 +5,17 @@ import { translate } from './i18n.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export function formatLimits(stats, opts, t = translate('en')) {
|
|
7
7
|
const out = [];
|
|
8
|
+
// A counter is a count and a pause is a duration: a negative/NaN/Infinity
|
|
9
|
+
// value (a hand-edited telemetry line, a broken provider counter) must not
|
|
10
|
+
// render as `-3s` / `NaNs`, which reads like a real measurement.
|
|
11
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? String(Math.max(0, v)) : '0';
|
|
8
12
|
out.push({
|
|
9
13
|
kind: 'title',
|
|
10
|
-
|
|
14
|
+
// The provider label comes from the config: a newline in it would start a
|
|
15
|
+
// row of its own.
|
|
16
|
+
text: t('limits.title', {
|
|
17
|
+
provider: String(opts.providerLabel ?? '').replace(/\s+/g, ' '),
|
|
18
|
+
}),
|
|
11
19
|
});
|
|
12
20
|
const windows = [
|
|
13
21
|
['10m', stats.last10m],
|
|
@@ -17,20 +25,20 @@ export function formatLimits(stats, opts, t = translate('en')) {
|
|
|
17
25
|
for (const [label, n] of windows) {
|
|
18
26
|
out.push({
|
|
19
27
|
kind: 'window',
|
|
20
|
-
text: t('limits.window', { label, n:
|
|
28
|
+
text: t('limits.window', { label, n: num(n) }),
|
|
21
29
|
});
|
|
22
30
|
}
|
|
23
31
|
out.push({
|
|
24
32
|
kind: 'rate',
|
|
25
33
|
text: t('limits.rate', {
|
|
26
|
-
n:
|
|
27
|
-
sec: String(Math.round(stats.avgBlockMs / 1000)),
|
|
34
|
+
n: num(stats.rateLimits),
|
|
35
|
+
sec: String(Math.round(Number(num(stats.avgBlockMs)) / 1000)),
|
|
28
36
|
}),
|
|
29
37
|
});
|
|
30
38
|
out.push({
|
|
31
39
|
kind: 'interval',
|
|
32
40
|
text: t('limits.interval', {
|
|
33
|
-
sec: String(Math.round(opts.intervalMs / 1000)),
|
|
41
|
+
sec: String(Math.round(Number(num(opts.intervalMs)) / 1000)),
|
|
34
42
|
adaptive: opts.adaptive ? t('limits.adaptive_on') : '',
|
|
35
43
|
}),
|
|
36
44
|
});
|
package/dist/release-policy.js
CHANGED
|
@@ -67,9 +67,13 @@ function kindsOf(list) {
|
|
|
67
67
|
}
|
|
68
68
|
function breakingNote(list) {
|
|
69
69
|
const first = list[0];
|
|
70
|
+
// One pair of parentheses around the WHOLE label: the scope used to close
|
|
71
|
+
// its own paren (`(cli): x)`) and leave a stray `)` in the operator report.
|
|
72
|
+
const label = first.scope
|
|
73
|
+
? first.scope + ': ' + first.description
|
|
74
|
+
: first.description;
|
|
70
75
|
return ('a BREAKING change (' +
|
|
71
|
-
|
|
72
|
-
first.description +
|
|
76
|
+
label +
|
|
73
77
|
') must be released by the operator, not by an unattended /loop');
|
|
74
78
|
}
|
|
75
79
|
/**
|
package/dist/restore-history.js
CHANGED
|
@@ -260,7 +260,16 @@ export function stripThinkBlocks(text) {
|
|
|
260
260
|
// "OK2"); the reasoning BODY was already removed with its subtree.
|
|
261
261
|
// A single newline: with the blank-line form the `[\s\S]*?` below ate
|
|
262
262
|
// the whole answer and the restored turn vanished.
|
|
263
|
-
|
|
263
|
+
// The header is `Thought for <duration>`: the FIRST word is the past tense
|
|
264
|
+
// (`Thought`) or the bare `Think`, and the duration must look like a TIME
|
|
265
|
+
// (`1 second`, `12s`, `2 minutes`). Matching any `think for <anything>`
|
|
266
|
+
// dropped a plain answer that OPENS with the same words.
|
|
267
|
+
t = t.replace(/^\s*(?:thought|thinking|think)\s+for\s+\d+(?:\.\d+)?\s*(?:seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)(?![a-z])\b[^\n]*\n?/i, '');
|
|
268
|
+
// The DOM may hand the header WITHOUT its newline (`Thought for 1
|
|
269
|
+
// secondOK2`), so a header followed directly by the answer text is dropped
|
|
270
|
+
// too. The duration must be a real TIME word, never just `for <word>`: a
|
|
271
|
+
// plain answer opening with the same words keeps its line.
|
|
272
|
+
t = t.replace(/^\s*(?:thought|thinking|think)\s+for\s+\d+(?:\.\d+)?\s*(?:seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)(?=[A-Z\u0410-\u042f])/i, '');
|
|
264
273
|
// The DOM may hand the header WITHOUT its newline ("Thought for 1
|
|
265
274
|
// secondOK2"): drop the header up to the first non-space run only when a
|
|
266
275
|
// line break or the end of the text follows it.
|
package/dist/shell.js
CHANGED
|
@@ -88,13 +88,20 @@ export function assertCommandInsideRoot(root, command) {
|
|
|
88
88
|
const raw = w.replace(/["']/g, '');
|
|
89
89
|
if (!raw)
|
|
90
90
|
continue;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
// A shell NORMALIZES a trailing separator before running the command:
|
|
92
|
+
// `rm -rf ./` and `rm -rf ../` delete the project root and its parent,
|
|
93
|
+
// but the raw word is neither `.` nor `..`, so the checks below let it
|
|
94
|
+
// through. Compare without the trailing slashes (a bare `//` is the
|
|
95
|
+
// root on POSIX and must stay one).
|
|
96
|
+
const norm = raw.replace(/\/+$/, '');
|
|
97
|
+
const bare = norm === '' ? raw.charAt(0) : norm;
|
|
98
|
+
if (bare === '/' ||
|
|
99
|
+
bare === '.' ||
|
|
100
|
+
bare === '..' ||
|
|
101
|
+
bare === '~' ||
|
|
102
|
+
bare.startsWith('~/') ||
|
|
103
|
+
bare.startsWith('/') ||
|
|
104
|
+
bare.startsWith('..')) {
|
|
98
105
|
throw new Error('Sandbox: recursive rm of an absolute/outside path is forbidden (' +
|
|
99
106
|
raw +
|
|
100
107
|
')');
|
package/dist/status-command.js
CHANGED
|
@@ -6,18 +6,29 @@ export function formatStatus(s, t = translate('en')) {
|
|
|
6
6
|
const none = t('common.none');
|
|
7
7
|
const onoff = (b) => (b ? t('common.on') : t('common.off'));
|
|
8
8
|
const lines = [];
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Every value printed here comes from the ENVIRONMENT (a working directory,
|
|
10
|
+
// a chat id) or from the config: a newline in one of them starts a status
|
|
11
|
+
// row of its own, so each value is flattened to one line — the same rule
|
|
12
|
+
// renderDoctor already applies to its cells.
|
|
13
|
+
const oneLine = (v) => String(v ?? '').replace(/\s+/g, ' ');
|
|
14
|
+
// A count must be a real number: `String(NaN)` used to print `NaN` in a row
|
|
15
|
+
// that reads like a measurement.
|
|
16
|
+
const count = (n) => typeof n === 'number' && Number.isFinite(n) && n >= 0 ? String(n) : '0';
|
|
17
|
+
const val = (v, fallback) => v ? oneLine(v) : fallback;
|
|
18
|
+
lines.push(t('status.workdir', { v: oneLine(s.workdir) }));
|
|
19
|
+
lines.push(t('status.review_mode', {
|
|
20
|
+
v: s.reviewMode ? oneLine(s.reviewMode) : no,
|
|
21
|
+
}));
|
|
11
22
|
if (s.reviewMode && s.origDir) {
|
|
12
|
-
lines.push(t('status.orig_dir', { v: s.origDir }));
|
|
23
|
+
lines.push(t('status.orig_dir', { v: oneLine(s.origDir) }));
|
|
13
24
|
}
|
|
14
|
-
lines.push(t('status.chat', { v: s.chatId
|
|
25
|
+
lines.push(t('status.chat', { v: val(s.chatId, none) }));
|
|
15
26
|
lines.push(t('status.fresh_next', { v: s.freshNext ? yes : no }));
|
|
16
27
|
lines.push(t('status.prompt_next', { v: s.promptNext ? yes : no }));
|
|
17
|
-
lines.push(t('status.last_chat', { v: s.lastChat
|
|
18
|
-
lines.push(t('status.sessions', { v: s.sessionsDir }));
|
|
28
|
+
lines.push(t('status.last_chat', { v: val(s.lastChat, none) }));
|
|
29
|
+
lines.push(t('status.sessions', { v: oneLine(s.sessionsDir) }));
|
|
19
30
|
lines.push(t('status.dev', { v: s.devMode ? t('common.on') : t('common.off') }));
|
|
20
|
-
lines.push(t('status.max_iter', { v: s.maxIterations }));
|
|
31
|
+
lines.push(t('status.max_iter', { v: count(s.maxIterations) }));
|
|
21
32
|
lines.push(t('status.headless', { v: s.headless ? yes : no }));
|
|
22
33
|
lines.push(t('status.debug', { v: s.debug ? yes : no }));
|
|
23
34
|
lines.push(t('status.toggles', {
|
|
@@ -26,27 +37,28 @@ export function formatStatus(s, t = translate('en')) {
|
|
|
26
37
|
}));
|
|
27
38
|
lines.push(t('status.undo', { v: onoff(s.undo) }));
|
|
28
39
|
lines.push(t('status.checkpoint', { v: onoff(s.checkpoint) }));
|
|
29
|
-
lines.push(t('status.transcript', {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
40
|
+
lines.push(t('status.transcript', {
|
|
41
|
+
v: s.transcriptFile ? oneLine(s.transcriptFile) : t('common.off'),
|
|
42
|
+
}));
|
|
43
|
+
// The GOAL is operator text (`/goal <free text>`): one status entry must
|
|
44
|
+
// stay on one line.
|
|
34
45
|
lines.push(t('status.goal', { v: s.goal ? oneLine(s.goal) : none }));
|
|
35
|
-
lines.push(t('status.locale', { v: s.localeName }));
|
|
46
|
+
lines.push(t('status.locale', { v: oneLine(s.localeName) }));
|
|
36
47
|
// A NEGATIVE tool count is nonsense (a broken MCP server status, a
|
|
37
|
-
// hand-edited value): "MCP tools: -2" reads like a real measurement
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const mcpCount = s.mcpToolCount === null || s.mcpToolCount < 0
|
|
48
|
+
// hand-edited value): "MCP tools: -2" reads like a real measurement.
|
|
49
|
+
const mcpCount = s.mcpToolCount === null ||
|
|
50
|
+
!Number.isFinite(s.mcpToolCount) ||
|
|
51
|
+
s.mcpToolCount < 0
|
|
42
52
|
? t('common.unknown')
|
|
43
|
-
: s.mcpToolCount;
|
|
53
|
+
: String(s.mcpToolCount);
|
|
44
54
|
lines.push(t('status.mcp', { v: mcpCount }));
|
|
45
55
|
// A NEGATIVE count is nonsense (a broken provider counter): show "unknown"
|
|
46
56
|
// rather than a number that reads like a real measurement.
|
|
47
|
-
const tokens = s.tokenUsage === null || s.tokenUsage
|
|
57
|
+
const tokens = s.tokenUsage === null || !Number.isFinite(s.tokenUsage) || s.tokenUsage < 0
|
|
58
|
+
? null
|
|
59
|
+
: s.tokenUsage;
|
|
48
60
|
lines.push(t('status.tokens', {
|
|
49
|
-
v: tokens === null ? t('common.unknown') : tokens,
|
|
61
|
+
v: tokens === null ? t('common.unknown') : String(tokens),
|
|
50
62
|
}));
|
|
51
63
|
return lines;
|
|
52
64
|
}
|
|
@@ -57,26 +69,31 @@ export function formatStatus(s, t = translate('en')) {
|
|
|
57
69
|
*/
|
|
58
70
|
export function doctorProbeSummary(p) {
|
|
59
71
|
const rows = [];
|
|
72
|
+
// A branch name, a missing capability id and a server/tool COUNT come from
|
|
73
|
+
// the repository and from a broken MCP probe: a newline started a row of its
|
|
74
|
+
// own and `NaN`/`-3` read like real measurements.
|
|
75
|
+
const oneLine = (v) => String(v ?? '').replace(/\s+/g, ' ');
|
|
76
|
+
const count = (n) => typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : 0;
|
|
60
77
|
rows.push({
|
|
61
78
|
status: p.gitOk ? 'ok' : 'warn',
|
|
62
|
-
label: p.gitOk
|
|
79
|
+
label: p.gitOk
|
|
80
|
+
? 'git: ' + oneLine(p.gitBranch || 'detached')
|
|
81
|
+
: 'git: absent',
|
|
63
82
|
});
|
|
64
83
|
rows.push({
|
|
65
84
|
status: p.authSaved ? 'ok' : 'warn',
|
|
66
85
|
label: p.authSaved ? 'auth: saved' : 'auth: not saved',
|
|
67
86
|
});
|
|
87
|
+
const servers = count(p.mcpServers);
|
|
88
|
+
const tools = count(p.mcpTools);
|
|
68
89
|
rows.push({
|
|
69
|
-
status:
|
|
70
|
-
label: 'mcp: ' +
|
|
71
|
-
String(p.mcpServers) +
|
|
72
|
-
' servers / ' +
|
|
73
|
-
String(p.mcpTools) +
|
|
74
|
-
' tools',
|
|
90
|
+
status: tools > 0 ? 'ok' : 'warn',
|
|
91
|
+
label: 'mcp: ' + String(servers) + ' servers / ' + String(tools) + ' tools',
|
|
75
92
|
});
|
|
76
93
|
if (p.uiMissingCritical && p.uiMissingCritical.length) {
|
|
77
94
|
rows.push({
|
|
78
95
|
status: 'warn',
|
|
79
|
-
label: 'ui: missing ' + p.uiMissingCritical.join(', '),
|
|
96
|
+
label: 'ui: missing ' + oneLine(p.uiMissingCritical.join(', ')),
|
|
80
97
|
});
|
|
81
98
|
}
|
|
82
99
|
return rows;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.96.
|
|
3
|
+
"version": "2.96.2",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — 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": {
|