zames_pro 2.66.1 → 2.67.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +192 -129
- package/README.md +2 -1
- package/dist/attachments.js +0 -17
- package/dist/context.js +0 -3
- package/dist/extraTools.js +10 -11
- package/dist/fsutil.js +38 -1
- package/dist/gitTools.js +83 -26
- package/dist/index.js +18 -18
- package/dist/input.js +134 -30
- package/dist/mcp.js +49 -10
- package/dist/permissions.js +40 -1
- package/dist/run-task.js +6 -0
- package/dist/sandbox.js +49 -0
- package/dist/scheduler.js +8 -1
- package/dist/self-review.js +37 -17
- package/dist/shell.js +74 -32
- package/dist/tools.js +40 -20
- package/dist/transcript.js +15 -2
- package/dist/web.js +107 -38
- package/package.json +1 -2
- package/scripts/build-readme.mts +30 -51
- package/scripts/coverage-gate.mjs +4 -0
- package/README.ru.md +0 -971
package/dist/gitTools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
1
|
+
import { exec, execFile } from 'child_process';
|
|
2
2
|
const BS = String.fromCharCode(92);
|
|
3
3
|
export function runGit(cmd, cwd, timeout = 15_000) {
|
|
4
4
|
return new Promise((resolve) => {
|
|
@@ -36,15 +36,44 @@ export function runGit(cmd, cwd, timeout = 15_000) {
|
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// Run git with an argument ARRAY (no shell). Use this whenever an argument
|
|
40
|
+
// comes from the model: the shell never sees it, so $(...), backticks and a
|
|
41
|
+
// leading `-` (option injection) cannot turn a read-only call into a command.
|
|
42
|
+
export function runGitArgs(args, cwd, timeout = 15_000) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
execFile('git', args, {
|
|
45
|
+
cwd,
|
|
46
|
+
timeout,
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
52
|
+
GIT_PAGER: 'cat',
|
|
53
|
+
PAGER: 'cat',
|
|
54
|
+
},
|
|
55
|
+
}, (err, stdout, stderr) => {
|
|
56
|
+
const out = (stdout || '').toString();
|
|
57
|
+
const errStr = (stderr || '').toString();
|
|
58
|
+
if (!err) {
|
|
59
|
+
const combined = (out + errStr).trim();
|
|
60
|
+
resolve(combined || '(command produced no output)');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const parts = [];
|
|
64
|
+
if (err.killed)
|
|
65
|
+
parts.push(`⏱ Timeout ${timeout}ms`);
|
|
66
|
+
else if (err.code !== undefined && err.code !== null)
|
|
67
|
+
parts.push(`Exit code: ${err.code}`);
|
|
68
|
+
else
|
|
69
|
+
parts.push(`Error: ${err.message}`);
|
|
70
|
+
if (out.trim())
|
|
71
|
+
parts.push(out.trim());
|
|
72
|
+
if (errStr.trim())
|
|
73
|
+
parts.push(errStr.trim());
|
|
74
|
+
resolve(parts.join(String.fromCharCode(10)));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
48
77
|
}
|
|
49
78
|
export async function getGitContext(workdir) {
|
|
50
79
|
const probe = await runGit('git rev-parse --is-inside-work-tree', workdir, 5000);
|
|
@@ -93,7 +122,12 @@ export function formatGitContext(ctx) {
|
|
|
93
122
|
if (ctx.changedFiles > 0) {
|
|
94
123
|
s += `\n- Uncommitted changes: ${ctx.changedFiles} file(s)
|
|
95
124
|
`;
|
|
96
|
-
|
|
125
|
+
// Choose a fence longer than any backtick run in the preview: a filename
|
|
126
|
+
// containing ``` would otherwise close the block early and corrupt the
|
|
127
|
+
// system-prompt markdown.
|
|
128
|
+
const longest = (ctx.statusPreview.match(/`+/g) || []).reduce((a, b) => Math.max(a, b.length), 0);
|
|
129
|
+
const fence = '`'.repeat(Math.max(3, longest + 1));
|
|
130
|
+
s += fence + '\n' + ctx.statusPreview + '\n' + fence;
|
|
97
131
|
}
|
|
98
132
|
else {
|
|
99
133
|
s += '\n- Working tree: clean';
|
|
@@ -130,12 +164,17 @@ export function createGitTools(workdir) {
|
|
|
130
164
|
const err = await ensureRepo();
|
|
131
165
|
if (err)
|
|
132
166
|
return err;
|
|
133
|
-
const
|
|
167
|
+
const args = ['diff'];
|
|
134
168
|
if (staged)
|
|
135
|
-
|
|
136
|
-
if (p)
|
|
137
|
-
|
|
138
|
-
|
|
169
|
+
args.push('--staged');
|
|
170
|
+
if (p) {
|
|
171
|
+
const rel = String(p);
|
|
172
|
+
if (rel.startsWith('-')) {
|
|
173
|
+
return `Error: invalid path: ${rel}`;
|
|
174
|
+
}
|
|
175
|
+
args.push('--', rel);
|
|
176
|
+
}
|
|
177
|
+
return runGitArgs(args, workdir, 20_000);
|
|
139
178
|
},
|
|
140
179
|
},
|
|
141
180
|
{
|
|
@@ -159,9 +198,17 @@ export function createGitTools(workdir) {
|
|
|
159
198
|
if (err)
|
|
160
199
|
return err;
|
|
161
200
|
const r = ref && String(ref).trim() ? String(ref).trim() : 'HEAD';
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
201
|
+
// Validate the ref so a value like `--output=/tmp/x` or `HEAD$(...)`
|
|
202
|
+
// cannot reach git as an option/substitution. `git show` treats `--`
|
|
203
|
+
// as a paths separator, not a ref separator, so it is NOT used here.
|
|
204
|
+
if (r.startsWith('-') || !/^[A-Za-z0-9._/~^-]+$/.test(r)) {
|
|
205
|
+
return `Error: invalid ref: ${r}`;
|
|
206
|
+
}
|
|
207
|
+
const args = ['show'];
|
|
208
|
+
if (stat)
|
|
209
|
+
args.push('--stat');
|
|
210
|
+
args.push(r);
|
|
211
|
+
return runGitArgs(args, workdir, 20_000);
|
|
165
212
|
},
|
|
166
213
|
},
|
|
167
214
|
{
|
|
@@ -187,9 +234,17 @@ export function createGitTools(workdir) {
|
|
|
187
234
|
const err = await ensureRepo();
|
|
188
235
|
if (err)
|
|
189
236
|
return err;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
237
|
+
if (paths && String(paths).trim()) {
|
|
238
|
+
const list = (Array.isArray(paths) ? paths : [paths])
|
|
239
|
+
.map((v) => String(v).trim())
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
for (const p of list) {
|
|
242
|
+
if (p.startsWith('-'))
|
|
243
|
+
return `Error: invalid path: ${p}`;
|
|
244
|
+
}
|
|
245
|
+
return runGitArgs(['add', '--', ...list], workdir, 20_000);
|
|
246
|
+
}
|
|
247
|
+
return runGit('git add -A', workdir, 20_000);
|
|
193
248
|
},
|
|
194
249
|
},
|
|
195
250
|
{
|
|
@@ -218,7 +273,7 @@ export function createGitTools(workdir) {
|
|
|
218
273
|
const tmp = path.join(os.tmpdir(), `dsa-commit-${Date.now()}.txt`);
|
|
219
274
|
await fs.writeFile(tmp, msg, 'utf-8');
|
|
220
275
|
try {
|
|
221
|
-
const result = await
|
|
276
|
+
const result = await runGitArgs(['commit', '-F', tmp], workdir, 30_000);
|
|
222
277
|
return `add: ${addResult}\ncommit: ${result}`;
|
|
223
278
|
}
|
|
224
279
|
finally {
|
|
@@ -246,9 +301,11 @@ export function createGitTools(workdir) {
|
|
|
246
301
|
if (!/^[A-Za-z0-9._/-]+$/.test(b) || b.startsWith('-')) {
|
|
247
302
|
return `Error: invalid branch name: ${b}`;
|
|
248
303
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
304
|
+
const args = ['push'];
|
|
305
|
+
if (setUpstream)
|
|
306
|
+
args.push('-u');
|
|
307
|
+
args.push('origin', b);
|
|
308
|
+
return runGitArgs(args, workdir, 120_000);
|
|
252
309
|
},
|
|
253
310
|
},
|
|
254
311
|
];
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, statSync } from 'fs';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { theme } from './theme.js';
|
|
8
8
|
import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
|
|
9
|
-
import { createTools } from './tools.js';
|
|
9
|
+
import { createTools, mergeTools } from './tools.js';
|
|
10
10
|
import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from './extraTools.js';
|
|
11
11
|
import { runAgentLoop } from './agent-loop.js';
|
|
12
12
|
import { createSpinner } from './spinner.js';
|
|
@@ -994,17 +994,16 @@ async function main() {
|
|
|
994
994
|
if (debug)
|
|
995
995
|
console.error(e.stack);
|
|
996
996
|
await browser.close().catch(() => { });
|
|
997
|
-
transcript.close();
|
|
997
|
+
await transcript.close();
|
|
998
998
|
process.exit(1);
|
|
999
999
|
}
|
|
1000
1000
|
// One-shot mode
|
|
1001
1001
|
if (task) {
|
|
1002
|
-
const
|
|
1002
|
+
const baseTools = mod.createTools(currentWorkdir, {
|
|
1003
1003
|
undo,
|
|
1004
1004
|
readOnly: planMode,
|
|
1005
1005
|
});
|
|
1006
|
-
|
|
1007
|
-
tools.push(...mcpPool.tools);
|
|
1006
|
+
const tools = mergeTools(baseTools, mcpPool?.tools, planMode);
|
|
1008
1007
|
let freshChat = true;
|
|
1009
1008
|
let sendSystemPrompt = true;
|
|
1010
1009
|
// By default we start a new chat. To continue a previous session —
|
|
@@ -1031,7 +1030,7 @@ async function main() {
|
|
|
1031
1030
|
}
|
|
1032
1031
|
}
|
|
1033
1032
|
await autoReload();
|
|
1034
|
-
await mod.runTask(runTaskDeps(), browser, tools, task, currentWorkdir, {
|
|
1033
|
+
const result = await mod.runTask(runTaskDeps(), browser, tools, task, currentWorkdir, {
|
|
1035
1034
|
transcript,
|
|
1036
1035
|
freshChat,
|
|
1037
1036
|
sendSystemPrompt,
|
|
@@ -1041,11 +1040,15 @@ async function main() {
|
|
|
1041
1040
|
saveLastChat(chatId, currentWorkdir);
|
|
1042
1041
|
},
|
|
1043
1042
|
});
|
|
1043
|
+
// One-shot mode (--task) is documented for scripts/CI. A failed agent run
|
|
1044
|
+
// used to exit 0, so the CI job stayed green; surface it as exit code 1.
|
|
1045
|
+
if (!result.ok)
|
|
1046
|
+
process.exitCode = 1;
|
|
1044
1047
|
await browser.close();
|
|
1045
1048
|
await mod.closeWeb().catch(() => { });
|
|
1046
1049
|
if (mcpPool)
|
|
1047
1050
|
await mcpPool.close().catch(() => { });
|
|
1048
|
-
transcript.close();
|
|
1051
|
+
await transcript.close();
|
|
1049
1052
|
return;
|
|
1050
1053
|
}
|
|
1051
1054
|
console.log(theme.system(t('msg.interactive')));
|
|
@@ -1203,7 +1206,7 @@ async function main() {
|
|
|
1203
1206
|
await mod.closeWeb().catch(() => { });
|
|
1204
1207
|
if (mcpPool)
|
|
1205
1208
|
await mcpPool.close().catch(() => { });
|
|
1206
|
-
transcript.close();
|
|
1209
|
+
await transcript.close();
|
|
1207
1210
|
console.log(theme.dim(t('msg.exit_summary', {
|
|
1208
1211
|
transcript: transcript.file || t('common.off'),
|
|
1209
1212
|
chat: currentChatId || t('chats.not_created'),
|
|
@@ -2817,13 +2820,12 @@ async function main() {
|
|
|
2817
2820
|
console.log(theme.system(t('retry.running')));
|
|
2818
2821
|
// Re-send the last task into the SAME chat (no new chat, no system
|
|
2819
2822
|
// prompt), so a truncated/empty answer can be retried cheaply.
|
|
2820
|
-
const
|
|
2823
|
+
const baseTools = mod.createTools(currentWorkdir, {
|
|
2821
2824
|
undo,
|
|
2822
2825
|
todos: todoStore,
|
|
2823
2826
|
readOnly: planMode,
|
|
2824
2827
|
});
|
|
2825
|
-
|
|
2826
|
-
tools.push(...mcpPool.tools);
|
|
2828
|
+
const tools = mergeTools(baseTools, mcpPool?.tools, planMode);
|
|
2827
2829
|
if (editor)
|
|
2828
2830
|
editor.busy = true;
|
|
2829
2831
|
try {
|
|
@@ -3027,13 +3029,12 @@ async function main() {
|
|
|
3027
3029
|
continue;
|
|
3028
3030
|
}
|
|
3029
3031
|
console.log(theme.system(t('improve.start', { id: item.id, title: item.title })));
|
|
3030
|
-
const
|
|
3032
|
+
const improveBase = mod.createTools(currentWorkdir, {
|
|
3031
3033
|
undo,
|
|
3032
3034
|
todos: todoStore,
|
|
3033
3035
|
readOnly: planMode,
|
|
3034
3036
|
});
|
|
3035
|
-
|
|
3036
|
-
improveTools.push(...mcpPool.tools);
|
|
3037
|
+
const improveTools = mergeTools(improveBase, mcpPool?.tools, planMode);
|
|
3037
3038
|
if (editor)
|
|
3038
3039
|
editor.busy = true;
|
|
3039
3040
|
try {
|
|
@@ -3367,13 +3368,12 @@ async function main() {
|
|
|
3367
3368
|
if (cp)
|
|
3368
3369
|
transcript.log('checkpoint', { stamp: cp.stamp });
|
|
3369
3370
|
}
|
|
3370
|
-
const
|
|
3371
|
+
const baseTools = mod.createTools(currentWorkdir, {
|
|
3371
3372
|
undo,
|
|
3372
3373
|
todos: todoStore,
|
|
3373
3374
|
readOnly: planMode,
|
|
3374
3375
|
});
|
|
3375
|
-
|
|
3376
|
-
tools.push(...mcpPool.tools);
|
|
3376
|
+
const tools = mergeTools(baseTools, mcpPool?.tools, planMode);
|
|
3377
3377
|
if (editor)
|
|
3378
3378
|
editor.busy = true;
|
|
3379
3379
|
try {
|
|
@@ -3450,7 +3450,7 @@ async function main() {
|
|
|
3450
3450
|
await mod.closeWeb().catch(() => { });
|
|
3451
3451
|
if (mcpPool)
|
|
3452
3452
|
await mcpPool.close().catch(() => { });
|
|
3453
|
-
transcript.close();
|
|
3453
|
+
await transcript.close();
|
|
3454
3454
|
// One dim line so the operator can find the log and the session after
|
|
3455
3455
|
// the run (previously the path was only available via /transcript).
|
|
3456
3456
|
console.log(theme.dim(t('msg.exit_summary', {
|
package/dist/input.js
CHANGED
|
@@ -3,12 +3,16 @@ import { renderMarkdown } from './markdown.js';
|
|
|
3
3
|
import { randomThinkingPhrase, stripEllipsis, DOTS, renderDots, } from './spinner.js';
|
|
4
4
|
import { translate } from './i18n.js';
|
|
5
5
|
import { AttachmentStore, parseImagePaste, looksLikeFilePath, } from './attachments.js';
|
|
6
|
-
import { NL, CR, ESC, PASTE_START, PASTE_END, SUGGEST_PAGE, isSubsequence, pasteReplacement, expandPastes, safeJson, visRows, visLen, layoutInput, formatTokenStatus, tokenStatusLevel, truncateToWidth, CONTEXT_LIMIT, } from './input/layout.js';
|
|
6
|
+
import { NL, CR, ESC, PASTE_START, PASTE_END, SUGGEST_PAGE, isSubsequence, pasteReplacement, expandPastes, safeJson, visRows, visLen, charWidth, layoutInput, formatTokenStatus, tokenStatusLevel, truncateToWidth, CONTEXT_LIMIT, } from './input/layout.js';
|
|
7
7
|
// The pure layout/formatting helpers (visual rows, cursor position, visible
|
|
8
8
|
// width, paste markers, token formatting) live in ./input/layout.js. They are
|
|
9
9
|
// re-exported here so every existing import from './input.js' keeps working,
|
|
10
10
|
// and so tests can import them from either module (C3 step 3).
|
|
11
11
|
export { PASTE_MIN_LINES, SUGGEST_PAGE, isSubsequence, countPasteLines, formatPasteMarker, pasteReplacement, expandPastes, visRows, visLen, charWidth, layoutInput, tokenStatusLevel, formatTokenStatus, formatCompactTokens, truncateToWidth, CONTEXT_LIMIT, CONTEXT_YELLOW_PCT, CONTEXT_RED_PCT, } from './input/layout.js';
|
|
12
|
+
// How long a lone ESC is held before it is treated as the Escape key (and not
|
|
13
|
+
// as the head of a control sequence split across two reads). 40ms is below a
|
|
14
|
+
// human "two Escapes" interval but above typical chunk-split latency.
|
|
15
|
+
export const ESC_DISAMBIGUATE_MS = 40;
|
|
12
16
|
// A permanent input line at the bottom of the terminal + a status/output area above it.
|
|
13
17
|
//
|
|
14
18
|
// Why a custom editor:
|
|
@@ -87,6 +91,11 @@ export class LineEditor {
|
|
|
87
91
|
// is stored so it can be removed in dispose().
|
|
88
92
|
_onResize;
|
|
89
93
|
_resizeTimer;
|
|
94
|
+
// ESC disambiguation (N29): a read may end on a bare ESC that is really the
|
|
95
|
+
// head of a control sequence split across two reads. We hold it briefly and
|
|
96
|
+
// fire onEscape only if no tail arrives.
|
|
97
|
+
_escPending;
|
|
98
|
+
_escTimer;
|
|
90
99
|
// History of sent messages (for the up/down arrows).
|
|
91
100
|
history;
|
|
92
101
|
_histIndex;
|
|
@@ -195,6 +204,8 @@ export class LineEditor {
|
|
|
195
204
|
this._wasRaw = false;
|
|
196
205
|
this._onData = (b) => this._handle(b);
|
|
197
206
|
this._resizeTimer = null;
|
|
207
|
+
this._escPending = false;
|
|
208
|
+
this._escTimer = null;
|
|
198
209
|
this._onResize = () => {
|
|
199
210
|
// Debounce: a window drag fires a burst of SIGWINCH events. We wait a
|
|
200
211
|
// short moment, then redraw the block at the NEW width.
|
|
@@ -462,6 +473,19 @@ export class LineEditor {
|
|
|
462
473
|
}
|
|
463
474
|
this._renderInputOnly();
|
|
464
475
|
}
|
|
476
|
+
// PageUp/PageDown: page the suggestion window by a full SUGGEST_PAGE. When
|
|
477
|
+
// no list is open it is a no-op (PageUp/Down used to be swallowed entirely).
|
|
478
|
+
_suggestPage(delta) {
|
|
479
|
+
const sugg = this._suggestions();
|
|
480
|
+
if (!sugg.length)
|
|
481
|
+
return;
|
|
482
|
+
const max = sugg.length - 1;
|
|
483
|
+
const base = this._suggestSelected < 0 ? 0 : this._suggestSelected;
|
|
484
|
+
const next = base + delta * SUGGEST_PAGE;
|
|
485
|
+
this._suggestSelected = Math.max(0, Math.min(max, next));
|
|
486
|
+
this._suggestOffset = Math.max(0, Math.min(this._suggestSelected, sugg.length - SUGGEST_PAGE));
|
|
487
|
+
this._renderInputOnly();
|
|
488
|
+
}
|
|
465
489
|
// Tab: complete the command up to the common prefix; if there is a single
|
|
466
490
|
// match — insert it whole and add a space.
|
|
467
491
|
_completeCommand() {
|
|
@@ -526,6 +550,8 @@ export class LineEditor {
|
|
|
526
550
|
process.stdout.removeListener('resize', this._onResize);
|
|
527
551
|
if (this._resizeTimer)
|
|
528
552
|
clearTimeout(this._resizeTimer);
|
|
553
|
+
if (this._escTimer)
|
|
554
|
+
clearTimeout(this._escTimer);
|
|
529
555
|
process.stdout.write(ESC + '[?2004l');
|
|
530
556
|
this._stopDots();
|
|
531
557
|
if (this.rendered)
|
|
@@ -1314,36 +1340,54 @@ export class LineEditor {
|
|
|
1314
1340
|
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
1315
1341
|
return lay.cursorRow === lay.rows.length - 1;
|
|
1316
1342
|
}
|
|
1343
|
+
// Bare ESC: wait a moment for a possible control-sequence tail before firing
|
|
1344
|
+
// onEscape. Without this a sequence split across two reads (ESC, then "[A")
|
|
1345
|
+
// looked like a lone Escape and aborted the current generation (N29).
|
|
1346
|
+
_scheduleEscape() {
|
|
1347
|
+
this._escPending = true;
|
|
1348
|
+
if (this._escTimer)
|
|
1349
|
+
clearTimeout(this._escTimer);
|
|
1350
|
+
this._escTimer = setTimeout(() => {
|
|
1351
|
+
this._escTimer = null;
|
|
1352
|
+
if (!this._escPending)
|
|
1353
|
+
return;
|
|
1354
|
+
this._escPending = false;
|
|
1355
|
+
if (this.onEscape)
|
|
1356
|
+
this.onEscape();
|
|
1357
|
+
}, ESC_DISAMBIGUATE_MS);
|
|
1358
|
+
if (this._escTimer.unref)
|
|
1359
|
+
this._escTimer.unref();
|
|
1360
|
+
}
|
|
1361
|
+
// Move the cursor `delta` visual rows (not logical lines) and land in the
|
|
1362
|
+
// column closest to the current one. A long line wraps over several visual
|
|
1363
|
+
// rows; the old _up/_down moved by LOGICAL lines only, so on a wrapped row
|
|
1364
|
+
// Up did nothing and Down skipped a whole logical line. The column is
|
|
1365
|
+
// measured in COLUMNS (layoutInput's cursorCol), so wide chars/emoji land on
|
|
1366
|
+
// the right cell. Returns false when the target row is off the buffer.
|
|
1367
|
+
_visualMoveRows(delta) {
|
|
1368
|
+
const cols = process.stdout.columns || 80;
|
|
1369
|
+
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
1370
|
+
const target = lay.cursorRow + delta;
|
|
1371
|
+
if (target < 0 || target >= lay.rows.length)
|
|
1372
|
+
return false;
|
|
1373
|
+
const row = lay.rows[target];
|
|
1374
|
+
const rowChars = Array.from(row.text);
|
|
1375
|
+
const promptW = visLen(row.prefix);
|
|
1376
|
+
const want = Math.max(0, lay.cursorCol - promptW);
|
|
1377
|
+
let idx = row.start;
|
|
1378
|
+
let w = 0;
|
|
1379
|
+
while (idx < row.start + rowChars.length && w < want) {
|
|
1380
|
+
w += charWidth(rowChars[idx - row.start].codePointAt(0));
|
|
1381
|
+
idx++;
|
|
1382
|
+
}
|
|
1383
|
+
this.cursor = idx;
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1317
1386
|
_up() {
|
|
1318
|
-
|
|
1319
|
-
let start = this.cursor;
|
|
1320
|
-
while (start > 0 && chars[start - 1] !== NL)
|
|
1321
|
-
start--;
|
|
1322
|
-
if (start === 0)
|
|
1323
|
-
return;
|
|
1324
|
-
const col = this.cursor - start;
|
|
1325
|
-
const prevEnd = start - 1;
|
|
1326
|
-
let prevStart = prevEnd;
|
|
1327
|
-
while (prevStart > 0 && chars[prevStart - 1] !== NL)
|
|
1328
|
-
prevStart--;
|
|
1329
|
-
this.cursor = prevStart + Math.min(col, prevEnd - prevStart);
|
|
1387
|
+
this._visualMoveRows(-1);
|
|
1330
1388
|
}
|
|
1331
1389
|
_down() {
|
|
1332
|
-
|
|
1333
|
-
let end = this.cursor;
|
|
1334
|
-
while (end < chars.length && chars[end] !== NL)
|
|
1335
|
-
end++;
|
|
1336
|
-
if (end >= chars.length)
|
|
1337
|
-
return;
|
|
1338
|
-
let lineStart = this.cursor;
|
|
1339
|
-
while (lineStart > 0 && chars[lineStart - 1] !== NL)
|
|
1340
|
-
lineStart--;
|
|
1341
|
-
const col = this.cursor - lineStart;
|
|
1342
|
-
const nextStart = end + 1;
|
|
1343
|
-
let nextEnd = nextStart;
|
|
1344
|
-
while (nextEnd < chars.length && chars[nextEnd] !== NL)
|
|
1345
|
-
nextEnd++;
|
|
1346
|
-
this.cursor = nextStart + Math.min(col, nextEnd - nextStart);
|
|
1390
|
+
this._visualMoveRows(1);
|
|
1347
1391
|
}
|
|
1348
1392
|
// Move one word back (Ctrl+Left / Alt+B): skip spaces
|
|
1349
1393
|
// on the left, then go to the start of the word.
|
|
@@ -1524,9 +1568,22 @@ export class LineEditor {
|
|
|
1524
1568
|
}
|
|
1525
1569
|
return;
|
|
1526
1570
|
}
|
|
1571
|
+
// A previous read ended on a bare ESC. That is ambiguous: the Escape key
|
|
1572
|
+
// or the head of a control sequence split across reads (arrows, Ctrl+Del).
|
|
1573
|
+
// Reattach the ESC and parse the whole sequence, so a split arrow does not
|
|
1574
|
+
// abort the generation (N29).
|
|
1575
|
+
if (this._escPending) {
|
|
1576
|
+
this._escPending = false;
|
|
1577
|
+
if (this._escTimer) {
|
|
1578
|
+
clearTimeout(this._escTimer);
|
|
1579
|
+
this._escTimer = null;
|
|
1580
|
+
}
|
|
1581
|
+
s = ESC + s;
|
|
1582
|
+
}
|
|
1527
1583
|
if (!this._inPaste && s === ESC && !this._searchMode) {
|
|
1528
|
-
|
|
1529
|
-
|
|
1584
|
+
// Bare ESC in its own chunk: wait a moment for the possible tail of a
|
|
1585
|
+
// control sequence before treating it as the Escape key.
|
|
1586
|
+
this._scheduleEscape();
|
|
1530
1587
|
return;
|
|
1531
1588
|
}
|
|
1532
1589
|
// While the reverse search (Ctrl+R) is active, the editor is in a modal
|
|
@@ -1728,6 +1785,12 @@ export class LineEditor {
|
|
|
1728
1785
|
this._renderInputOnly();
|
|
1729
1786
|
continue;
|
|
1730
1787
|
}
|
|
1788
|
+
if (code === 12) {
|
|
1789
|
+
// Ctrl+L — clear the screen and repaint the input block (the standard
|
|
1790
|
+
// shell binding; it was silently swallowed before).
|
|
1791
|
+
this.clearScreen();
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1731
1794
|
if (code === 127 || code === 8) {
|
|
1732
1795
|
this._pushUndo(true);
|
|
1733
1796
|
this._backspace();
|
|
@@ -1780,6 +1843,24 @@ export class LineEditor {
|
|
|
1780
1843
|
this._renderInputOnly();
|
|
1781
1844
|
continue;
|
|
1782
1845
|
}
|
|
1846
|
+
// Same CSI-u protocol for Delete: Ctrl+Delete is CSI 3;5u (Alt 3;3u),
|
|
1847
|
+
// plain Delete is CSI 3u — without the tilde. Missing these made the
|
|
1848
|
+
// key silently dead on kitty/foot/WezTerm, while the tilde forms above
|
|
1849
|
+
// worked (N27).
|
|
1850
|
+
if (s.startsWith('[3;5u') || s.startsWith('[3;3u')) {
|
|
1851
|
+
this._pushUndo();
|
|
1852
|
+
this._deleteWordRight();
|
|
1853
|
+
s = s.slice(5);
|
|
1854
|
+
this._renderInputOnly();
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
if (s.startsWith('[3u')) {
|
|
1858
|
+
this._pushUndo(true);
|
|
1859
|
+
this._delete();
|
|
1860
|
+
s = s.slice(3);
|
|
1861
|
+
this._renderInputOnly();
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1783
1864
|
if (s.startsWith('[D')) {
|
|
1784
1865
|
this._left();
|
|
1785
1866
|
s = s.slice(2);
|
|
@@ -1823,6 +1904,19 @@ export class LineEditor {
|
|
|
1823
1904
|
this._renderInputOnly();
|
|
1824
1905
|
continue;
|
|
1825
1906
|
}
|
|
1907
|
+
// PageUp / PageDown (ESC[5~ / ESC[6~) — page the slash-command
|
|
1908
|
+
// suggestion list, which is otherwise only reachable with Ctrl+N/P.
|
|
1909
|
+
// Without this the sequence fell through to the generic skip.
|
|
1910
|
+
if (s.startsWith('[5~')) {
|
|
1911
|
+
this._suggestPage(-1);
|
|
1912
|
+
s = s.slice(3);
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
if (s.startsWith('[6~')) {
|
|
1916
|
+
this._suggestPage(1);
|
|
1917
|
+
s = s.slice(3);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1826
1920
|
if (s.startsWith('[3~')) {
|
|
1827
1921
|
this._pushUndo(true);
|
|
1828
1922
|
this._delete();
|
|
@@ -1846,6 +1940,16 @@ export class LineEditor {
|
|
|
1846
1940
|
let j = 0;
|
|
1847
1941
|
while (j < s.length && !/[A-Za-z~]/.test(s[j]))
|
|
1848
1942
|
j++;
|
|
1943
|
+
const skipped = s.slice(0, j + 1);
|
|
1944
|
+
// Surface a swallowed sequence in debug mode: an unrecognized control
|
|
1945
|
+
// sequence used to be skipped SILENTLY, which is how a dead key looked
|
|
1946
|
+
// like a frozen editor with no way to tell why.
|
|
1947
|
+
if (process.env.ZAMES_DEBUG_KEYS && skipped.length > 1) {
|
|
1948
|
+
process.stderr.write(String.fromCharCode(10) +
|
|
1949
|
+
'[keys] unhandled: ' +
|
|
1950
|
+
JSON.stringify(skipped) +
|
|
1951
|
+
String.fromCharCode(10));
|
|
1952
|
+
}
|
|
1849
1953
|
s = s.slice(j + 1);
|
|
1850
1954
|
continue;
|
|
1851
1955
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -34,13 +34,6 @@ export function floatingVersionWarnings(servers) {
|
|
|
34
34
|
}
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
|
-
/** Names of the servers found in a config, before any connection. */
|
|
38
|
-
export async function listConfiguredServers(workdir) {
|
|
39
|
-
const files = await discoverConfigFiles(workdir);
|
|
40
|
-
const config = await loadConfigFiles(files);
|
|
41
|
-
const servers = config.mcpServers || {};
|
|
42
|
-
return Object.keys(servers).filter((n) => !servers[n].disabled);
|
|
43
|
-
}
|
|
44
37
|
// @playwright/mcp defaults its --user-data-dir to the SAME profile zames uses
|
|
45
38
|
// (~/.zames/profile). Two chromium instances on one profile break each other:
|
|
46
39
|
// the chat page shows "Something went wrong when opening your profile" and
|
|
@@ -179,14 +172,32 @@ export function describeParamsExternally(description, schema) {
|
|
|
179
172
|
}
|
|
180
173
|
}
|
|
181
174
|
/* ---------- result rendering ---------- */
|
|
175
|
+
// Cap on the MCP result string handed to the model. MCP results (e.g.
|
|
176
|
+
// browser_snapshot) can be hundreds of KB; without a cap a single call floods
|
|
177
|
+
// the context and speeds up rate-limiting/truncation. Chosen to match the
|
|
178
|
+
// magnitude of the built-in tools' truncation.
|
|
179
|
+
export const MCP_RESULT_LIMIT = 12_000;
|
|
180
|
+
// Truncate a tool-result string with an explicit marker (the model must be able
|
|
181
|
+
// to tell it is looking at a partial output). Exported for tests.
|
|
182
|
+
export function truncateMcpResult(s, limit = MCP_RESULT_LIMIT) {
|
|
183
|
+
const str = String(s);
|
|
184
|
+
if (str.length <= limit)
|
|
185
|
+
return str;
|
|
186
|
+
const head = str.slice(0, limit);
|
|
187
|
+
// Prefer a line boundary so a cut does not split a line in half.
|
|
188
|
+
const nl = head.lastIndexOf(String.fromCharCode(10));
|
|
189
|
+
const cut = nl > limit * 0.5 ? head.slice(0, nl + 1) : head;
|
|
190
|
+
const omitted = str.length - cut.length;
|
|
191
|
+
return cut + `\n[...truncated ${omitted} chars]`;
|
|
192
|
+
}
|
|
182
193
|
// MCP tool results are a list of content blocks (text, image, resource, ...)
|
|
183
194
|
// and an isError flag. The agent needs a single string for the tool result
|
|
184
195
|
// message. Images are summarized (the model cannot see them through
|
|
185
196
|
// DeepSeek web anyway), text is joined.
|
|
186
|
-
export function renderMcpResult(result) {
|
|
197
|
+
export function renderMcpResult(result, limit = MCP_RESULT_LIMIT) {
|
|
187
198
|
const r = (result || {});
|
|
188
199
|
if (r.toolResult !== undefined && !r.content) {
|
|
189
|
-
return formatResultString(r.toolResult, r.isError);
|
|
200
|
+
return truncateMcpResult(formatResultString(r.toolResult, r.isError), limit);
|
|
190
201
|
}
|
|
191
202
|
const parts = [];
|
|
192
203
|
for (const block of r.content || []) {
|
|
@@ -236,7 +247,7 @@ export function renderMcpResult(result) {
|
|
|
236
247
|
.filter((p) => p !== '')
|
|
237
248
|
.join(String.fromCharCode(10) + String.fromCharCode(10))
|
|
238
249
|
.trim();
|
|
239
|
-
return formatResultString(text || '(MCP tool returned no content)', r.isError);
|
|
250
|
+
return truncateMcpResult(formatResultString(text || '(MCP tool returned no content)', r.isError), limit);
|
|
240
251
|
}
|
|
241
252
|
function formatResultString(result, isError) {
|
|
242
253
|
const text = typeof result === 'string' ? result : safeJson(result);
|
|
@@ -258,6 +269,27 @@ function safeJson(v) {
|
|
|
258
269
|
export function qualifyToolName(server, tool) {
|
|
259
270
|
return server + '__' + tool.replace(/[^A-Za-z0-9_]/g, '_');
|
|
260
271
|
}
|
|
272
|
+
// Make every tool name unique IN PLACE. agent-loop resolves a call by name and
|
|
273
|
+
// takes the first match, so a duplicate silently shadows the later tool. The
|
|
274
|
+
// qualifier is not injective (see the call site), hence this guard. Pure;
|
|
275
|
+
// exported for tests.
|
|
276
|
+
export function dedupeToolNames(tools) {
|
|
277
|
+
const seen = new Set();
|
|
278
|
+
for (const t of tools) {
|
|
279
|
+
if (!seen.has(t.name)) {
|
|
280
|
+
seen.add(t.name);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
let n = 2;
|
|
284
|
+
let candidate = `${t.name}_${n}`;
|
|
285
|
+
while (seen.has(candidate)) {
|
|
286
|
+
n++;
|
|
287
|
+
candidate = `${t.name}_${n}`;
|
|
288
|
+
}
|
|
289
|
+
t.name = candidate;
|
|
290
|
+
seen.add(candidate);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
261
293
|
// Connect to ONE server and return a ready connection (or an error).
|
|
262
294
|
async function connectServer(name, cfg, debug) {
|
|
263
295
|
try {
|
|
@@ -352,6 +384,13 @@ export async function createMcpPool(opts) {
|
|
|
352
384
|
});
|
|
353
385
|
}
|
|
354
386
|
}
|
|
387
|
+
// Distinct servers can produce the SAME qualified name (server 'a' + tool
|
|
388
|
+
// 'b__c' vs server 'a__b' + tool 'c'), and agent-loop resolves a call with
|
|
389
|
+
// tools.find(name) — it takes the FIRST. A duplicate would make the second
|
|
390
|
+
// tool silently unreachable (the model calls "the right name", a DIFFERENT
|
|
391
|
+
// tool runs). Rename the later one with a numeric suffix so every name is
|
|
392
|
+
// unique.
|
|
393
|
+
dedupeToolNames(tools);
|
|
355
394
|
let closed = false;
|
|
356
395
|
return {
|
|
357
396
|
tools,
|