zames_pro 2.82.49 → 2.82.51
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 +60 -0
- package/README.md +159 -26
- package/SECURITY.md +50 -0
- package/dist/agent-loop.js +35 -5
- package/dist/answer-parse.js +1 -1
- package/dist/attach-flow.js +7 -3
- package/dist/attach-refs.js +30 -6
- package/dist/attachments.js +9 -0
- package/dist/browser-chats.js +9 -2
- package/dist/browser-dom.js +1 -1
- package/dist/browser-pure.js +9 -3
- package/dist/browser.js +1 -1
- package/dist/changelog.js +1 -1
- package/dist/checkpoint.js +1 -1
- package/dist/cli-args.js +1 -1
- package/dist/config-command.js +1 -1
- package/dist/config-commands.js +5 -2
- package/dist/config-live.js +10 -0
- package/dist/config.js +13 -1
- package/dist/context-command.js +6 -0
- package/dist/context.js +27 -4
- package/dist/diff.js +52 -9
- package/dist/extraTools.js +7 -0
- package/dist/glm-chats.js +18 -1
- package/dist/glm-net-capture.js +23 -5
- package/dist/goal.js +6 -1
- package/dist/i18n/deepseek.js +3 -3
- package/dist/i18n/self-review.js +4 -0
- package/dist/i18n.js +8 -2
- package/dist/index.js +90 -101
- package/dist/input/layout.js +33 -8
- package/dist/input/suggest.js +1 -1
- package/dist/input.js +1 -1
- package/dist/limits-command.js +42 -0
- package/dist/mcp.js +23 -4
- package/dist/model-command.js +21 -0
- package/dist/path-token.js +36 -9
- package/dist/permissions.js +32 -3
- package/dist/provider-command.js +29 -0
- package/dist/provider-handoff.js +18 -1
- package/dist/queue-command.js +15 -0
- package/dist/qwen-chats.js +13 -6
- package/dist/qwen-net-capture.js +13 -6
- package/dist/resume-command.js +38 -0
- package/dist/send-telemetry.js +15 -3
- package/dist/send-throttle.js +3 -1
- package/dist/status-command.js +68 -0
- package/dist/subagent.js +5 -12
- package/dist/tool-summary.js +8 -1
- package/dist/tools.js +13 -1
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/log-window.js +41 -4
- package/dist/undo.js +11 -1
- package/dist/web.js +16 -6
- package/llms-full.txt +679 -0
- package/llms.txt +102 -0
- package/package.json +84 -23
package/dist/path-token.js
CHANGED
|
@@ -89,7 +89,7 @@ function stripRefPunct(tok) {
|
|
|
89
89
|
t = t.slice(0, -1);
|
|
90
90
|
return t;
|
|
91
91
|
}
|
|
92
|
-
// Find all `@path` references in a task text
|
|
92
|
+
// Find all `@path` references in a task text. Returns the unique paths in
|
|
93
93
|
// order of appearance. A reference is an '@' at a word boundary followed by a
|
|
94
94
|
// token that looks like a file name/path (a known extension). Implemented
|
|
95
95
|
// without regular expressions, like extractPathToken, to keep the source free
|
|
@@ -107,35 +107,62 @@ export function extractAtFileRefs(text) {
|
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
109
|
const prev = i > 0 ? chars[i - 1] : '';
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
110
|
+
// The at-sign must START a reference: at the very beginning, after
|
|
111
|
+
// whitespace/a bracket/a quote, or right after a LIST SEPARATOR (a comma
|
|
112
|
+
// or a semicolon). A second at-sign WITHOUT a separator (`@a.ts@b.ts`) is
|
|
113
|
+
// an email/decorator shape and is skipped — the glued path cannot be
|
|
114
|
+
// inlined anyway.
|
|
115
|
+
if (prev && !isRefBoundary(prev) && prev !== ',' && prev !== ';') {
|
|
115
116
|
i++;
|
|
116
117
|
continue;
|
|
117
118
|
}
|
|
118
119
|
let j = i + 1;
|
|
119
120
|
let tok = '';
|
|
121
|
+
// The token is rejected WHOLE when it contains an at-sign that is NOT a
|
|
122
|
+
// list separator — `@Component` / `user@host` are a decorator and an
|
|
123
|
+
// email. Trimming the prefix at that at-sign would otherwise inline a
|
|
124
|
+
// bogus path.
|
|
125
|
+
let hadInnerAt = false;
|
|
126
|
+
// A token that contains an at-sign (`@Component`, `user@host`) is a
|
|
127
|
+
// decorator / an email, NOT a file reference. The scan used to stop at
|
|
128
|
+
// that at-sign and hand the PREFIX (`Component` … no) or, worse, the part
|
|
129
|
+
// BEFORE it to the inliner. Now the whole token is rejected instead.
|
|
130
|
+
const isRef = (t) => t.length > 0 && !t.includes('@');
|
|
120
131
|
while (j < n && !isRefEnd(chars[j])) {
|
|
121
132
|
// A COMMA directly followed by another `@` starts the NEXT reference:
|
|
122
133
|
// `@a.ts,@b.ts` is a list, and scanning to whitespace made the whole
|
|
123
134
|
// `a.ts,@b.ts` one path, so neither file was inlined.
|
|
124
135
|
if (chars[j] === ',' && chars[j + 1] === '@')
|
|
125
136
|
break;
|
|
137
|
+
// A SEMICOLON or a second at-sign also separates list items. Scanning
|
|
138
|
+
// to whitespace made `a.ts;@b.ts` ONE path whose extension consumed the
|
|
139
|
+
// separator, so `looksLikeToken` failed and BOTH files were dropped.
|
|
140
|
+
if ((chars[j] === ';' || chars[j] === '@') && chars[j + 1] === '@')
|
|
141
|
+
break;
|
|
142
|
+
// An at-sign followed by an ordinary char is part of the token (an
|
|
143
|
+
// email/decorator) — remember it so the token is rejected below.
|
|
144
|
+
if (chars[j] === '@')
|
|
145
|
+
hadInnerAt = true;
|
|
126
146
|
tok += chars[j];
|
|
127
147
|
j++;
|
|
128
148
|
}
|
|
129
149
|
const cleaned = stripRefPunct(tok);
|
|
130
|
-
if (
|
|
150
|
+
if (!hadInnerAt &&
|
|
151
|
+
isRef(cleaned) &&
|
|
152
|
+
looksLikeToken(cleaned) &&
|
|
153
|
+
!seen.has(cleaned)) {
|
|
131
154
|
seen.add(cleaned);
|
|
132
155
|
out.push(cleaned);
|
|
133
156
|
}
|
|
134
157
|
// The break above leaves `j` ON the comma; step past it so the next `@`
|
|
135
158
|
// is seen as the start of a reference. When the scan ended on a
|
|
136
159
|
// terminator instead, continue AFTER it.
|
|
137
|
-
|
|
138
|
-
|
|
160
|
+
// The scan stopped ON a separator that split the token (`,`, `;` or a
|
|
161
|
+
// second at-sign): resume AT it so the next part is scanned as its own
|
|
162
|
+
// reference. Continuing past it dropped the second file of a list.
|
|
163
|
+
const sep = j < n ? chars[j] : '';
|
|
164
|
+
if (j > i + 1 && (sep === ',' || sep === ';' || sep === '@'))
|
|
165
|
+
i = j;
|
|
139
166
|
else
|
|
140
167
|
i = j > i + 1 ? j : i + 1;
|
|
141
168
|
}
|
package/dist/permissions.js
CHANGED
|
@@ -156,14 +156,43 @@ export function decidePermission(policy, tool, args) {
|
|
|
156
156
|
// the natural shape for execFile-style MCP tools). Accepting only strings
|
|
157
157
|
// let a `command` deny be bypassed by passing the words as an array.
|
|
158
158
|
const commands = [];
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// Collect a value as a command string: a string is one command, an ARRAY
|
|
160
|
+
// (argv) is joined into one line so a rule can match the words in order.
|
|
161
|
+
const addCommand = (v) => {
|
|
161
162
|
if (typeof v === 'string')
|
|
162
163
|
commands.push(v);
|
|
163
164
|
else if (Array.isArray(v)) {
|
|
164
|
-
|
|
165
|
+
const parts = v.filter((x) => typeof x === 'string');
|
|
166
|
+
if (parts.length)
|
|
167
|
+
commands.push(parts.join(' '));
|
|
165
168
|
}
|
|
169
|
+
};
|
|
170
|
+
for (const k of COMMAND_KEYS) {
|
|
171
|
+
if (k in a)
|
|
172
|
+
addCommand(a[k]);
|
|
166
173
|
}
|
|
174
|
+
// MCP tools (and a model that nests its payload) send the command one or
|
|
175
|
+
// more levels down — `{args: {command}}`, `{input: {params: {cmd}}}`.
|
|
176
|
+
// Checking only the TOP level let a `command` deny rule be bypassed by
|
|
177
|
+
// wrapping the same words in an object, so nested objects and arrays are
|
|
178
|
+
// scanned too (depth-bounded like the path scan).
|
|
179
|
+
const walkCommands = (v, depth) => {
|
|
180
|
+
if (depth > 4 || v === null || v === undefined)
|
|
181
|
+
return;
|
|
182
|
+
if (Array.isArray(v)) {
|
|
183
|
+
for (const x of v)
|
|
184
|
+
walkCommands(x, depth + 1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (typeof v !== 'object')
|
|
188
|
+
return;
|
|
189
|
+
for (const [key, val] of Object.entries(v)) {
|
|
190
|
+
if (COMMAND_KEYS.includes(key))
|
|
191
|
+
addCommand(val);
|
|
192
|
+
walkCommands(val, depth + 1);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
walkCommands(a, 0);
|
|
167
196
|
const paths = collectPathCandidates(tool, args);
|
|
168
197
|
for (const r of policy.rules) {
|
|
169
198
|
// An UNPARSABLE regex in any field of the rule makes the rule fail
|
|
@@ -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
|
+
}
|
package/dist/provider-handoff.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/qwen-chats.js
CHANGED
|
@@ -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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
}
|
package/dist/qwen-net-capture.js
CHANGED
|
@@ -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
|
-
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
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 (
|
|
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
|
+
}
|
package/dist/send-telemetry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/send-throttle.js
CHANGED
|
@@ -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' &&
|
|
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
|
+
}
|
package/dist/subagent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadHooks, runLifecycleHooks } from './hooks.js';
|
|
2
2
|
import { markInternalChat } from './sessions.js';
|
|
3
|
-
// Subagent runner
|
|
3
|
+
// Subagent runner. A `Task` tool call from the model is handled
|
|
4
4
|
// by the agent loop as a SEAM (like onAutoCompact): it calls back into the
|
|
5
5
|
// caller, which owns the whole lifecycle. This module builds that callback.
|
|
6
6
|
//
|
|
@@ -101,17 +101,10 @@ export function createSubagentRunner(opts) {
|
|
|
101
101
|
// parent chat id) returned BEFORE anything ran, and charging those
|
|
102
102
|
// attempts burned the whole budget on runs that never started.
|
|
103
103
|
used++;
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
transcript?.log('subagent_start', {
|
|
109
|
-
type: req.type,
|
|
110
|
-
description: req.description || '',
|
|
111
|
-
promptChars: String(req.prompt || '').length,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
catch { }
|
|
104
|
+
// NOTE: `subagent_start` is NOT logged here. agent-loop already writes it
|
|
105
|
+
// for the `Task` seam (with the description and the prompt), so logging it
|
|
106
|
+
// here too put a DUPLICATE event for the same run into the transcript —
|
|
107
|
+
// /export rendered every delegation twice. The seam owns the event.
|
|
115
108
|
const subTools = buildTools(req.type === 'explore');
|
|
116
109
|
const taskText = subagentPersona(req.type) + '\n\n--- SUB-TASK ---\n' + req.prompt;
|
|
117
110
|
// Save the parent's send hooks: the nested loop OVERWRITES them on
|
package/dist/tool-summary.js
CHANGED
|
@@ -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
|
@@ -70,6 +70,15 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
|
|
|
70
70
|
if (v === undefined || v === null) {
|
|
71
71
|
throw new Error(`Missing required argument: ${name}. Add "${name}" to the tool call arguments and retry.`);
|
|
72
72
|
}
|
|
73
|
+
// The contract says `path: string`/`command: string`; nothing used to
|
|
74
|
+
// enforce it, so an OBJECT became "[object Object]" and the file tools
|
|
75
|
+
// CREATED a file with that literal name (the same family as the old
|
|
76
|
+
// `undefined` file). A garbled tool call is exactly this shape.
|
|
77
|
+
if (typeof v === 'object' || typeof v === 'function') {
|
|
78
|
+
throw new Error(`Invalid required argument ${name}: expected a string, got ` +
|
|
79
|
+
JSON.stringify(v) +
|
|
80
|
+
'. Re-send the call with a plain string value.');
|
|
81
|
+
}
|
|
73
82
|
const s = String(v);
|
|
74
83
|
if (s === '' || s === 'undefined' || s === 'null') {
|
|
75
84
|
throw new Error(`Invalid required argument ${name}: ${JSON.stringify(v)}. Provide a non-empty value for "${name}" and retry.`);
|
|
@@ -417,7 +426,10 @@ export function filterToolsForReadOnly(tools) {
|
|
|
417
426
|
export function filterToolsByAllowList(tools, allowed) {
|
|
418
427
|
if (!allowed || !allowed.length)
|
|
419
428
|
return tools;
|
|
420
|
-
|
|
429
|
+
// `allowed-tools:` is human-written: an entry may carry surrounding spaces
|
|
430
|
+
// (a quoted value, a trailing comma). Lower-casing alone left `read `, which
|
|
431
|
+
// matched nothing and silently reduced the skill to `respond` only.
|
|
432
|
+
const allow = new Set(allowed.map((n) => String(n).trim().toLowerCase()).filter(Boolean));
|
|
421
433
|
allow.add('respond');
|
|
422
434
|
return tools.filter((t) => allow.has(t.name.toLowerCase()));
|
|
423
435
|
}
|
package/dist/tui/StatusBar.js
CHANGED
|
@@ -27,7 +27,7 @@ function stateLabel(state, locale) {
|
|
|
27
27
|
* status). Measured with the shared `visLen` so wide chars (CJK/emoji) count
|
|
28
28
|
* as 2 and the emoji's text-presentation selector (\uFE0E) as 0 — a plain
|
|
29
29
|
* `.length` counted an emoji as 1 and a CJK badge was clipped on a narrow
|
|
30
|
-
* terminal
|
|
30
|
+
* terminal. Exported so the width arithmetic is unit-tested
|
|
31
31
|
* without rendering. */
|
|
32
32
|
export function badgeTextWidth(state) {
|
|
33
33
|
let w = 0;
|
package/dist/tui/log-window.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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 +=
|
|
90
|
-
|
|
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
|
-
|
|
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/dist/web.js
CHANGED
|
@@ -654,22 +654,32 @@ export function parseDuckDuckGoResults(body, limit) {
|
|
|
654
654
|
// single regex made the snippet group OPTIONAL with an unbounded gap, so a
|
|
655
655
|
// result without a snippet picked up the FOLLOWING result's snippet (wrong
|
|
656
656
|
// text under a URL). A row scan ties every snippet to its own result.
|
|
657
|
-
|
|
657
|
+
// A result anchor is `<a … class="… result__a|result__snippet …" …>`. The
|
|
658
|
+
// attribute VALUES may be single-quoted (HTML allows both, and a page served
|
|
659
|
+
// through a proxy does), so each quote style is accepted rather than only
|
|
660
|
+
// `class="…"`. Rejecting them made a valid page parse to zero results and
|
|
661
|
+
// WebSearch answer "the output format may have changed".
|
|
662
|
+
const anchorRe = /<a[^>]*class=(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi;
|
|
658
663
|
const hrefOf = (tag) => {
|
|
659
|
-
const m = tag.match(/href="([^"]*)"/i);
|
|
660
|
-
return m ? m[1] : '';
|
|
664
|
+
const m = tag.match(/href=(?:"([^"]*)"|'([^']*)')/i);
|
|
665
|
+
return m ? (m[1] ?? m[2] ?? '') : '';
|
|
661
666
|
};
|
|
662
667
|
let cur = null;
|
|
663
668
|
let m;
|
|
664
669
|
while ((m = anchorRe.exec(body))) {
|
|
665
|
-
const
|
|
670
|
+
const classes = m[1] ?? m[2] ?? '';
|
|
671
|
+
const kindMatch = /result__(a|snippet)/i.exec(classes);
|
|
672
|
+
if (!kindMatch)
|
|
673
|
+
continue;
|
|
674
|
+
const kind = kindMatch[1].toLowerCase();
|
|
675
|
+
const text = m[3] || '';
|
|
666
676
|
if (kind === 'a') {
|
|
667
677
|
if (cur && cur.title && /^https?:\/\//i.test(cur.url)) {
|
|
668
678
|
results.push(cur);
|
|
669
679
|
if (results.length >= limit)
|
|
670
680
|
return results;
|
|
671
681
|
}
|
|
672
|
-
cur = { title: htmlToText(
|
|
682
|
+
cur = { title: htmlToText(text).trim(), url: '', snippet: '' };
|
|
673
683
|
// The href lives in the OPENING tag, which m[0] starts with.
|
|
674
684
|
let href = hrefOf(m[0].slice(0, m[0].indexOf('>') + 1));
|
|
675
685
|
const uddg = href.match(/[?&]uddg=([^&]+)/);
|
|
@@ -685,7 +695,7 @@ export function parseDuckDuckGoResults(body, limit) {
|
|
|
685
695
|
continue;
|
|
686
696
|
}
|
|
687
697
|
if (cur && !cur.snippet)
|
|
688
|
-
cur.snippet = htmlToText(
|
|
698
|
+
cur.snippet = htmlToText(text).trim();
|
|
689
699
|
}
|
|
690
700
|
if (cur && cur.title && /^https?:\/\//i.test(cur.url))
|
|
691
701
|
results.push(cur);
|