zames_pro 2.6.0 → 2.7.0
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 +118 -82
- package/dist/browser.js +40 -41
- package/dist/config-menu.js +25 -25
- package/dist/config.js +5 -5
- package/dist/gitTools.js +4 -4
- package/dist/i18n.js +11 -7
- package/dist/index.js +153 -150
- package/dist/input.js +82 -77
- package/dist/markdown.js +1 -1
- package/dist/net-capture.js +20 -20
- package/dist/self-review.js +16 -16
- package/dist/sessions.js +11 -11
- package/dist/spinner.js +20 -16
- package/dist/system-prompt.js +23 -0
- package/dist/theme.js +26 -26
- package/dist/tools.js +9 -9
- package/dist/transcript.js +2 -2
- package/dist/types.js +2 -2
- package/dist/web.js +16 -16
- package/dist/xml-toolcall.js +14 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,12 +41,12 @@ function getPositional() {
|
|
|
41
41
|
return positional;
|
|
42
42
|
}
|
|
43
43
|
const config = loadConfig();
|
|
44
|
-
//
|
|
44
|
+
// Current interface/agent language. Changed by the /config lang <ru|en> command.
|
|
45
45
|
let currentLocale = isLocale(config.ui?.locale)
|
|
46
46
|
? config.ui.locale
|
|
47
47
|
: 'ru';
|
|
48
|
-
//
|
|
49
|
-
//
|
|
48
|
+
// Translation: reads currentLocale at call time, so a language change takes
|
|
49
|
+
// effect immediately, without a restart (for lines printed afterwards).
|
|
50
50
|
const t = (key, params) => translate(currentLocale)(key, params);
|
|
51
51
|
const headless = hasFlag('--headless') || config.headless;
|
|
52
52
|
const debug = hasFlag('--debug') || config.debug;
|
|
@@ -56,21 +56,21 @@ const maxIter = Number(getArg('--max-iter', String(config.maxIterations))) ||
|
|
|
56
56
|
const positional = getPositional();
|
|
57
57
|
const task = getArg('--task', positional.join(' ').trim() || null);
|
|
58
58
|
const chatIdArg = getArg('--chat', null);
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
59
|
+
// When resuming an existing chat, the system-prompt is by default NOT
|
|
60
|
+
// resent (it is already at the start of the chat). The --resend-prompt flag
|
|
61
|
+
// forces resending it — for example, if the prompt was updated.
|
|
62
62
|
const resendPrompt = hasFlag('--resend-prompt');
|
|
63
|
-
//
|
|
64
|
-
// --resume-last:
|
|
65
|
-
// --new-chat:
|
|
63
|
+
// By default a NEW chat is started on launch (context is not carried over).
|
|
64
|
+
// --resume-last: return to the last session for the working directory.
|
|
65
|
+
// --new-chat: kept for compatibility — this is the default behavior anyway.
|
|
66
66
|
const newChatFlag = hasFlag('--new-chat');
|
|
67
67
|
const resumeLastFlag = hasFlag('--resume-last');
|
|
68
68
|
// ---------- session persistence ----------
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// --chat <id>, /resume <n>).
|
|
72
|
-
//
|
|
73
|
-
//
|
|
69
|
+
// We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
|
|
70
|
+
// a restart and are available for explicit restore (--resume-last,
|
|
71
|
+
// --chat <id>, /resume <n>). They are NOT brought up automatically on launch.
|
|
72
|
+
// Previously there was a single last-chat.json file that got lost when the
|
|
73
|
+
// project changed and gave no list of sessions to restore.
|
|
74
74
|
function saveLastChat(id, workdir = '', title = '') {
|
|
75
75
|
if (!id)
|
|
76
76
|
return;
|
|
@@ -81,13 +81,13 @@ function loadLastChat(workdir = '') {
|
|
|
81
81
|
return s ? s.id : null;
|
|
82
82
|
}
|
|
83
83
|
// ---------- hot reload ----------
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
84
|
+
// We keep references to the logic modules in the mod object. The /reload
|
|
85
|
+
// command re-reads them via a dynamic import with a timestamp query — Node
|
|
86
|
+
// caches ESM by URL, so such an import returns a fresh version of the module.
|
|
87
|
+
// The browser, chat and current state are NOT restarted: only the logic is updated.
|
|
88
|
+
// In dev mode tsx loads the .ts sources from src/, and in the built dist — .js.
|
|
89
|
+
// A dynamic import with the ?t= query bypasses tsx's extension rewriting,
|
|
90
|
+
// so we pick the extension ourselves — from the actual file of the current module.
|
|
91
91
|
const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
|
|
92
92
|
const RELOADABLE = [
|
|
93
93
|
'tools',
|
|
@@ -151,13 +151,13 @@ async function reloadModules() {
|
|
|
151
151
|
mod.createSpinner = pick('spinner', 'createSpinner');
|
|
152
152
|
return { count: loaded.size, errors };
|
|
153
153
|
}
|
|
154
|
-
//
|
|
154
|
+
// Initial load so that mod.buildSystemPrompt and the rest are populated.
|
|
155
155
|
await reloadModules();
|
|
156
|
-
// Dev
|
|
157
|
-
//
|
|
158
|
-
//
|
|
156
|
+
// Dev mode: auto-reload of the logic modules before each task.
|
|
157
|
+
// Enabled by the --dev flag (`npm run dev` sets it) or config.hotReload === true.
|
|
158
|
+
// In normal mode (npm start, global zames) auto-reload is off.
|
|
159
159
|
const devMode = hasFlag('--dev') || config.hotReload === true;
|
|
160
|
-
//
|
|
160
|
+
// Safe auto-reload: on a load error we keep the previous working modules.
|
|
161
161
|
async function autoReload() {
|
|
162
162
|
if (!devMode)
|
|
163
163
|
return;
|
|
@@ -239,8 +239,8 @@ ${theme.bold(t('help.files'))}
|
|
|
239
239
|
function dirLabel(p) {
|
|
240
240
|
return path.basename(p) || p;
|
|
241
241
|
}
|
|
242
|
-
//
|
|
243
|
-
//
|
|
242
|
+
// List of slash commands for completion when you type «/» (Tab — complete).
|
|
243
|
+
// The description is localized by the help.cmd.* key at display time — see
|
|
244
244
|
// buildSlashCommands().
|
|
245
245
|
const SLASH_COMMANDS = [
|
|
246
246
|
{ name: '/help', key: 'help.cmd.help' },
|
|
@@ -269,15 +269,15 @@ const SLASH_COMMANDS = [
|
|
|
269
269
|
{ name: '/exit', key: 'help.cmd.exit' },
|
|
270
270
|
{ name: '/quit', key: 'help.cmd.exit' },
|
|
271
271
|
];
|
|
272
|
-
//
|
|
272
|
+
// Slash-command descriptions in the current language (for LineEditor hints).
|
|
273
273
|
function buildSlashCommands() {
|
|
274
274
|
return SLASH_COMMANDS.map((c) => ({
|
|
275
275
|
name: c.name,
|
|
276
276
|
description: t(c.key),
|
|
277
277
|
}));
|
|
278
278
|
}
|
|
279
|
-
//
|
|
280
|
-
//
|
|
279
|
+
// We put the agent's temporary files (one-off scripts, etc.) in
|
|
280
|
+
// <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
|
|
281
281
|
const TMP_DIR = path.join(__dirname, '..', 'tmp');
|
|
282
282
|
async function cleanTmpDir() {
|
|
283
283
|
try {
|
|
@@ -289,23 +289,22 @@ async function cleanTmpDir() {
|
|
|
289
289
|
console.error('tmp: не удалось очистить:', e.message);
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
-
//
|
|
293
|
-
// Ctrl+Shift+V,
|
|
292
|
+
// Terminal line input with correct paste handling (Shift+Insert,
|
|
293
|
+
// Ctrl+Shift+V, right mouse button, etc.).
|
|
294
294
|
//
|
|
295
|
-
//
|
|
296
|
-
// 1) readline
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// не считается отправкой.
|
|
295
|
+
// Why a custom reader instead of readline:
|
|
296
|
+
// 1) readline submits the line on the FIRST newline. When pasting
|
|
297
|
+
// multiline text this caused immediate submission and only the first
|
|
298
|
+
// line went to the chat. Here newlines inside a paste are replaced with
|
|
299
|
+
// spaces, and submission happens only on a single Enter press.
|
|
300
|
+
// 2) We enable bracketed paste mode (\x1b[?2004h): the terminal wraps the
|
|
301
|
+
// pasted text in the markers \x1b[200~ … \x1b[201~, so we know for sure
|
|
302
|
+
// that this is a paste, not keyboard typing, and Enter inside it is not
|
|
303
|
+
// treated as submission.
|
|
305
304
|
async function promptOnce(question) {
|
|
306
305
|
const stdin = process.stdin;
|
|
307
306
|
const stdout = process.stdout;
|
|
308
|
-
//
|
|
307
|
+
// Non-TTY (pipe, redirect): read everything up to EOF as a single string.
|
|
309
308
|
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
310
309
|
const chunks = [];
|
|
311
310
|
return await new Promise((resolve) => {
|
|
@@ -323,7 +322,7 @@ async function promptOnce(question) {
|
|
|
323
322
|
const wasRaw = stdin.isRaw;
|
|
324
323
|
stdin.setRawMode(true);
|
|
325
324
|
stdin.resume();
|
|
326
|
-
//
|
|
325
|
+
// We enable/disable bracketed paste in pairs.
|
|
327
326
|
stdout.write('\x1b[?2004h');
|
|
328
327
|
stdout.write(question);
|
|
329
328
|
let line = '';
|
|
@@ -332,11 +331,11 @@ async function promptOnce(question) {
|
|
|
332
331
|
const PASTE_START = '\x1b[200~';
|
|
333
332
|
const PASTE_END = '\x1b[201~';
|
|
334
333
|
const redraw = () => {
|
|
335
|
-
//
|
|
334
|
+
// Return to the start of the line, erase and print again.
|
|
336
335
|
stdout.write(String.fromCharCode(13));
|
|
337
336
|
stdout.write('\x1b[K');
|
|
338
337
|
stdout.write(question + line);
|
|
339
|
-
//
|
|
338
|
+
// Put the cursor in the right position.
|
|
340
339
|
const back = line.length - cursor;
|
|
341
340
|
if (back > 0)
|
|
342
341
|
stdout.write('\x1b[' + back + 'D');
|
|
@@ -352,9 +351,9 @@ async function promptOnce(question) {
|
|
|
352
351
|
resolve(value);
|
|
353
352
|
};
|
|
354
353
|
const insertText = (text) => {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
354
|
+
// Normalize newlines: they come from a multiline paste but mean
|
|
355
|
+
// "submit". Inside the message we replace them with a space so the
|
|
356
|
+
// whole paste goes as ONE message.
|
|
358
357
|
const clean = text
|
|
359
358
|
.replace(/\r\n/g, ' ')
|
|
360
359
|
.replace(/\r/g, ' ')
|
|
@@ -364,11 +363,11 @@ async function promptOnce(question) {
|
|
|
364
363
|
};
|
|
365
364
|
const onData = (buf) => {
|
|
366
365
|
let s = buf.toString('utf-8');
|
|
367
|
-
// Fallback
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
366
|
+
// Fallback for terminals without bracketed paste: if the whole chunk is
|
|
367
|
+
// a "bare" newline (one byte), then Enter was pressed → submit.
|
|
368
|
+
// If newlines came TOGETHER with other text in one chunk — that's a
|
|
369
|
+
// paste; such newlines don't submit the message but are replaced with
|
|
370
|
+
// spaces (see insertText).
|
|
372
371
|
if (!inPaste && (s === '\r' || s === '\n')) {
|
|
373
372
|
return finish(line, true);
|
|
374
373
|
}
|
|
@@ -389,14 +388,14 @@ async function promptOnce(question) {
|
|
|
389
388
|
}
|
|
390
389
|
const start = s.indexOf(PASTE_START);
|
|
391
390
|
if (start !== -1) {
|
|
392
|
-
//
|
|
391
|
+
// Everything before the marker is processed as regular input.
|
|
393
392
|
const before = s.slice(0, start);
|
|
394
393
|
s = s.slice(start + PASTE_START.length);
|
|
395
394
|
inPaste = true;
|
|
396
395
|
if (before) {
|
|
397
396
|
for (const ch of before) {
|
|
398
397
|
if (ch === '\r' || ch === '\n') {
|
|
399
|
-
/*
|
|
398
|
+
/* inside a paste — skip */
|
|
400
399
|
}
|
|
401
400
|
else
|
|
402
401
|
insertText(ch);
|
|
@@ -409,22 +408,22 @@ async function promptOnce(question) {
|
|
|
409
408
|
const code = s.charCodeAt(0);
|
|
410
409
|
s = s.slice(1);
|
|
411
410
|
if (ch === '\r' || ch === '\n') {
|
|
412
|
-
//
|
|
413
|
-
// bracketed paste):
|
|
411
|
+
// A newline inside a chunk with other text (paste without
|
|
412
|
+
// bracketed paste): don't submit, insert a space instead.
|
|
414
413
|
insertText(' ');
|
|
415
414
|
redraw();
|
|
416
415
|
continue;
|
|
417
416
|
}
|
|
418
417
|
if (code === 3) {
|
|
419
|
-
// Ctrl+C —
|
|
418
|
+
// Ctrl+C — abort input.
|
|
420
419
|
return finish('', true);
|
|
421
420
|
}
|
|
422
421
|
if (code === 4) {
|
|
423
|
-
// Ctrl+D —
|
|
422
|
+
// Ctrl+D — like submitting an empty line.
|
|
424
423
|
return finish(line, true);
|
|
425
424
|
}
|
|
426
425
|
if (code === 21) {
|
|
427
|
-
// Ctrl+U —
|
|
426
|
+
// Ctrl+U — erase the line.
|
|
428
427
|
line = '';
|
|
429
428
|
cursor = 0;
|
|
430
429
|
redraw();
|
|
@@ -440,7 +439,7 @@ async function promptOnce(question) {
|
|
|
440
439
|
continue;
|
|
441
440
|
}
|
|
442
441
|
if (ch === '\x1b') {
|
|
443
|
-
// Escape
|
|
442
|
+
// Escape sequences (arrows, Home/End, Delete…).
|
|
444
443
|
const rest = s;
|
|
445
444
|
if (rest.startsWith('[D')) {
|
|
446
445
|
if (cursor > 0)
|
|
@@ -477,14 +476,14 @@ async function promptOnce(question) {
|
|
|
477
476
|
s = s.slice(3);
|
|
478
477
|
continue;
|
|
479
478
|
}
|
|
480
|
-
//
|
|
479
|
+
// Other ESC sequences are skipped up to a letter/tilde.
|
|
481
480
|
const m = s.match(/^\[[0-9;]*[A-Za-z~]/);
|
|
482
481
|
if (m)
|
|
483
482
|
s = s.slice(m[0].length);
|
|
484
483
|
continue;
|
|
485
484
|
}
|
|
486
485
|
if (code < 32)
|
|
487
|
-
continue; //
|
|
486
|
+
continue; // other control characters are ignored
|
|
488
487
|
insertText(ch);
|
|
489
488
|
redraw();
|
|
490
489
|
}
|
|
@@ -492,24 +491,24 @@ async function promptOnce(question) {
|
|
|
492
491
|
stdin.on('data', onData);
|
|
493
492
|
});
|
|
494
493
|
}
|
|
495
|
-
//
|
|
494
|
+
// Keyboard monitoring while the agent works.
|
|
496
495
|
//
|
|
497
|
-
//
|
|
498
|
-
// - Esc (
|
|
499
|
-
// -
|
|
500
|
-
//
|
|
501
|
-
//
|
|
496
|
+
// The terminal stays live while the agent thinks:
|
|
497
|
+
// - Esc (or Ctrl+C) — abort the current generation (a Stop click in the browser);
|
|
498
|
+
// - typing + Enter — queue a message; it goes to the agent right after
|
|
499
|
+
// the current task finishes (like "send during generation" in the
|
|
500
|
+
// DeepSeek web version).
|
|
502
501
|
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
502
|
+
// Input is buffered without a line editor: the typed text is shown in the
|
|
503
|
+
// spinner line via onChange -> ui.setPending(). Enter sends the buffer to
|
|
504
|
+
// onQueue, an empty Enter is ignored. Backspace, Ctrl+U, Esc sequences
|
|
505
|
+
// (arrows/Home/End/Delete are ignored) and bracketed paste are supported.
|
|
507
506
|
function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
508
507
|
const stdin = process.stdin;
|
|
509
508
|
if (!stdin.isTTY || !stdin.setRawMode)
|
|
510
509
|
return () => { };
|
|
511
|
-
//
|
|
512
|
-
//
|
|
510
|
+
// Control characters are assembled from codes: this file must not contain
|
|
511
|
+
// "raw" ESC/CR/LF in string literals (see AGENTS.md).
|
|
513
512
|
const ESC = String.fromCharCode(27);
|
|
514
513
|
const CSI = String.fromCharCode(91);
|
|
515
514
|
const CR = String.fromCharCode(13);
|
|
@@ -527,8 +526,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
527
526
|
if (onChange)
|
|
528
527
|
onChange(buf);
|
|
529
528
|
};
|
|
530
|
-
//
|
|
531
|
-
//
|
|
529
|
+
// Pasted text: newlines inside a multiline paste are treated as spaces —
|
|
530
|
+
// the message goes as a single line.
|
|
532
531
|
const insert = (text) => {
|
|
533
532
|
buf += text
|
|
534
533
|
.split(CR + LF)
|
|
@@ -540,8 +539,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
540
539
|
};
|
|
541
540
|
function onData(data) {
|
|
542
541
|
let s = data.toString('utf-8');
|
|
543
|
-
//
|
|
544
|
-
//
|
|
542
|
+
// A single Esc — abort generation. Arrows come as a whole chunk and
|
|
543
|
+
// don't reach here.
|
|
545
544
|
if (!inPaste && s === ESC) {
|
|
546
545
|
if (onEscape)
|
|
547
546
|
onEscape();
|
|
@@ -585,13 +584,13 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
585
584
|
continue;
|
|
586
585
|
}
|
|
587
586
|
if (code === 3) {
|
|
588
|
-
// Ctrl+C —
|
|
587
|
+
// Ctrl+C — like Esc: abort generation.
|
|
589
588
|
if (onEscape)
|
|
590
589
|
onEscape();
|
|
591
590
|
continue;
|
|
592
591
|
}
|
|
593
592
|
if (code === 21) {
|
|
594
|
-
// Ctrl+U —
|
|
593
|
+
// Ctrl+U — clear what was typed.
|
|
595
594
|
buf = '';
|
|
596
595
|
emitChange();
|
|
597
596
|
continue;
|
|
@@ -605,14 +604,14 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
605
604
|
continue;
|
|
606
605
|
}
|
|
607
606
|
if (ch === ESC) {
|
|
608
|
-
// Escape
|
|
607
|
+
// Escape sequence (arrows, Home/End, Delete) — skip.
|
|
609
608
|
const m = s.match(CSI_RE);
|
|
610
609
|
if (m)
|
|
611
610
|
s = s.slice(m[0].length);
|
|
612
611
|
continue;
|
|
613
612
|
}
|
|
614
613
|
if (code < 32)
|
|
615
|
-
continue; //
|
|
614
|
+
continue; // other control characters — ignore
|
|
616
615
|
buf += ch;
|
|
617
616
|
emitChange();
|
|
618
617
|
}
|
|
@@ -627,8 +626,8 @@ function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
627
626
|
};
|
|
628
627
|
}
|
|
629
628
|
// ---------- workdir resolution ----------
|
|
630
|
-
//
|
|
631
|
-
//
|
|
629
|
+
// The agent works in the directory it was launched from (process.cwd()).
|
|
630
|
+
// This is the sandbox root: tools cannot go above it.
|
|
632
631
|
async function resolveWorkdir() {
|
|
633
632
|
const explicitDir = getArg('--dir', null);
|
|
634
633
|
const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
|
|
@@ -641,10 +640,10 @@ async function resolveWorkdir() {
|
|
|
641
640
|
// ---------- task runner ----------
|
|
642
641
|
async function runTask(browser, tools, taskText, workdir, opts) {
|
|
643
642
|
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
|
|
644
|
-
//
|
|
645
|
-
// Ctrl+C)
|
|
646
|
-
// (
|
|
647
|
-
//
|
|
643
|
+
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
644
|
+
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
645
|
+
// mode (pipes) — a regular spinner + watchInput.
|
|
646
|
+
// A new task from the prompt — we reset the "stop" from the previous abort.
|
|
648
647
|
browser._stopped = false;
|
|
649
648
|
browser._abort = false;
|
|
650
649
|
const ui = editor || mod.createSpinner(currentLocale);
|
|
@@ -668,8 +667,8 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
668
667
|
});
|
|
669
668
|
try {
|
|
670
669
|
let next = { task: taskText, freshChat, sendSystemPrompt };
|
|
671
|
-
//
|
|
672
|
-
//
|
|
670
|
+
// Execute the task, then everything the user managed to type while it
|
|
671
|
+
// ran. The queue may be replenished right during draining.
|
|
673
672
|
while (true) {
|
|
674
673
|
ui.thinking();
|
|
675
674
|
await mod.runAgentLoop({
|
|
@@ -685,12 +684,13 @@ async function runTask(browser, tools, taskText, workdir, opts) {
|
|
|
685
684
|
onToolCall: (name, toolArgs) => ui.toolCall(name, toolArgs),
|
|
686
685
|
onToolResult: (result) => ui.toolResult(result),
|
|
687
686
|
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
687
|
+
onWarning: (msg) => ui.warning(msg),
|
|
688
688
|
onChatReady,
|
|
689
689
|
debugLog: debug,
|
|
690
690
|
locale: currentLocale,
|
|
691
691
|
});
|
|
692
|
-
//
|
|
693
|
-
//
|
|
692
|
+
// Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
|
|
693
|
+
// and clear it, so "stop" really stops everything.
|
|
694
694
|
if (browser._stopped) {
|
|
695
695
|
queue.length = 0;
|
|
696
696
|
break;
|
|
@@ -740,7 +740,7 @@ async function main() {
|
|
|
740
740
|
let sandboxRoot;
|
|
741
741
|
try {
|
|
742
742
|
currentWorkdir = await resolveWorkdir();
|
|
743
|
-
//
|
|
743
|
+
// Sandbox root: the agent cannot go above the launch directory.
|
|
744
744
|
sandboxRoot = currentWorkdir;
|
|
745
745
|
}
|
|
746
746
|
catch (e) {
|
|
@@ -779,13 +779,13 @@ async function main() {
|
|
|
779
779
|
transcript.close();
|
|
780
780
|
process.exit(1);
|
|
781
781
|
}
|
|
782
|
-
//
|
|
782
|
+
// One-shot mode
|
|
783
783
|
if (task) {
|
|
784
784
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
785
785
|
let freshChat = true;
|
|
786
786
|
let sendSystemPrompt = true;
|
|
787
|
-
//
|
|
788
|
-
//
|
|
787
|
+
// By default we start a new chat. To continue a previous session —
|
|
788
|
+
// explicitly: --chat <id> or --resume-last.
|
|
789
789
|
let resumeId = chatIdArg;
|
|
790
790
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
791
791
|
const last = loadLastSession(currentWorkdir);
|
|
@@ -828,29 +828,30 @@ async function main() {
|
|
|
828
828
|
let lastChats = [];
|
|
829
829
|
let currentChatId = null;
|
|
830
830
|
let running = true;
|
|
831
|
-
//
|
|
832
|
-
//
|
|
831
|
+
// Messages the user typed while the agent worked. runTask takes them one
|
|
832
|
+
// by one after the current task finishes.
|
|
833
833
|
const pendingQueue = [];
|
|
834
834
|
// ---------- review mode state ----------
|
|
835
|
-
// null —
|
|
836
|
-
// { snapDir, snapName, originalWorkdir } —
|
|
837
|
-
//
|
|
835
|
+
// null — normal mode.
|
|
836
|
+
// { snapDir, snapName, originalWorkdir } — we're inside a snapshot, the chat
|
|
837
|
+
// is already initialized with the review prompt, the user can just type "fix...".
|
|
838
838
|
let reviewMode = null;
|
|
839
839
|
process.on('SIGINT', async () => {
|
|
840
840
|
running = false;
|
|
841
841
|
await browser.close().catch(() => { });
|
|
842
|
-
//
|
|
843
|
-
//
|
|
842
|
+
// Close the lazy headless browser from web.js, otherwise it would stay
|
|
843
|
+
// as a separate process after the agent exits.
|
|
844
844
|
await mod.closeWeb().catch(() => { });
|
|
845
845
|
transcript.close();
|
|
846
846
|
console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
|
|
847
847
|
process.exit(0);
|
|
848
848
|
});
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
// --
|
|
853
|
-
// --
|
|
849
|
+
// By default we start a NEW chat on launch: the context of a previous
|
|
850
|
+
// session is not carried over automatically. To continue a previous
|
|
851
|
+
// session explicitly:
|
|
852
|
+
// --chat <id> open a specific chat
|
|
853
|
+
// --resume-last return to the last chat for this working directory
|
|
854
|
+
// --new-chat kept for compatibility (this is the default behavior)
|
|
854
855
|
let resumeId = chatIdArg;
|
|
855
856
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
856
857
|
const last = loadLastSession(currentWorkdir);
|
|
@@ -873,11 +874,11 @@ async function main() {
|
|
|
873
874
|
console.error(theme.error(t('msg.open_chat_error', { v: e.message })));
|
|
874
875
|
}
|
|
875
876
|
}
|
|
876
|
-
// ----------
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
877
|
+
// ---------- input: permanent line below + status above ----------
|
|
878
|
+
// In TTY we use LineEditor: it owns the input all the time, shows the
|
|
879
|
+
// status above the input line and prints the agent's answers ABOVE it, so
|
|
880
|
+
// the typed text is never overwritten by output. In non-TTY (pipe) —
|
|
881
|
+
// the old promptOnce.
|
|
881
882
|
let editor = null;
|
|
882
883
|
let waiter = null;
|
|
883
884
|
const takeInput = () => {
|
|
@@ -924,7 +925,7 @@ async function main() {
|
|
|
924
925
|
browser.stopGeneration().catch(() => { });
|
|
925
926
|
}
|
|
926
927
|
else {
|
|
927
|
-
//
|
|
928
|
+
// Not busy — exit. We wake takeInput() so the loop finishes.
|
|
928
929
|
running = false;
|
|
929
930
|
if (waiter) {
|
|
930
931
|
const r = waiter;
|
|
@@ -934,9 +935,9 @@ async function main() {
|
|
|
934
935
|
}
|
|
935
936
|
};
|
|
936
937
|
ed.start();
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
938
|
+
// All command output (console.log/error) must go ABOVE the input line,
|
|
939
|
+
// otherwise it overwrites the text being typed. While the editor is
|
|
940
|
+
// active, we wrap both streams in ed.printAbove.
|
|
940
941
|
const origLog = console.log.bind(console);
|
|
941
942
|
const origErr = console.error.bind(console);
|
|
942
943
|
const fmt = (a) => typeof a === 'string'
|
|
@@ -951,21 +952,21 @@ async function main() {
|
|
|
951
952
|
})();
|
|
952
953
|
console.log = (...a) => ed.printAbove(a.map(fmt).join(' '));
|
|
953
954
|
console.error = (...a) => ed.printAbove(a.map(fmt).join(' '));
|
|
954
|
-
//
|
|
955
|
+
// Save for debugging.
|
|
955
956
|
const edAny = ed;
|
|
956
957
|
edAny._origLog = origLog;
|
|
957
958
|
edAny._origErr = origErr;
|
|
958
959
|
}
|
|
959
960
|
// ---------- /config ----------
|
|
960
|
-
//
|
|
961
|
-
// CONFIG_SCHEMA (src/config.ts)
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
961
|
+
// View and edit settings without a restart. Values are validated against
|
|
962
|
+
// CONFIG_SCHEMA (src/config.ts) and written to the project .zamesrc.json.
|
|
963
|
+
// The language (ui.locale) is applied immediately: we change currentLocale,
|
|
964
|
+
// rebuild the editor hints and update config.ui.locale (agent-loop reads it
|
|
965
|
+
// on the next task).
|
|
965
966
|
//
|
|
966
|
-
//
|
|
967
|
-
// Enter —
|
|
968
|
-
// (list/get/set/reset/lang/path) —
|
|
967
|
+
// With no arguments in an interactive terminal a menu opens (arrows,
|
|
968
|
+
// Enter — edit, d — reset, q — quit). There are also text subcommands
|
|
969
|
+
// (list/get/set/reset/lang/path) — for scripts and non-TTY.
|
|
969
970
|
function setConfigRuntime(path, value) {
|
|
970
971
|
const segs = path.split('.');
|
|
971
972
|
let obj = config;
|
|
@@ -989,7 +990,7 @@ async function main() {
|
|
|
989
990
|
}
|
|
990
991
|
function configResetField(field) {
|
|
991
992
|
resetConfigValue('project', field.path);
|
|
992
|
-
//
|
|
993
|
+
// Reset the runtime value to the default.
|
|
993
994
|
const def = getByPath(DEFAULTS, field.path);
|
|
994
995
|
setConfigRuntime(field.path, def);
|
|
995
996
|
}
|
|
@@ -1028,9 +1029,9 @@ async function main() {
|
|
|
1028
1029
|
configShowList();
|
|
1029
1030
|
return;
|
|
1030
1031
|
}
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1032
|
+
// The menu draws directly to stdout and reads keys itself. So that its
|
|
1033
|
+
// output doesn't overlap the LineEditor's permanent input line, we "pause"
|
|
1034
|
+
// the editor for the duration of the menu and restore it afterwards.
|
|
1034
1035
|
if (editor)
|
|
1035
1036
|
editor.pause();
|
|
1036
1037
|
try {
|
|
@@ -1053,7 +1054,7 @@ async function main() {
|
|
|
1053
1054
|
async function handleConfigCommand(input) {
|
|
1054
1055
|
const parts = input.trim().split(/\s+/);
|
|
1055
1056
|
const sub = (parts[1] || '').toLowerCase();
|
|
1056
|
-
//
|
|
1057
|
+
// With no subcommand — the menu (or a text list in non-TTY).
|
|
1057
1058
|
if (!sub || sub === 'menu' || sub === 'ui') {
|
|
1058
1059
|
await configOpenMenu();
|
|
1059
1060
|
return;
|
|
@@ -1069,7 +1070,7 @@ async function main() {
|
|
|
1069
1070
|
})));
|
|
1070
1071
|
return;
|
|
1071
1072
|
}
|
|
1072
|
-
// /config lang <ru|en> —
|
|
1073
|
+
// /config lang <ru|en> — quick access to ui.locale
|
|
1073
1074
|
if (sub === 'lang' || sub === 'language' || sub === 'язык') {
|
|
1074
1075
|
const val = parts[2];
|
|
1075
1076
|
if (!val) {
|
|
@@ -1152,7 +1153,7 @@ async function main() {
|
|
|
1152
1153
|
console.log(theme.assistant(t('cfg.reset', { v: key })));
|
|
1153
1154
|
return;
|
|
1154
1155
|
}
|
|
1155
|
-
//
|
|
1156
|
+
// Unknown subcommand — show the list.
|
|
1156
1157
|
console.error(theme.error(t('cfg.unknown_key', { v: sub })));
|
|
1157
1158
|
configShowList();
|
|
1158
1159
|
}
|
|
@@ -1206,10 +1207,10 @@ async function main() {
|
|
|
1206
1207
|
}
|
|
1207
1208
|
continue;
|
|
1208
1209
|
}
|
|
1209
|
-
// ----------
|
|
1210
|
+
// ---------- Self-review ----------
|
|
1210
1211
|
if (lower === '/self-review' || lower.startsWith('/self-review ')) {
|
|
1211
1212
|
const focus = trimmed.slice('/self-review'.length).trim();
|
|
1212
|
-
//
|
|
1213
|
+
// Remember where to return
|
|
1213
1214
|
const originalWorkdir = reviewMode
|
|
1214
1215
|
? reviewMode.originalWorkdir
|
|
1215
1216
|
: currentWorkdir;
|
|
@@ -1220,10 +1221,10 @@ async function main() {
|
|
|
1220
1221
|
focus: focus || undefined,
|
|
1221
1222
|
transcript,
|
|
1222
1223
|
});
|
|
1223
|
-
//
|
|
1224
|
-
// -
|
|
1225
|
-
// -
|
|
1226
|
-
//
|
|
1224
|
+
// Switch to review mode:
|
|
1225
|
+
// - working directory = snapshot
|
|
1226
|
+
// - we do NOT reset the chat — inside selfReview a fresh chat was
|
|
1227
|
+
// already created and the review prompt sent, we continue in it
|
|
1227
1228
|
reviewMode = {
|
|
1228
1229
|
snapDir: result.snapDir,
|
|
1229
1230
|
snapName: path.basename(result.snapDir),
|
|
@@ -1261,7 +1262,7 @@ async function main() {
|
|
|
1261
1262
|
const originalWorkdir = reviewMode
|
|
1262
1263
|
? reviewMode.originalWorkdir
|
|
1263
1264
|
: currentWorkdir;
|
|
1264
|
-
//
|
|
1265
|
+
// Fresh chat + review prompt for this snapshot
|
|
1265
1266
|
try {
|
|
1266
1267
|
const { runAgentLoop: ral } = await import('./agent-loop.js');
|
|
1267
1268
|
const tools = mod.createTools(snapRoot, { undo: null });
|
|
@@ -1291,6 +1292,8 @@ async function main() {
|
|
|
1291
1292
|
onAssistantMessage: (m) => {
|
|
1292
1293
|
ui.assistant(m);
|
|
1293
1294
|
},
|
|
1295
|
+
onWarning: (m) => ui.warning(m),
|
|
1296
|
+
locale: currentLocale,
|
|
1294
1297
|
});
|
|
1295
1298
|
}
|
|
1296
1299
|
reviewMode = {
|
|
@@ -1318,7 +1321,7 @@ async function main() {
|
|
|
1318
1321
|
const back = reviewMode.originalWorkdir;
|
|
1319
1322
|
reviewMode = null;
|
|
1320
1323
|
currentWorkdir = back;
|
|
1321
|
-
//
|
|
1324
|
+
// Since the chat is busy with the review context, we'll create a new one for regular work
|
|
1322
1325
|
freshChatNext = true;
|
|
1323
1326
|
sendSystemPromptNext = true;
|
|
1324
1327
|
console.log(theme.system(t('self.done_hint', { v: back })));
|
|
@@ -1361,7 +1364,7 @@ async function main() {
|
|
|
1361
1364
|
}
|
|
1362
1365
|
continue;
|
|
1363
1366
|
}
|
|
1364
|
-
// ----------
|
|
1367
|
+
// ---------- Regular commands ----------
|
|
1365
1368
|
if (lower === '/chats') {
|
|
1366
1369
|
const spin = editor || mod.createSpinner(currentLocale);
|
|
1367
1370
|
spin.thinking();
|
|
@@ -1616,8 +1619,8 @@ async function main() {
|
|
|
1616
1619
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
1617
1620
|
continue;
|
|
1618
1621
|
}
|
|
1619
|
-
// ----
|
|
1620
|
-
// Dev
|
|
1622
|
+
// ---- Regular task (including in review mode) ----
|
|
1623
|
+
// Dev mode: pick up fresh logic modules before the task.
|
|
1621
1624
|
await autoReload();
|
|
1622
1625
|
transcript.log('user_task', { task: trimmed, workdir: currentWorkdir });
|
|
1623
1626
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
@@ -1631,9 +1634,9 @@ async function main() {
|
|
|
1631
1634
|
queue: pendingQueue,
|
|
1632
1635
|
ui: editor || null,
|
|
1633
1636
|
onChatReady: (chatId) => {
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1636
|
-
// ~/.zames/.sessions
|
|
1637
|
+
// Save the session right at the start of the dialog, without waiting
|
|
1638
|
+
// for the task to finish. Otherwise a long/aborted task would not
|
|
1639
|
+
// get the chat into ~/.zames/.sessions and it would be lost after a restart.
|
|
1637
1640
|
if (chatId) {
|
|
1638
1641
|
currentChatId = chatId;
|
|
1639
1642
|
saveLastChat(chatId, currentWorkdir);
|