zames_pro 2.82.48 → 2.82.50

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.
@@ -0,0 +1,29 @@
1
+ import { providerHasModelPicker, providerLabel, isProviderId, PROVIDER_IDS, } from './providers.js';
2
+ import { translate } from './i18n.js';
3
+ /** Parse the text after `/provider` into an intent. `current` is the ACTIVE id. */
4
+ export function parseProviderIntent(arg, current) {
5
+ const a = String(arg ?? '')
6
+ .trim()
7
+ .toLowerCase();
8
+ if (!a || a === 'status')
9
+ return { kind: 'status' };
10
+ if (a === 'login')
11
+ return { kind: 'login' };
12
+ if (isProviderId(a))
13
+ return { kind: 'switch', provider: a, same: a === current };
14
+ return { kind: 'usage' };
15
+ }
16
+ /** The usage line for an unknown `/provider` argument. */
17
+ export function providerUsageMessage(t = translate('en')) {
18
+ return t('provider.usage', { providers: PROVIDER_IDS.join('|') });
19
+ }
20
+ /**
21
+ * Why a provider with no model picker cannot be selected via `/model`.
22
+ * (Kept here as well as in model-command so the two commands share the
23
+ * capability check instead of each hardcoding a provider id.)
24
+ */
25
+ export function providerWithoutPickerMessage(provider, t = translate('en')) {
26
+ if (providerHasModelPicker(provider))
27
+ return null;
28
+ return t('model.unsupported', { provider: providerLabel(provider) });
29
+ }
@@ -133,10 +133,27 @@ function sameDir(a, b) {
133
133
  };
134
134
  return real(x) === real(y);
135
135
  }
136
- /** Remove the handoff once it has been posted (or is stale). Best-effort. */
136
+ /** Remove the handoff once it has been posted (or is stale). Best-effort.
137
+ *
138
+ * The GLOBAL file and every per-workdir SIDE file are removed. Clearing only
139
+ * the global one left a displaced record readable through its side file, so
140
+ * an already-consumed summary was posted again on a later switch in that
141
+ * project ("the summary came back from the dead"). Side files are small and
142
+ * their whole purpose is a pending handoff, so removing all of them is safe. */
137
143
  export function clearProviderHandoff() {
138
144
  try {
139
145
  fs.unlinkSync(HANDOFF_FILE);
140
146
  }
141
147
  catch { }
148
+ try {
149
+ for (const f of fs.readdirSync(ZAMES_HOME)) {
150
+ if (!f.startsWith('provider-handoff-') || !f.endsWith('.json'))
151
+ continue;
152
+ try {
153
+ fs.unlinkSync(path.join(ZAMES_HOME, f));
154
+ }
155
+ catch { }
156
+ }
157
+ }
158
+ catch { }
142
159
  }
@@ -0,0 +1,15 @@
1
+ import { formatQueueList } from './commands.js';
2
+ /**
3
+ * The full output of `/queue` (minus the `clear` SIDE EFFECT, which the caller
4
+ * performs on its own live queue). `sub` is the parsed subcommand or null when
5
+ * it was not recognized; `count` is the live queue length.
6
+ */
7
+ export function renderQueueView(sub, count, queue) {
8
+ if (!sub)
9
+ return { kind: 'usage' };
10
+ if (sub === 'clear')
11
+ return { kind: 'cleared', n: count };
12
+ if (!count || !queue.length)
13
+ return { kind: 'empty' };
14
+ return { kind: 'list', lines: formatQueueList(queue) };
15
+ }
@@ -187,12 +187,19 @@ export function parseQwenHistory(json) {
187
187
  arr = asArr(d.messages);
188
188
  else if (d.chat && typeof d.chat === 'object') {
189
189
  const chat = d.chat;
190
- const hist = chat.history && typeof chat.history === 'object'
191
- ? chat.history
192
- : null;
193
- if (hist && hist.messages)
194
- arr = asArr(hist.messages);
195
- else if (chat.messages)
190
+ // `history` may itself be the message LIST (an array), not only an
191
+ // object with a `messages` field: the array form was read as
192
+ // `history.messages` -> undefined and the whole restored dialogue came
193
+ // back EMPTY even though the payload carried it.
194
+ const hist = chat.history;
195
+ if (Array.isArray(hist))
196
+ arr = hist;
197
+ else if (hist && typeof hist === 'object') {
198
+ const h = hist;
199
+ if (h.messages)
200
+ arr = asArr(h.messages);
201
+ }
202
+ if (!arr && chat.messages)
196
203
  arr = asArr(chat.messages);
197
204
  }
198
205
  }
@@ -97,6 +97,12 @@ function pickAnswer(node) {
97
97
  if (typeof node !== 'object')
98
98
  return '';
99
99
  const obj = node;
100
+ // A plain-JSON reply can carry the SAME phase marker as an SSE delta
101
+ // (`thinking_summary`). The reasoning guard used to live only in the
102
+ // choices[] branch, so a top-level `{phase:"thinking_summary",content}`
103
+ // (or the same object one envelope deeper) was returned as the ANSWER.
104
+ if (isThinkingPhase(obj))
105
+ return '';
100
106
  if (Array.isArray(obj.choices)) {
101
107
  let out = '';
102
108
  for (const ch of obj.choices) {
@@ -220,13 +226,14 @@ export function isRateLimitText(body) {
220
226
  const b = String(body || '');
221
227
  if (!b)
222
228
  return false;
223
- // `quota_limit` is a SEPARATE code Qwen returns in the SSE error field
224
- // ("The service is currently experiencing high demand. Please try again
225
- // later.") — a TRANSIENT capacity limit, so it belongs with the rate-limit
226
- // family (back off and retry), not with the guest DAILY limit.
227
- if (/quota_limit/i.test(b))
229
+ const errNotice = /event:\s*error|"type"\s*:\s*"error"|"error"\s*:/i.test(b);
230
+ // The `quota_limit` CODE only means something in an ERROR payload: a normal
231
+ // ANSWER may MENTION `quota_limit` (the operator asked how to handle that
232
+ // API error), and this check used to run BEFORE the answer-content guard, so
233
+ // such an answer made ask() back off and re-send a good turn.
234
+ if (/quota_limit/i.test(b) && (errNotice || !hasAnswerContent(b)))
228
235
  return true;
229
- if (/event:\s*error|"type"\s*:\s*"error"|"error"\s*:/i.test(b)) {
236
+ if (errNotice) {
230
237
  return /too many requests|rate limit|too frequent|please try again later|high demand|слишком часто/i.test(b);
231
238
  }
232
239
  // No structured error marker: only treat the body as a notice when it has
@@ -0,0 +1,38 @@
1
+ import { translate } from './i18n.js';
2
+ // Pure half of the session commands (/sessions, /resume-id). The resume itself
3
+ // drives a browser; deciding WHICH session may be opened — the filter, the id
4
+ // normalization, a foreign-provider refusal — is pure and lives here.
5
+ /** Parse the text after `/resume-id` into a chat id, or null when empty. */
6
+ export function parseResumeId(arg) {
7
+ const id = String(arg ?? '').trim();
8
+ return id ? id : null;
9
+ }
10
+ /** Keep the sessions matching a `/sessions <query>` filter (title or workdir). */
11
+ export function filterSessions(sessions, filter) {
12
+ const q = String(filter ?? '')
13
+ .trim()
14
+ .toLowerCase();
15
+ if (!q)
16
+ return sessions.slice();
17
+ return sessions.filter((s) => (s.title || '').toLowerCase().includes(q) ||
18
+ (s.workdir || '').toLowerCase().includes(q));
19
+ }
20
+ /**
21
+ * A chat id only exists on the backend that created it, so opening a session
22
+ * saved by ANOTHER provider would fail inside the browser with a confusing
23
+ * error. Old files carry no `provider` field and belong to DeepSeek (the only
24
+ * backend that existed then).
25
+ *
26
+ * Returns a user-facing refusal, or null when the session may be opened.
27
+ */
28
+ export function resumeProviderRefusal(session, activeProvider, t = translate('en')) {
29
+ if (!session)
30
+ return null;
31
+ const owner = session.provider || 'deepseek';
32
+ if (owner === activeProvider)
33
+ return null;
34
+ return t('sessions.provider_mismatch', {
35
+ owner,
36
+ active: activeProvider,
37
+ });
38
+ }
@@ -43,7 +43,13 @@ export function parseSendEvents(raw) {
43
43
  export function summarizeSends(events, now = Date.now()) {
44
44
  const within = (ms) => events.filter((e) => {
45
45
  const t = Date.parse(e.at);
46
- return Number.isFinite(t) && now - t <= ms;
46
+ // The event must be in the PAST: a future stamp (clock skew on the
47
+ // machine that wrote the log, a hand-edited line) satisfies
48
+ // `now - t <= ms` for EVERY window and inflated all three counters.
49
+ if (!Number.isFinite(t))
50
+ return false;
51
+ const age = now - t;
52
+ return age >= 0 && age <= ms;
47
53
  }).length;
48
54
  // `blockMs` may arrive as a NUMBER (the writer) or a string (an older
49
55
  // build, a hand-edited file): a numeric string is accepted so the average
@@ -62,9 +68,15 @@ export function summarizeSends(events, now = Date.now()) {
62
68
  avgBlockMs,
63
69
  };
64
70
  }
65
- /** `~/.zames/logs/sends-<provider>.jsonl`. */
71
+ /** `~/.zames/logs/sends-<provider>.jsonl`. The provider id comes from the
72
+ * CONFIG (a hand-edited file, `/provider`), so it must not be able to escape
73
+ * the log directory: a separator or `..` used to build a path OUTSIDE
74
+ * `~/.zames/logs` (a subdirectory at best, another file at worst — and the
75
+ * rotation DELETES files it finds there). Everything outside a plain file-name
76
+ * alphabet is replaced with `_`. */
66
77
  export function sendLogPath(logsDir, provider) {
67
- return path.join(logsDir, 'sends-' + provider + '.jsonl');
78
+ const safe = String(provider || 'unknown').replace(/[^A-Za-z0-9._-]/g, '_');
79
+ return path.join(logsDir, 'sends-' + safe + '.jsonl');
68
80
  }
69
81
  /** Keep at most `keep` sends-*.jsonl files (rotate the oldest away). */
70
82
  export function rotateSendLogs(logsDir, keep = 5) {
@@ -87,7 +87,9 @@ export function applySendEvent(state, event, cfg, now = Date.now()) {
87
87
  next.blockedAt = state.blocked ? state.blockedAt || stamp : stamp;
88
88
  next.okStreak = 0;
89
89
  next.forcedWaitMs =
90
- typeof event.retryAfterMs === 'number' && event.retryAfterMs > 0
90
+ typeof event.retryAfterMs === 'number' &&
91
+ Number.isFinite(event.retryAfterMs) &&
92
+ event.retryAfterMs > 0
91
93
  ? event.retryAfterMs
92
94
  : cfg.rateLimitWaitMs;
93
95
  next.forcedWaitAt = now;
@@ -0,0 +1,68 @@
1
+ import { translate } from './i18n.js';
2
+ /** Render `/status` as an array of lines (already localized). */
3
+ export function formatStatus(s, t = translate('en')) {
4
+ const yes = t('common.yes');
5
+ const no = t('common.no');
6
+ const none = t('common.none');
7
+ const onoff = (b) => (b ? t('common.on') : t('common.off'));
8
+ const lines = [];
9
+ lines.push(t('status.workdir', { v: s.workdir }));
10
+ lines.push(t('status.review_mode', { v: s.reviewMode || t('status.review_none') }));
11
+ if (s.reviewMode && s.origDir) {
12
+ lines.push(t('status.orig_dir', { v: s.origDir }));
13
+ }
14
+ lines.push(t('status.chat', { v: s.chatId || none }));
15
+ lines.push(t('status.fresh_next', { v: s.freshNext ? yes : no }));
16
+ lines.push(t('status.prompt_next', { v: s.promptNext ? yes : no }));
17
+ lines.push(t('status.last_chat', { v: s.lastChat || none }));
18
+ lines.push(t('status.sessions', { v: s.sessionsDir }));
19
+ lines.push(t('status.dev', { v: s.devMode ? t('common.on') : t('common.off') }));
20
+ lines.push(t('status.max_iter', { v: s.maxIterations }));
21
+ lines.push(t('status.headless', { v: s.headless ? yes : no }));
22
+ lines.push(t('status.debug', { v: s.debug ? yes : no }));
23
+ lines.push(t('status.toggles', {
24
+ think: s.deepThinking ? yes : no,
25
+ search: s.webSearch ? yes : no,
26
+ }));
27
+ lines.push(t('status.undo', { v: onoff(s.undo) }));
28
+ lines.push(t('status.checkpoint', { v: onoff(s.checkpoint) }));
29
+ lines.push(t('status.transcript', { v: s.transcriptFile || t('common.off') }));
30
+ lines.push(t('status.goal', { v: s.goal || none }));
31
+ lines.push(t('status.locale', { v: s.localeName }));
32
+ lines.push(t('status.mcp', { v: s.mcpToolCount === null ? none : s.mcpToolCount }));
33
+ lines.push(t('status.tokens', {
34
+ v: s.tokenUsage === null ? t('common.unknown') : s.tokenUsage,
35
+ }));
36
+ return lines;
37
+ }
38
+ /**
39
+ * The `/doctor` rows that depend on a LIVE PROBE rather than on the config:
40
+ * git availability + branch, clipboard, MCP, the auth marker and the UI health
41
+ * result. `renderDoctor` (commands.ts) renders the full table from these.
42
+ */
43
+ export function doctorProbeSummary(p) {
44
+ const rows = [];
45
+ rows.push({
46
+ status: p.gitOk ? 'ok' : 'warn',
47
+ label: p.gitOk ? 'git: ' + (p.gitBranch || 'detached') : 'git: absent',
48
+ });
49
+ rows.push({
50
+ status: p.authSaved ? 'ok' : 'warn',
51
+ label: p.authSaved ? 'auth: saved' : 'auth: not saved',
52
+ });
53
+ rows.push({
54
+ status: p.mcpTools > 0 ? 'ok' : 'warn',
55
+ label: 'mcp: ' +
56
+ String(p.mcpServers) +
57
+ ' servers / ' +
58
+ String(p.mcpTools) +
59
+ ' tools',
60
+ });
61
+ if (p.uiMissingCritical && p.uiMissingCritical.length) {
62
+ rows.push({
63
+ status: 'warn',
64
+ label: 'ui: missing ' + p.uiMissingCritical.join(', '),
65
+ });
66
+ }
67
+ return rows;
68
+ }
@@ -101,5 +101,12 @@ export function looksLikeToolError(result) {
101
101
  head.startsWith('subagent failed') ||
102
102
  head.startsWith('subagent error') ||
103
103
  head.includes('process killed') ||
104
- head.startsWith('(aborted by the operator)'));
104
+ head.startsWith('(aborted by the operator)') ||
105
+ // A SANDBOX refusal: `safePath` throws
106
+ // `Access outside the working directory is forbidden: …` and the shell
107
+ // guard throws `Sandbox: …` (eval+cd, an unresolvable cd target, chroot).
108
+ // Both mean the call did NOTHING, yet neither prefix matched and the
109
+ // operator saw a green check next to a refused call.
110
+ head.includes('outside the working directory is forbidden') ||
111
+ head.includes('sandbox:'));
105
112
  }
package/dist/tools.js CHANGED
@@ -417,7 +417,10 @@ export function filterToolsForReadOnly(tools) {
417
417
  export function filterToolsByAllowList(tools, allowed) {
418
418
  if (!allowed || !allowed.length)
419
419
  return tools;
420
- const allow = new Set(allowed.map((n) => n.toLowerCase()));
420
+ // `allowed-tools:` is human-written: an entry may carry surrounding spaces
421
+ // (a quoted value, a trailing comma). Lower-casing alone left `read `, which
422
+ // matched nothing and silently reduced the skill to `respond` only.
423
+ const allow = new Set(allowed.map((n) => String(n).trim().toLowerCase()).filter(Boolean));
421
424
  allow.add('respond');
422
425
  return tools.filter((t) => allow.has(t.name.toLowerCase()));
423
426
  }
@@ -20,6 +20,26 @@ function visWidth(s) {
20
20
  i++;
21
21
  i++;
22
22
  }
23
+ else if (s[i] === ']') {
24
+ // OSC sequence: `ESC ] ... (BEL | ESC \)`. OSC-8 hyperlinks
25
+ // (produced by renderMarkdown when the terminal supports them) wrap
26
+ // the LABEL in such a sequence, and the OLD parser did not skip it:
27
+ // every URL character counted as a visible column, so the viewport
28
+ // believed a short link line filled many rows and the log jumped.
29
+ i++;
30
+ while (i < s.length) {
31
+ const c = s.charCodeAt(i);
32
+ if (c === 7) {
33
+ i++;
34
+ break;
35
+ }
36
+ if (c === 27 && s[i + 1] === String.fromCharCode(92)) {
37
+ i += 2;
38
+ break;
39
+ }
40
+ i++;
41
+ }
42
+ }
23
43
  continue;
24
44
  }
25
45
  const cp = s.codePointAt(i);
@@ -73,21 +93,38 @@ export function windowByRows(entries, height, scroll, cols) {
73
93
  let skip = Math.max(0, Math.floor(scroll));
74
94
  const picked = [];
75
95
  let used = 0;
96
+ // The pane above the panel is flexGrow:1 and overflow="hidden" with
97
+ // justifyContent="flex-end", so only the TAIL of a too-tall entry stays
98
+ // visible. `used` must therefore count the VISIBLE rows of the entry being
99
+ // kept, not its full height: counting the full height while scrolling
100
+ // INSIDE a tall entry exceeded the reserved height and pushed the status /
101
+ // input rows off a short terminal (the caret then drifted under the box).
76
102
  for (let i = entries.length - 1; i >= 0; i--) {
77
103
  const r = entryRows(entries[i], cols);
104
+ let visibleRows = r;
78
105
  if (skip > 0) {
79
106
  if (skip >= r) {
80
107
  skip -= r;
81
108
  continue;
82
109
  }
83
- // Partially scrolled into this entry: keep the hidden part accounted.
110
+ // Partially scrolled into this entry: the rows above the fold are
111
+ // hidden and must not be counted against the viewport.
112
+ visibleRows = r - skip;
84
113
  skip = 0;
85
114
  }
86
- if (used + r > cap && picked.length > 0)
115
+ // The pane is flexGrow:1 + overflow="hidden" + flex-end, so a tall entry
116
+ // is CLIPPED to its tail and never grows the window. A clipped entry is
117
+ // therefore always kept — dropping it blanked the pane while the operator
118
+ // scrolled inside a long answer (the old code required a free row for it).
119
+ const clipped = r > cap || visibleRows < r;
120
+ if (!clipped && used + visibleRows > cap)
87
121
  break;
88
122
  picked.push(entries[i]);
89
- used += r;
90
- if (used >= cap)
123
+ used += visibleRows;
124
+ // Only an EXACT fill stops the walk. A clipped entry already fills the
125
+ // viewport visually but leaves room in the budget for the shorter entries
126
+ // above it, which used to be dropped right after a tall one.
127
+ if (!clipped && used >= cap)
91
128
  break;
92
129
  }
93
130
  picked.reverse();
package/dist/undo.js CHANGED
@@ -17,7 +17,17 @@ export class UndoStore {
17
17
  }
18
18
  async _readIndex() {
19
19
  try {
20
- return JSON.parse(await fs.readFile(INDEX, 'utf-8'));
20
+ const data = JSON.parse(await fs.readFile(INDEX, 'utf-8'));
21
+ // The index is a JSON FILE: a crash, a hand edit or an old format can
22
+ // leave VALID JSON that is not an array (`{}`, `null`, `"x"`, `42`),
23
+ // or an array with junk entries. Returning it as-is made /undo throw
24
+ // `history.pop is not a function` and /undo-list throw
25
+ // `history.slice is not a function` out of the command dispatcher.
26
+ if (!Array.isArray(data))
27
+ return [];
28
+ return data.filter((r) => !!r &&
29
+ typeof r === 'object' &&
30
+ typeof r.originalPath === 'string');
21
31
  }
22
32
  catch {
23
33
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.82.48",
3
+ "version": "2.82.50",
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": {