zames_pro 2.20.0 → 2.21.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/dist/agent-loop.js +20 -12
- package/dist/attachments.js +80 -17
- package/dist/browser.js +55 -12
- package/dist/commands.js +7 -2
- package/dist/config-menu.js +3 -1
- package/dist/config.js +157 -23
- package/dist/context.js +68 -13
- package/dist/extraTools.js +8 -2
- package/dist/i18n.js +745 -187
- package/dist/index.js +37 -16
- package/dist/input.js +144 -22
- package/dist/mcp.js +16 -4
- package/dist/path-token.js +4 -3
- package/dist/self-review.js +6 -6
- package/dist/spinner.js +8 -2
- package/dist/system-prompt.js +19 -5
- package/dist/tools.js +2 -6
- package/dist/web.js +1 -1
- package/dist/xml-toolcall.js +10 -5
- package/package.json +13 -2
- package/scripts/eslint-ts6.mjs +51 -0
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { DeepSeekBrowser } from './browser.js';
|
|
|
8
8
|
import { createTools } from './tools.js';
|
|
9
9
|
import { runAgentLoop } from './agent-loop.js';
|
|
10
10
|
import { createSpinner } from './spinner.js';
|
|
11
|
-
import { LineEditor, expandPastes, pasteReplacement } from './input.js';
|
|
11
|
+
import { LineEditor, expandPastes, pasteReplacement, } from './input.js';
|
|
12
12
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
|
|
13
13
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
14
14
|
import { runConfigMenu } from './config-menu.js';
|
|
@@ -370,7 +370,9 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
370
370
|
body = '';
|
|
371
371
|
}
|
|
372
372
|
const header = 'Follow the skill "' + skill.name + '" (from ' + skill.path + ').';
|
|
373
|
-
const extra = rest
|
|
373
|
+
const extra = rest
|
|
374
|
+
? '\n\nAdditional instructions from the operator: ' + rest
|
|
375
|
+
: '';
|
|
374
376
|
return header + '\n\n' + body + extra;
|
|
375
377
|
}
|
|
376
378
|
}
|
|
@@ -403,7 +405,8 @@ async function resolveAttachPath(workdir, raw) {
|
|
|
403
405
|
let s = String(raw || '').trim();
|
|
404
406
|
if (!s)
|
|
405
407
|
return null;
|
|
406
|
-
if ((s.startsWith('"') && s.endsWith('"')) ||
|
|
408
|
+
if ((s.startsWith('"') && s.endsWith('"')) ||
|
|
409
|
+
(s.startsWith("'") && s.endsWith("'"))) {
|
|
407
410
|
s = s.slice(1, -1);
|
|
408
411
|
}
|
|
409
412
|
s = s.replace(/\\ /g, ' ');
|
|
@@ -1016,7 +1019,10 @@ async function main() {
|
|
|
1016
1019
|
mcpPool = await mod.createMcpPool({ workdir: currentWorkdir });
|
|
1017
1020
|
const st = mcpPool.status();
|
|
1018
1021
|
if (st.toolCount > 0) {
|
|
1019
|
-
const names = st.servers
|
|
1022
|
+
const names = st.servers
|
|
1023
|
+
.filter((x) => !x.error)
|
|
1024
|
+
.map((x) => x.name)
|
|
1025
|
+
.join(', ');
|
|
1020
1026
|
console.log(theme.system(t('mcp.loaded', { n: String(st.toolCount), servers: names })));
|
|
1021
1027
|
}
|
|
1022
1028
|
for (const srv of st.servers) {
|
|
@@ -1117,8 +1123,6 @@ async function main() {
|
|
|
1117
1123
|
// Messages the user typed while the agent worked. runTask takes them one
|
|
1118
1124
|
// by one after the current task finishes.
|
|
1119
1125
|
const pendingQueue = [];
|
|
1120
|
-
// Attachments for the next submit (filled by the editor's onAttachments).
|
|
1121
|
-
let pendingAttachments = [];
|
|
1122
1126
|
// Show the "no clipboard image" hint only once per session.
|
|
1123
1127
|
let clipboardWarned = false;
|
|
1124
1128
|
// ---------- review mode state ----------
|
|
@@ -1185,7 +1189,9 @@ async function main() {
|
|
|
1185
1189
|
let tail;
|
|
1186
1190
|
if (reviewMode) {
|
|
1187
1191
|
tail =
|
|
1188
|
-
theme.warn(t('prompt.review')) +
|
|
1192
|
+
theme.warn(t('prompt.review')) +
|
|
1193
|
+
theme.dim(':') +
|
|
1194
|
+
theme.dir(reviewMode.snapName);
|
|
1189
1195
|
}
|
|
1190
1196
|
else {
|
|
1191
1197
|
tail = theme.dir(dirLabel(currentWorkdir));
|
|
@@ -1217,7 +1223,10 @@ async function main() {
|
|
|
1217
1223
|
mime: image.mime,
|
|
1218
1224
|
size: image.data.length,
|
|
1219
1225
|
});
|
|
1220
|
-
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1226
|
+
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1227
|
+
marker: att.marker,
|
|
1228
|
+
size: formatSize(image.data.length),
|
|
1229
|
+
})));
|
|
1221
1230
|
return att;
|
|
1222
1231
|
}
|
|
1223
1232
|
// Case 2: the paste is a path to a local file (drag&drop or copy path).
|
|
@@ -1235,7 +1244,11 @@ async function main() {
|
|
|
1235
1244
|
mime,
|
|
1236
1245
|
size: data.length,
|
|
1237
1246
|
});
|
|
1238
|
-
ed.printAbove(theme.system(t('msg.attached_file', {
|
|
1247
|
+
ed.printAbove(theme.system(t('msg.attached_file', {
|
|
1248
|
+
marker: att.marker,
|
|
1249
|
+
name,
|
|
1250
|
+
size: formatSize(data.length),
|
|
1251
|
+
})));
|
|
1239
1252
|
return att;
|
|
1240
1253
|
};
|
|
1241
1254
|
ed.onClipboard = async () => {
|
|
@@ -1932,10 +1945,16 @@ async function main() {
|
|
|
1932
1945
|
console.log(theme.system(t('mcp.title', { n: String(st.toolCount) })));
|
|
1933
1946
|
for (const srv of st.servers) {
|
|
1934
1947
|
if (srv.error) {
|
|
1935
|
-
console.log(' ' +
|
|
1948
|
+
console.log(' ' +
|
|
1949
|
+
theme.user(srv.name) +
|
|
1950
|
+
' ' +
|
|
1951
|
+
theme.dim(t('mcp.status_error', { v: srv.error })));
|
|
1936
1952
|
}
|
|
1937
1953
|
else {
|
|
1938
|
-
console.log(' ' +
|
|
1954
|
+
console.log(' ' +
|
|
1955
|
+
theme.user(srv.name) +
|
|
1956
|
+
' ' +
|
|
1957
|
+
theme.dim('(' + String(srv.tools.length) + ')'));
|
|
1939
1958
|
}
|
|
1940
1959
|
}
|
|
1941
1960
|
continue;
|
|
@@ -2132,7 +2151,7 @@ async function main() {
|
|
|
2132
2151
|
continue;
|
|
2133
2152
|
}
|
|
2134
2153
|
if (lower === '/diff' || lower.startsWith('/diff ')) {
|
|
2135
|
-
const staged = lower.indexOf(
|
|
2154
|
+
const staged = lower.indexOf('--staged') !== -1;
|
|
2136
2155
|
const { runGit } = await import('./gitTools.js');
|
|
2137
2156
|
const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
|
|
2138
2157
|
if (probe.trim() !== 'true') {
|
|
@@ -2196,14 +2215,14 @@ async function main() {
|
|
|
2196
2215
|
}
|
|
2197
2216
|
}
|
|
2198
2217
|
catch {
|
|
2199
|
-
gitOk
|
|
2218
|
+
// git is not available here; gitOk stays false.
|
|
2200
2219
|
}
|
|
2201
2220
|
let clipboardTool = null;
|
|
2202
2221
|
try {
|
|
2203
2222
|
clipboardTool = hasClipboardTool() ? 'available' : null;
|
|
2204
2223
|
}
|
|
2205
2224
|
catch {
|
|
2206
|
-
clipboardTool
|
|
2225
|
+
// not available; clipboardTool stays null
|
|
2207
2226
|
}
|
|
2208
2227
|
const mcpStatus = mcpPool
|
|
2209
2228
|
? mcpPool.status()
|
|
@@ -2283,7 +2302,9 @@ async function main() {
|
|
|
2283
2302
|
// A model "answer" that is actually an error/abort sentinel is not a
|
|
2284
2303
|
// summary - do not carry it over.
|
|
2285
2304
|
if (!summary || /^\(прервано пользователем\)$/.test(summary)) {
|
|
2286
|
-
console.error(theme.error(t('compact.summary_failed', {
|
|
2305
|
+
console.error(theme.error(t('compact.summary_failed', {
|
|
2306
|
+
v: summary || t('common.unknown'),
|
|
2307
|
+
})));
|
|
2287
2308
|
continue;
|
|
2288
2309
|
}
|
|
2289
2310
|
transcript.log('compact_summary', {
|
|
@@ -2335,7 +2356,7 @@ async function main() {
|
|
|
2335
2356
|
}
|
|
2336
2357
|
if (lower === '/review' || lower.startsWith('/review ')) {
|
|
2337
2358
|
const rest = trimmed.slice('/review'.length).trim();
|
|
2338
|
-
const staged = rest.indexOf(
|
|
2359
|
+
const staged = rest.indexOf('--staged') !== -1;
|
|
2339
2360
|
const focus = rest.replace(/--staged/g, '').trim();
|
|
2340
2361
|
const reviewTask = buildReviewPrompt(focus, staged);
|
|
2341
2362
|
const reviewTools = mod.createTools(currentWorkdir, { undo });
|
package/dist/input.js
CHANGED
|
@@ -36,7 +36,11 @@ const PASTE_END = ESC + '[201~';
|
|
|
36
36
|
export const PASTE_MIN_LINES = 3;
|
|
37
37
|
// Number of lines in a pasted block (the trailing newline doesn't add a line).
|
|
38
38
|
export function countPasteLines(raw) {
|
|
39
|
-
const normalized = String(raw)
|
|
39
|
+
const normalized = String(raw)
|
|
40
|
+
.split(CR + NL)
|
|
41
|
+
.join(NL)
|
|
42
|
+
.split(CR)
|
|
43
|
+
.join(NL);
|
|
40
44
|
const trimmed = normalized.replace(/\n+$/, '');
|
|
41
45
|
if (trimmed === '')
|
|
42
46
|
return 1;
|
|
@@ -49,7 +53,11 @@ export function formatPasteMarker(lines) {
|
|
|
49
53
|
// otherwise { marker, text }: the marker goes into the input line, and the
|
|
50
54
|
// original text is expanded back on submit.
|
|
51
55
|
export function pasteReplacement(raw) {
|
|
52
|
-
const text = String(raw)
|
|
56
|
+
const text = String(raw)
|
|
57
|
+
.split(CR + NL)
|
|
58
|
+
.join(NL)
|
|
59
|
+
.split(CR)
|
|
60
|
+
.join(NL);
|
|
53
61
|
const lines = countPasteLines(text);
|
|
54
62
|
if (lines < PASTE_MIN_LINES)
|
|
55
63
|
return null;
|
|
@@ -204,7 +212,9 @@ export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
|
|
|
204
212
|
}
|
|
205
213
|
else if (n >= 1_000) {
|
|
206
214
|
const k = n / 1_000;
|
|
207
|
-
compact =
|
|
215
|
+
compact =
|
|
216
|
+
(k >= 100 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '')) +
|
|
217
|
+
'k';
|
|
208
218
|
}
|
|
209
219
|
else {
|
|
210
220
|
compact = String(n);
|
|
@@ -232,6 +242,13 @@ export class LineEditor {
|
|
|
232
242
|
_animating;
|
|
233
243
|
_dotPhase;
|
|
234
244
|
_thinkBase;
|
|
245
|
+
// The last rendered status row(s) (status + right-aligned context). Cached so
|
|
246
|
+
// a re-render driven by TYPING (the buffer changed, the status did not) can
|
|
247
|
+
// skip erasing and re-printing the status line entirely. On terminals like
|
|
248
|
+
// Tabby a full erase+rewrite on every keystroke makes the whole screen blink;
|
|
249
|
+
// keeping the status untouched removes that flicker. Cleared whenever the
|
|
250
|
+
// status genuinely changes so the cache can never go stale.
|
|
251
|
+
_lastStatusBlock;
|
|
235
252
|
_wasRaw;
|
|
236
253
|
_onData;
|
|
237
254
|
// History of sent messages (for the up/down arrows).
|
|
@@ -270,7 +287,7 @@ export class LineEditor {
|
|
|
270
287
|
// A callback the editor calls to fetch the CURRENT token count before every
|
|
271
288
|
// status render, so the status line stays fresh without the caller polling.
|
|
272
289
|
onContextQuery;
|
|
273
|
-
constructor({ prompt = '> ', commands = [], locale = 'ru' } = {}) {
|
|
290
|
+
constructor({ prompt = '> ', commands = [], locale = 'ru', } = {}) {
|
|
274
291
|
this.locale = locale;
|
|
275
292
|
this.promptStr = prompt;
|
|
276
293
|
this.buf = '';
|
|
@@ -288,6 +305,7 @@ export class LineEditor {
|
|
|
288
305
|
this._animating = false;
|
|
289
306
|
this._dotPhase = 0;
|
|
290
307
|
this._thinkBase = '';
|
|
308
|
+
this._lastStatusBlock = '';
|
|
291
309
|
this._wasRaw = false;
|
|
292
310
|
this._onData = (b) => this._handle(b);
|
|
293
311
|
this.history = [];
|
|
@@ -488,36 +506,71 @@ export class LineEditor {
|
|
|
488
506
|
}
|
|
489
507
|
process.stdout.write(CR + ESC + '[J');
|
|
490
508
|
this.rendered = false;
|
|
509
|
+
this._lastStatusBlock = '';
|
|
510
|
+
}
|
|
511
|
+
// Erase ONLY the input rows (leave the status block untouched). Used when a
|
|
512
|
+
// keystroke re-renders the line but the status/context did not change: the
|
|
513
|
+
// status stays on screen, only the input area is redrawn. This removes the
|
|
514
|
+
// full-screen flicker on terminals like Tabby, where erasing the whole block
|
|
515
|
+
// on every character made the display blink.
|
|
516
|
+
_eraseInputOnly(inputRows) {
|
|
517
|
+
if (!this.rendered)
|
|
518
|
+
return;
|
|
519
|
+
const rows = process.stdout.rows || 24;
|
|
520
|
+
const up = Math.min(Math.max(0, inputRows - 1), Math.max(0, rows - 1));
|
|
521
|
+
if (up > 0)
|
|
522
|
+
process.stdout.write(ESC + '[' + up + 'A');
|
|
523
|
+
process.stdout.write(CR + ESC + '[J');
|
|
524
|
+
this.rendered = false;
|
|
491
525
|
}
|
|
492
526
|
_writeBlock() {
|
|
493
527
|
const cols = process.stdout.columns || 80;
|
|
494
|
-
let out = '';
|
|
495
|
-
let top = 0;
|
|
496
528
|
// Status line above the input: the spinner/answer text on the left and the
|
|
497
529
|
// token context right-aligned on the SAME row (10k · 12%). The context is
|
|
498
530
|
// refreshed from onContextQuery() on every render, so it follows the live
|
|
499
531
|
// DeepSeek counter without a polling timer of its own.
|
|
532
|
+
//
|
|
533
|
+
// `top` MUST count EXACTLY the rows we print, or the next _eraseBlock()
|
|
534
|
+
// moves the cursor to the wrong row and the block (spinner + input) is
|
|
535
|
+
// re-drawn on top of the previous one instead of replacing it — the
|
|
536
|
+
// operator sees the status line duplicated/stacking, most visibly during
|
|
537
|
+
// the send-pause countdown (which re-renders about once per second).
|
|
538
|
+
// The earlier code computed `top = visRows(status + ' ' + ctxText)` even
|
|
539
|
+
// though it PRINTS `status + ' '.repeat(space) + ctxText` with a DIFFERENT
|
|
540
|
+
// total width, so at certain widths `top` disagreed with the printed rows.
|
|
541
|
+
// Both branches below now derive `top` from the exact string they print.
|
|
500
542
|
const ctxText = this._contextText();
|
|
543
|
+
let statusOut = '';
|
|
544
|
+
let top = 0;
|
|
545
|
+
// Never fill a row to the FULL terminal width: on terminals with
|
|
546
|
+
// autowrap (Tabby, iTerm, Windows Terminal) a row that reaches the last
|
|
547
|
+
// column wraps the cursor to the next line, so the following erase moves
|
|
548
|
+
// to the wrong row and the status line STACKS on screen. Keep one column
|
|
549
|
+
// free (effective width = cols - 1) for the right-aligned context and for
|
|
550
|
+
// the padded status row.
|
|
551
|
+
const usable = Math.max(1, cols - 1);
|
|
501
552
|
if (this.statusText) {
|
|
502
553
|
if (ctxText) {
|
|
503
554
|
// Right-align the context on the SAME row as the status. When the
|
|
504
555
|
// status is too long to leave room, do NOT cram them together (that
|
|
505
556
|
// pushed the trailing `%` past the right edge and it got truncated):
|
|
506
557
|
// put the context on its own line instead.
|
|
507
|
-
const space =
|
|
558
|
+
const space = usable - visLen(this.statusText) - visLen(ctxText);
|
|
508
559
|
if (space >= 2) {
|
|
509
|
-
|
|
510
|
-
|
|
560
|
+
const row = this.statusText + ' '.repeat(space) + ctxText;
|
|
561
|
+
statusOut = row + NL;
|
|
562
|
+
top = visRows(row, cols);
|
|
511
563
|
}
|
|
512
564
|
else {
|
|
513
|
-
|
|
565
|
+
statusOut = this.statusText + NL;
|
|
514
566
|
top = visRows(this.statusText, cols);
|
|
515
|
-
|
|
516
|
-
|
|
567
|
+
const ctxRow = ' '.repeat(Math.max(0, usable - visLen(ctxText))) + ctxText;
|
|
568
|
+
statusOut += ctxRow + NL;
|
|
569
|
+
top += visRows(ctxRow, cols);
|
|
517
570
|
}
|
|
518
571
|
}
|
|
519
572
|
else {
|
|
520
|
-
|
|
573
|
+
statusOut = this.statusText + NL;
|
|
521
574
|
// The status may wrap onto several lines — we account for this,
|
|
522
575
|
// otherwise the block erase misses and statuses pile up.
|
|
523
576
|
top = visRows(this.statusText, cols);
|
|
@@ -526,9 +579,57 @@ export class LineEditor {
|
|
|
526
579
|
else if (ctxText) {
|
|
527
580
|
// Idle: no spinner, but the context still belongs on its own line just
|
|
528
581
|
// above the input, right-aligned.
|
|
529
|
-
|
|
530
|
-
|
|
582
|
+
const ctxRow = ' '.repeat(Math.max(0, usable - visLen(ctxText))) + ctxText;
|
|
583
|
+
statusOut = ctxRow + NL;
|
|
584
|
+
top = visRows(ctxRow, cols);
|
|
585
|
+
}
|
|
586
|
+
// Incremental re-render: when the status block is byte-identical to what is
|
|
587
|
+
// already on screen (the common case while typing — the buffer changes, the
|
|
588
|
+
// spinner/context does not) we redraw ONLY the input rows. Erasing and
|
|
589
|
+
// re-printing the whole block on every keystroke is what made the display
|
|
590
|
+
// flicker on terminals like Tabby. `top` is recovered from the cache so the
|
|
591
|
+
// cursor math stays correct.
|
|
592
|
+
const statusBlock = statusOut;
|
|
593
|
+
const statusUnchanged = this.rendered && statusBlock === this._lastStatusBlock;
|
|
594
|
+
if (statusUnchanged) {
|
|
595
|
+
// Count the real input rows via layout (wrapping aware).
|
|
596
|
+
const layOnly = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
597
|
+
const inputLineRows = layOnly.rows.length;
|
|
598
|
+
const suggOnly = this._suggestions();
|
|
599
|
+
const shownOnly = suggOnly.slice(0, 8);
|
|
600
|
+
const linesBelowOnly = shownOnly.length
|
|
601
|
+
? shownOnly.length + (suggOnly.length > shownOnly.length ? 1 : 0)
|
|
602
|
+
: 0;
|
|
603
|
+
this._eraseInputOnly(inputLineRows + linesBelowOnly);
|
|
604
|
+
let outOnly = '';
|
|
605
|
+
outOnly += layOnly.rows.map((r) => r.prefix + r.text).join(NL);
|
|
606
|
+
if (shownOnly.length) {
|
|
607
|
+
const maxName = Math.max(...shownOnly.map((c) => c.name.length));
|
|
608
|
+
const lines = shownOnly.map((c) => {
|
|
609
|
+
const name = theme.prompt(c.name.padEnd(maxName));
|
|
610
|
+
const desc = theme.dim(' ' + c.description);
|
|
611
|
+
return ' ' + name + desc;
|
|
612
|
+
});
|
|
613
|
+
outOnly += NL + lines.join(NL);
|
|
614
|
+
const hidden = suggOnly.length - shownOnly.length;
|
|
615
|
+
if (hidden > 0)
|
|
616
|
+
outOnly +=
|
|
617
|
+
NL +
|
|
618
|
+
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
619
|
+
}
|
|
620
|
+
process.stdout.write(outOnly);
|
|
621
|
+
const upOnly = layOnly.rows.length - 1 - layOnly.cursorRow + linesBelowOnly;
|
|
622
|
+
if (upOnly > 0)
|
|
623
|
+
process.stdout.write(ESC + '[' + upOnly + 'A');
|
|
624
|
+
process.stdout.write(CR);
|
|
625
|
+
if (layOnly.cursorCol > 0)
|
|
626
|
+
process.stdout.write(ESC + '[' + layOnly.cursorCol + 'C');
|
|
627
|
+
this.rendered = true;
|
|
628
|
+
this.cursorRowFromTop = top + layOnly.cursorRow;
|
|
629
|
+
return;
|
|
531
630
|
}
|
|
631
|
+
let out = statusOut;
|
|
632
|
+
this._lastStatusBlock = statusBlock;
|
|
532
633
|
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
533
634
|
out += lay.rows.map((r) => r.prefix + r.text).join(NL);
|
|
534
635
|
// We draw the slash-command hints BELOW the input line. We then move the
|
|
@@ -546,13 +647,17 @@ export class LineEditor {
|
|
|
546
647
|
out += NL + lines.join(NL);
|
|
547
648
|
const hidden = sugg.length - shown.length;
|
|
548
649
|
if (hidden > 0)
|
|
549
|
-
out +=
|
|
650
|
+
out +=
|
|
651
|
+
NL +
|
|
652
|
+
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
550
653
|
}
|
|
551
654
|
process.stdout.write(out);
|
|
552
655
|
const lastRow = lay.rows.length - 1;
|
|
553
656
|
// Move the cursor up: first to the input line within lay, then further by
|
|
554
657
|
// the hint lines (if any) — the cursor must sit on the input.
|
|
555
|
-
const linesBelow = shown.length
|
|
658
|
+
const linesBelow = shown.length
|
|
659
|
+
? shown.length + (sugg.length > shown.length ? 1 : 0)
|
|
660
|
+
: 0;
|
|
556
661
|
const up = lastRow - lay.cursorRow + linesBelow;
|
|
557
662
|
if (up > 0)
|
|
558
663
|
process.stdout.write(ESC + '[' + up + 'A');
|
|
@@ -663,7 +768,12 @@ export class LineEditor {
|
|
|
663
768
|
assistant(msg) {
|
|
664
769
|
this.stop();
|
|
665
770
|
const rendered = renderMarkdown(msg);
|
|
666
|
-
this.printAbove(NL +
|
|
771
|
+
this.printAbove(NL +
|
|
772
|
+
theme.assistant(translate(this.locale)('editor.answer')) +
|
|
773
|
+
NL +
|
|
774
|
+
rendered +
|
|
775
|
+
NL +
|
|
776
|
+
theme.dim('─'.repeat(60)));
|
|
667
777
|
}
|
|
668
778
|
// Warning to the operator (e.g. the agent stopped suspiciously).
|
|
669
779
|
// Printed above the input line without overwriting it.
|
|
@@ -707,7 +817,9 @@ export class LineEditor {
|
|
|
707
817
|
_insert(text) {
|
|
708
818
|
const chars = Array.from(this.buf);
|
|
709
819
|
const ins = Array.from(String(text));
|
|
710
|
-
const next = chars
|
|
820
|
+
const next = chars
|
|
821
|
+
.slice(0, this.cursor)
|
|
822
|
+
.concat(ins, chars.slice(this.cursor));
|
|
711
823
|
this.buf = next.join('');
|
|
712
824
|
this.cursor += ins.length;
|
|
713
825
|
}
|
|
@@ -751,7 +863,11 @@ export class LineEditor {
|
|
|
751
863
|
_insertFallback(raw) {
|
|
752
864
|
const rep = pasteReplacement(raw);
|
|
753
865
|
if (!rep) {
|
|
754
|
-
const text = String(raw)
|
|
866
|
+
const text = String(raw)
|
|
867
|
+
.split(CR + NL)
|
|
868
|
+
.join(NL)
|
|
869
|
+
.split(CR)
|
|
870
|
+
.join(NL);
|
|
755
871
|
this._insert(text);
|
|
756
872
|
return;
|
|
757
873
|
}
|
|
@@ -1186,9 +1302,15 @@ export function selftest() {
|
|
|
1186
1302
|
];
|
|
1187
1303
|
for (const t of cases) {
|
|
1188
1304
|
const r = layoutInput(t.p, t.b, t.c, t.w);
|
|
1189
|
-
console.log(JSON.stringify({
|
|
1305
|
+
console.log(JSON.stringify({
|
|
1306
|
+
rows: r.rows.length,
|
|
1307
|
+
row: r.cursorRow,
|
|
1308
|
+
col: r.cursorCol,
|
|
1309
|
+
}));
|
|
1190
1310
|
}
|
|
1191
1311
|
}
|
|
1192
|
-
if (process.argv[1] &&
|
|
1312
|
+
if (process.argv[1] &&
|
|
1313
|
+
process.argv[1].endsWith('input.js') &&
|
|
1314
|
+
process.argv.includes('--selftest')) {
|
|
1193
1315
|
selftest();
|
|
1194
1316
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -15,7 +15,10 @@ export function hardenPlaywrightArgs(command, args) {
|
|
|
15
15
|
const hay = (String(command || '') + ' ' + args.join(' ')).toLowerCase();
|
|
16
16
|
if (!hay.includes('playwright') || !hay.includes('mcp'))
|
|
17
17
|
return args;
|
|
18
|
-
if (args.some((a) => a === '--isolated' ||
|
|
18
|
+
if (args.some((a) => a === '--isolated' ||
|
|
19
|
+
a.startsWith('--user-data-dir') ||
|
|
20
|
+
a === '--extension' ||
|
|
21
|
+
a.startsWith('--cdp-endpoint'))) {
|
|
19
22
|
return args;
|
|
20
23
|
}
|
|
21
24
|
return [...args, '--isolated'];
|
|
@@ -161,7 +164,9 @@ export function renderMcpResult(result) {
|
|
|
161
164
|
parts.push(String(b.text ?? ''));
|
|
162
165
|
}
|
|
163
166
|
else if (type === 'image') {
|
|
164
|
-
parts.push('[image ' +
|
|
167
|
+
parts.push('[image ' +
|
|
168
|
+
String(b.mimeType ?? 'unknown') +
|
|
169
|
+
', written to disk by the MCP server]');
|
|
165
170
|
}
|
|
166
171
|
else if (type === 'resource') {
|
|
167
172
|
const res = (b.resource || {});
|
|
@@ -169,7 +174,11 @@ export function renderMcpResult(result) {
|
|
|
169
174
|
parts.push(String(res.uri ?? '') + String.fromCharCode(10) + res.text);
|
|
170
175
|
}
|
|
171
176
|
else if (typeof res.blob === 'string') {
|
|
172
|
-
parts.push('[binary resource ' +
|
|
177
|
+
parts.push('[binary resource ' +
|
|
178
|
+
String(res.uri ?? '') +
|
|
179
|
+
', ' +
|
|
180
|
+
String(res.mimeType ?? '') +
|
|
181
|
+
']');
|
|
173
182
|
}
|
|
174
183
|
else {
|
|
175
184
|
parts.push(String(res.uri ?? ''));
|
|
@@ -187,7 +196,10 @@ export function renderMcpResult(result) {
|
|
|
187
196
|
}
|
|
188
197
|
}
|
|
189
198
|
}
|
|
190
|
-
const text = parts
|
|
199
|
+
const text = parts
|
|
200
|
+
.filter((p) => p !== '')
|
|
201
|
+
.join(String.fromCharCode(10) + String.fromCharCode(10))
|
|
202
|
+
.trim();
|
|
191
203
|
return formatResultString(text || '(MCP tool returned no content)', r.isError);
|
|
192
204
|
}
|
|
193
205
|
function formatResultString(result, isError) {
|
package/dist/path-token.js
CHANGED
|
@@ -13,10 +13,9 @@ export function extractPathToken(text) {
|
|
|
13
13
|
i++;
|
|
14
14
|
continue;
|
|
15
15
|
}
|
|
16
|
-
let quote = '';
|
|
17
16
|
let start = i;
|
|
18
17
|
if (c === String.fromCharCode(39) || c === String.fromCharCode(34)) {
|
|
19
|
-
quote = c;
|
|
18
|
+
const quote = c;
|
|
20
19
|
start = i + 1;
|
|
21
20
|
i = start;
|
|
22
21
|
while (i < n && chars[i] !== quote)
|
|
@@ -63,7 +62,9 @@ function looksLikeToken(tok) {
|
|
|
63
62
|
if (ext.length > 8)
|
|
64
63
|
return false;
|
|
65
64
|
for (const ch of ext) {
|
|
66
|
-
const ok = (ch >= 'a' && ch <= 'z') ||
|
|
65
|
+
const ok = (ch >= 'a' && ch <= 'z') ||
|
|
66
|
+
(ch >= 'A' && ch <= 'Z') ||
|
|
67
|
+
(ch >= '0' && ch <= '9');
|
|
67
68
|
if (!ok)
|
|
68
69
|
return false;
|
|
69
70
|
}
|
package/dist/self-review.js
CHANGED
|
@@ -54,7 +54,7 @@ async function copyDirJsFiles(from, to) {
|
|
|
54
54
|
return copied;
|
|
55
55
|
}
|
|
56
56
|
// ---------- /self-review ----------
|
|
57
|
-
export async function selfReview({ browser, focus, transcript }) {
|
|
57
|
+
export async function selfReview({ browser, focus, transcript, }) {
|
|
58
58
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
59
59
|
const snapRoot = path.join(SNAP_ROOT);
|
|
60
60
|
const snapDir = path.join(snapRoot, `run-${stamp}`);
|
|
@@ -125,7 +125,7 @@ export async function selfReview({ browser, focus, transcript }) {
|
|
|
125
125
|
return { snapDir, reportPath, changed };
|
|
126
126
|
}
|
|
127
127
|
// ---------- /self-diff ----------
|
|
128
|
-
export async function selfDiff({ name }) {
|
|
128
|
+
export async function selfDiff({ name, }) {
|
|
129
129
|
const snapDir = path.join(SNAP_ROOT, name);
|
|
130
130
|
const snapStat = await fs.stat(snapDir).catch(() => null);
|
|
131
131
|
if (!snapStat)
|
|
@@ -154,7 +154,7 @@ export async function selfDiff({ name }) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
// ---------- /self-apply ----------
|
|
157
|
-
export async function selfApply({ name }) {
|
|
157
|
+
export async function selfApply({ name, }) {
|
|
158
158
|
const snapDir = path.join(SNAP_ROOT, name);
|
|
159
159
|
const snapStat = await fs.stat(snapDir).catch(() => null);
|
|
160
160
|
if (!snapStat)
|
|
@@ -180,9 +180,9 @@ export async function selfApply({ name }) {
|
|
|
180
180
|
console.log(theme.warn(`\nПерезапусти агента, чтобы изменения вступили в силу.\n`));
|
|
181
181
|
}
|
|
182
182
|
// ---------- /self-list ----------
|
|
183
|
-
export async function selfList({ config } = {}) {
|
|
183
|
+
export async function selfList({ config: _config, } = {}) {
|
|
184
184
|
const snapRoot = path.join(SNAP_ROOT);
|
|
185
|
-
let entries
|
|
185
|
+
let entries;
|
|
186
186
|
try {
|
|
187
187
|
entries = await fs.readdir(snapRoot);
|
|
188
188
|
}
|
|
@@ -230,7 +230,7 @@ async function diffFiles(origDir, newDir) {
|
|
|
230
230
|
}
|
|
231
231
|
return changed;
|
|
232
232
|
}
|
|
233
|
-
function buildReviewPrompt({ focus, snapDir }) {
|
|
233
|
+
function buildReviewPrompt({ focus, snapDir, }) {
|
|
234
234
|
const focusLine = focus
|
|
235
235
|
? `\nThe user asked you to focus especially on: ${focus}\n`
|
|
236
236
|
: '';
|
package/dist/spinner.js
CHANGED
|
@@ -108,7 +108,10 @@ export function createSpinner(locale = 'ru') {
|
|
|
108
108
|
// so the countdown refreshes without resetting the dots.
|
|
109
109
|
if (animating && dotTimer && spinner) {
|
|
110
110
|
animBase = theme.brown(stripEllipsis(label));
|
|
111
|
-
spinner.text =
|
|
111
|
+
spinner.text =
|
|
112
|
+
animBase +
|
|
113
|
+
theme.brown(DOTS[dotPhase] + DOTS_PAD.slice(DOTS[dotPhase].length)) +
|
|
114
|
+
HINT;
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
117
|
startAnimated(label);
|
|
@@ -130,7 +133,10 @@ export function createSpinner(locale = 'ru') {
|
|
|
130
133
|
toolResult: (result) => {
|
|
131
134
|
stop();
|
|
132
135
|
const text = typeof result === 'string' ? result : JSON.stringify(result);
|
|
133
|
-
const preview = text
|
|
136
|
+
const preview = text
|
|
137
|
+
.slice(0, 200)
|
|
138
|
+
.split(String.fromCharCode(10))
|
|
139
|
+
.join(' ↵ ');
|
|
134
140
|
console.log(theme.toolResult(' → ' + preview + String.fromCharCode(10)));
|
|
135
141
|
},
|
|
136
142
|
assistant: (msg) => {
|
package/dist/system-prompt.js
CHANGED
|
@@ -6,14 +6,20 @@ export function renderContextSection(context) {
|
|
|
6
6
|
let out = '';
|
|
7
7
|
for (const f of context.agents) {
|
|
8
8
|
out +=
|
|
9
|
-
'\n## Project instructions (' +
|
|
10
|
-
|
|
9
|
+
'\n## Project instructions (' +
|
|
10
|
+
f.path +
|
|
11
|
+
')\n\n' +
|
|
12
|
+
'The following come from ' +
|
|
13
|
+
f.path +
|
|
14
|
+
'. Follow them for this project unless the operator explicitly overrides them.\n\n' +
|
|
11
15
|
f.content +
|
|
12
16
|
'\n';
|
|
13
17
|
}
|
|
14
18
|
for (const f of context.memory) {
|
|
15
19
|
out +=
|
|
16
|
-
'\n## Memory (' +
|
|
20
|
+
'\n## Memory (' +
|
|
21
|
+
f.path +
|
|
22
|
+
')\n\n' +
|
|
17
23
|
'Durable notes accumulated across sessions. Treat as background knowledge; update this file when you learn something worth remembering.\n\n' +
|
|
18
24
|
f.content +
|
|
19
25
|
'\n';
|
|
@@ -21,7 +27,9 @@ export function renderContextSection(context) {
|
|
|
21
27
|
if (context.memory.length) {
|
|
22
28
|
out +=
|
|
23
29
|
'\nWhen you learn a durable fact about this project or the operator preferences (build quirks, ' +
|
|
24
|
-
'conventions, gotchas), append a short bullet to ' +
|
|
30
|
+
'conventions, gotchas), append a short bullet to ' +
|
|
31
|
+
context.memory[0].path +
|
|
32
|
+
' via Edit/Write so it survives ' +
|
|
25
33
|
'into future sessions. Keep entries concise.\n';
|
|
26
34
|
}
|
|
27
35
|
if (context.skills.length) {
|
|
@@ -29,7 +37,13 @@ export function renderContextSection(context) {
|
|
|
29
37
|
const tools = s.allowedTools && s.allowedTools.length
|
|
30
38
|
? ' [tools: ' + s.allowedTools.join(', ') + ']'
|
|
31
39
|
: '';
|
|
32
|
-
return '- ' +
|
|
40
|
+
return ('- ' +
|
|
41
|
+
s.name +
|
|
42
|
+
tools +
|
|
43
|
+
': ' +
|
|
44
|
+
(s.description || '(no description)') +
|
|
45
|
+
'\n file: ' +
|
|
46
|
+
s.path);
|
|
33
47
|
});
|
|
34
48
|
out +=
|
|
35
49
|
'\n## Skills\n\n' +
|
package/dist/tools.js
CHANGED
|
@@ -166,7 +166,7 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
166
166
|
old_base64: 'string?',
|
|
167
167
|
new_base64: 'string?',
|
|
168
168
|
},
|
|
169
|
-
fn: async ({ path: p, old_string, new_string, old_base64, new_base64 }) => {
|
|
169
|
+
fn: async ({ path: p, old_string, new_string, old_base64, new_base64, }) => {
|
|
170
170
|
const file = safe(req(p, 'path'));
|
|
171
171
|
const oldStr = decodeContent(old_string, old_base64);
|
|
172
172
|
const newStr = decodeContent(new_string, new_base64);
|
|
@@ -241,11 +241,7 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
241
241
|
const scope = searchPath ? '"' + target + '\\*' : '*';
|
|
242
242
|
return runShell(`findstr /s /n /r /c:"${escaped}" ` + scope);
|
|
243
243
|
}
|
|
244
|
-
const flags = mode === 'files_only'
|
|
245
|
-
? '-rlE'
|
|
246
|
-
: mode === 'count'
|
|
247
|
-
? '-rcE'
|
|
248
|
-
: '-rnE';
|
|
244
|
+
const flags = mode === 'files_only' ? '-rlE' : mode === 'count' ? '-rcE' : '-rnE';
|
|
249
245
|
const includeArg = inc ? ' --include=' + JSON.stringify(inc) : '';
|
|
250
246
|
return runShell(`grep ${flags}${includeArg} ${JSON.stringify(pat)} ` +
|
|
251
247
|
`${JSON.stringify(target)} || true`);
|
package/dist/web.js
CHANGED
|
@@ -43,7 +43,7 @@ function htmlToText(html) {
|
|
|
43
43
|
return s;
|
|
44
44
|
}
|
|
45
45
|
// fetch with redirects and a timeout
|
|
46
|
-
async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {} } = {}) {
|
|
46
|
+
async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {}, } = {}) {
|
|
47
47
|
const ctrl = new AbortController();
|
|
48
48
|
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
49
49
|
try {
|