zames_pro 2.54.0 → 2.58.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/CHANGELOG.md +209 -0
- package/README.md +113 -6
- package/dist/agent-loop.js +67 -0
- package/dist/attachments.js +32 -0
- package/dist/commands.js +57 -20
- package/dist/config.js +3 -42
- package/dist/fsutil.js +18 -0
- package/dist/gitTools.js +37 -0
- package/dist/i18n.js +140 -53
- package/dist/index.js +295 -34
- package/dist/input.js +5 -0
- package/dist/mcp.js +20 -0
- package/dist/self-review.js +44 -6
- package/dist/sessions.js +4 -8
- package/dist/shell.js +72 -0
- package/dist/spinner.js +42 -3
- package/dist/system-prompt.js +13 -0
- package/dist/tools.js +43 -75
- package/dist/transcript.js +14 -4
- package/dist/undo.js +8 -6
- package/dist/web.js +31 -0
- package/package.json +14 -4
- package/scripts/clean-dist.mjs +17 -0
- package/scripts/coverage-gate.mjs +83 -0
- package/dist/confirm.js +0 -105
package/dist/index.js
CHANGED
|
@@ -11,21 +11,26 @@ import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from '.
|
|
|
11
11
|
import { runAgentLoop } from './agent-loop.js';
|
|
12
12
|
import { createSpinner } from './spinner.js';
|
|
13
13
|
import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
|
|
14
|
-
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, contentHash, } from './attachments.js';
|
|
14
|
+
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
|
|
15
15
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
16
16
|
import { runConfigMenu } from './config-menu.js';
|
|
17
17
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
18
18
|
import { Transcript } from './transcript.js';
|
|
19
19
|
import { UndoStore } from './undo.js';
|
|
20
20
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
21
|
-
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor,
|
|
21
|
+
import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
22
22
|
import { performCompact } from './compact.js';
|
|
23
23
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
24
24
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
25
25
|
import { closeWeb } from './web.js';
|
|
26
|
+
import { assertCommandInsideRoot, runShell } from './shell.js';
|
|
26
27
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
27
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
const NL = String.fromCharCode(10);
|
|
30
|
+
// The last final assistant message, for the /copy command. Module-level
|
|
31
|
+
// because runTask() (which sees the answers) and the main loop (which handles
|
|
32
|
+
// /copy) are separate functions.
|
|
33
|
+
let lastAssistantMessage = '';
|
|
29
34
|
// Floor for a /loop interval. A loop faster than the send throttle (15s)
|
|
30
35
|
// cannot do anything useful and would just hammer the rate limit, so we refuse
|
|
31
36
|
// it. It does NOT touch the throttle itself.
|
|
@@ -53,7 +58,7 @@ function getPositional() {
|
|
|
53
58
|
const positional = [];
|
|
54
59
|
for (let i = 0; i < args.length; i++) {
|
|
55
60
|
const a = args[i];
|
|
56
|
-
if (['--dir', '--task', '--max-iter', '--
|
|
61
|
+
if (['--dir', '--task', '--max-iter', '--chat', '--output-format'].includes(a)) {
|
|
57
62
|
i++;
|
|
58
63
|
continue;
|
|
59
64
|
}
|
|
@@ -87,7 +92,6 @@ const headless = hasFlag('--headed')
|
|
|
87
92
|
? false
|
|
88
93
|
: hasFlag('--headless') || config.headless;
|
|
89
94
|
const debug = hasFlag('--debug') || config.debug;
|
|
90
|
-
const calibrate = hasFlag('--calibrate');
|
|
91
95
|
const maxIterArg = getArg('--max-iter', null);
|
|
92
96
|
const maxIter = maxIterArg !== null ? Number(maxIterArg) : config.maxIterations;
|
|
93
97
|
const positional = getPositional();
|
|
@@ -102,6 +106,23 @@ const resendPrompt = hasFlag('--resend-prompt');
|
|
|
102
106
|
// --new-chat: kept for compatibility — this is the default behavior anyway.
|
|
103
107
|
const newChatFlag = hasFlag('--new-chat');
|
|
104
108
|
const resumeLastFlag = hasFlag('--resume-last');
|
|
109
|
+
// Plan mode (read-only): start with it ON via --plan, toggle with /plan. When
|
|
110
|
+
// on, mutating tools are filtered out of the tool set entirely (see
|
|
111
|
+
// filterToolsForReadOnly). Declared here because tool creation happens before
|
|
112
|
+
// the main loop.
|
|
113
|
+
let planMode = hasFlag('--plan');
|
|
114
|
+
// Machine-readable output for scripts/CI. 'text' (default) is the normal
|
|
115
|
+
// human UI; 'json'/'jsonl' stream the transcript events as JSON lines on
|
|
116
|
+
// stdout, while all human text is pushed to stderr so the stream stays clean.
|
|
117
|
+
// Meant for one-shot mode (--task).
|
|
118
|
+
const outputFormat = (getArg('--output-format', 'text') || 'text').toLowerCase();
|
|
119
|
+
const jsonMode = outputFormat === 'json' || outputFormat === 'jsonl';
|
|
120
|
+
if (jsonMode) {
|
|
121
|
+
// Keep stdout for the event stream only.
|
|
122
|
+
console.log = ((...a) => {
|
|
123
|
+
console.error(...a);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
105
126
|
// ---------- session persistence ----------
|
|
106
127
|
// We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
|
|
107
128
|
// a restart and are available for explicit restore (--resume-last,
|
|
@@ -167,10 +188,10 @@ const RELOADABLE = [
|
|
|
167
188
|
'self-review',
|
|
168
189
|
'diff',
|
|
169
190
|
'undo',
|
|
170
|
-
'confirm',
|
|
171
191
|
'transcript',
|
|
172
192
|
'spinner',
|
|
173
193
|
'config',
|
|
194
|
+
'fsutil',
|
|
174
195
|
];
|
|
175
196
|
const mod = {
|
|
176
197
|
createTools,
|
|
@@ -289,8 +310,9 @@ ${theme.bold(t('help.options'))}
|
|
|
289
310
|
--max-iter <n> ${t('help.opt.max_iter', { n: config.maxIterations })}
|
|
290
311
|
--headless ${t('help.opt.headless')}
|
|
291
312
|
--headed ${t('help.opt.headed')}
|
|
313
|
+
--plan ${t('help.opt.plan')}
|
|
314
|
+
--output-format <f> ${t('help.opt.output_format')}
|
|
292
315
|
--debug ${t('help.opt.debug')}
|
|
293
|
-
--calibrate ${t('help.opt.calibrate')}
|
|
294
316
|
--dev ${t('help.opt.dev')}
|
|
295
317
|
--no-color ${t('help.opt.no_color')}
|
|
296
318
|
--version, -v ${t('help.opt.version')}
|
|
@@ -309,40 +331,63 @@ ${theme.bold(t('help.while_working'))}
|
|
|
309
331
|
${t('help.key.backslash')}
|
|
310
332
|
${t('help.key.attach')}
|
|
311
333
|
${t('help.key.esc')}
|
|
334
|
+
${t('help.key.bang')}
|
|
312
335
|
|
|
313
336
|
${theme.bold(t('help.commands'))}
|
|
337
|
+
|
|
338
|
+
${theme.bold(t('help.sec.session'))}
|
|
314
339
|
${t('help.cmd.new')}
|
|
315
340
|
${t('help.cmd.sessions')}
|
|
316
|
-
${t('help.cmd.resume_id')}
|
|
317
341
|
${t('help.cmd.chats')}
|
|
318
342
|
${t('help.cmd.resume')}
|
|
343
|
+
${t('help.cmd.resume_id')}
|
|
319
344
|
${t('help.cmd.last')}
|
|
345
|
+
${t('help.cmd.retry')}
|
|
346
|
+
${t('help.cmd.rename')}
|
|
347
|
+
|
|
348
|
+
${theme.bold(t('help.sec.workspace'))}
|
|
320
349
|
${t('help.cmd.chat')}
|
|
321
350
|
${t('help.cmd.cd')}
|
|
322
351
|
${t('help.cmd.pwd')}
|
|
323
352
|
${t('help.cmd.status')}
|
|
324
353
|
${t('help.cmd.reload')}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
${t('help.cmd.transcript')}
|
|
354
|
+
|
|
355
|
+
${theme.bold(t('help.sec.git'))}
|
|
328
356
|
${t('help.cmd.diff')}
|
|
329
|
-
${t('help.cmd.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
${t('help.cmd.permissions')}
|
|
333
|
-
${t('help.cmd.add_dir')}
|
|
334
|
-
${t('help.cmd.review')}
|
|
357
|
+
${t('help.cmd.diffstat')}
|
|
358
|
+
|
|
359
|
+
${theme.bold(t('help.sec.agent'))}
|
|
335
360
|
${t('help.cmd.goal')}
|
|
336
361
|
${t('help.cmd.loop')}
|
|
337
362
|
${t('help.cmd.cron')}
|
|
338
363
|
${t('help.cmd.jobs')}
|
|
339
364
|
${t('help.cmd.thinking')}
|
|
340
365
|
${t('help.cmd.web')}
|
|
366
|
+
${t('help.cmd.plan')}
|
|
341
367
|
${t('help.cmd.compact')}
|
|
342
368
|
${t('help.cmd.queue')}
|
|
343
369
|
${t('help.cmd.tasks')}
|
|
370
|
+
|
|
371
|
+
${theme.bold(t('help.sec.context'))}
|
|
372
|
+
${t('help.cmd.context')}
|
|
373
|
+
${t('help.cmd.skills')}
|
|
374
|
+
${t('help.cmd.memory')}
|
|
375
|
+
${t('help.cmd.remember')}
|
|
376
|
+
${t('help.cmd.init')}
|
|
377
|
+
${t('help.cmd.mcp')}
|
|
344
378
|
${t('help.cmd.config')}
|
|
345
379
|
${t('help.cmd.lang')}
|
|
380
|
+
|
|
381
|
+
${theme.bold(t('help.sec.files'))}
|
|
382
|
+
${t('help.cmd.transcript')}
|
|
383
|
+
${t('help.cmd.undo')}
|
|
384
|
+
${t('help.cmd.undo_list')}
|
|
385
|
+
${t('help.cmd.export')}
|
|
386
|
+
${t('help.cmd.cost')}
|
|
387
|
+
${t('help.cmd.copy')}
|
|
388
|
+
${t('help.cmd.doctor')}
|
|
389
|
+
${t('help.cmd.add_dir')}
|
|
390
|
+
${t('help.cmd.review')}
|
|
346
391
|
${t('help.cmd.debug_dom')}
|
|
347
392
|
${t('help.cmd.help')}
|
|
348
393
|
${t('help.cmd.exit')}
|
|
@@ -384,6 +429,10 @@ const SLASH_COMMANDS = [
|
|
|
384
429
|
{ name: '/resume', key: 'help.cmd.resume' },
|
|
385
430
|
{ name: '/resume-id', key: 'help.cmd.resume_id' },
|
|
386
431
|
{ name: '/last', key: 'help.cmd.last' },
|
|
432
|
+
{ name: '/retry', key: 'help.cmd.retry' },
|
|
433
|
+
{ name: '/rename', key: 'help.cmd.rename' },
|
|
434
|
+
{ name: '/context', key: 'help.cmd.context' },
|
|
435
|
+
{ name: '/copy', key: 'help.cmd.copy' },
|
|
387
436
|
{ name: '/chat', key: 'help.cmd.chat' },
|
|
388
437
|
{ name: '/cd', key: 'help.cmd.cd' },
|
|
389
438
|
{ name: '/pwd', key: 'help.cmd.pwd' },
|
|
@@ -393,10 +442,10 @@ const SLASH_COMMANDS = [
|
|
|
393
442
|
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
394
443
|
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
395
444
|
{ name: '/diff', key: 'help.cmd.diff' },
|
|
445
|
+
{ name: '/diffstat', key: 'help.cmd.diffstat' },
|
|
396
446
|
{ name: '/cost', key: 'help.cmd.cost' },
|
|
397
447
|
{ name: '/export', key: 'help.cmd.export' },
|
|
398
448
|
{ name: '/doctor', key: 'help.cmd.doctor' },
|
|
399
|
-
{ name: '/permissions', key: 'help.cmd.permissions' },
|
|
400
449
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
401
450
|
{ name: '/review', key: 'help.cmd.review' },
|
|
402
451
|
{ name: '/goal', key: 'help.cmd.goal' },
|
|
@@ -405,12 +454,14 @@ const SLASH_COMMANDS = [
|
|
|
405
454
|
{ name: '/jobs', key: 'help.cmd.jobs' },
|
|
406
455
|
{ name: '/thinking', key: 'help.cmd.thinking' },
|
|
407
456
|
{ name: '/web', key: 'help.cmd.web' },
|
|
457
|
+
{ name: '/plan', key: 'help.cmd.plan' },
|
|
408
458
|
{ name: '/compact', key: 'help.cmd.compact' },
|
|
409
459
|
{ name: '/queue', key: 'help.cmd.queue' },
|
|
410
460
|
{ name: '/tasks', key: 'help.cmd.tasks' },
|
|
411
461
|
{ name: '/config', key: 'help.cmd.config' },
|
|
412
462
|
{ name: '/skills', key: 'help.cmd.skills' },
|
|
413
463
|
{ name: '/memory', key: 'help.cmd.memory' },
|
|
464
|
+
{ name: '/remember', key: 'help.cmd.remember' },
|
|
414
465
|
{ name: '/init', key: 'help.cmd.init' },
|
|
415
466
|
{ name: '/mcp', key: 'help.cmd.mcp' },
|
|
416
467
|
{ name: '/debug-dom', key: 'help.cmd.debug_dom' },
|
|
@@ -1003,7 +1054,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
1003
1054
|
}
|
|
1004
1055
|
// ---------- task runner ----------
|
|
1005
1056
|
async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
|
|
1006
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, } = opts;
|
|
1057
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, } = opts;
|
|
1007
1058
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
1008
1059
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
1009
1060
|
// mode (pipes) — a regular spinner + watchInput.
|
|
@@ -1011,6 +1062,12 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1011
1062
|
browser._stopped = false;
|
|
1012
1063
|
browser._abort = false;
|
|
1013
1064
|
const ui = editor || mod.createSpinner(currentLocale);
|
|
1065
|
+
// Non-TTY parity: the ora spinner shows the same "tasks: 2/5" badge the
|
|
1066
|
+
// LineEditor shows (the editor gets it via onTasksQuery in main()).
|
|
1067
|
+
if (!editor && todosQuery && 'onTasksQuery' in ui) {
|
|
1068
|
+
;
|
|
1069
|
+
ui.onTasksQuery = todosQuery;
|
|
1070
|
+
}
|
|
1014
1071
|
// Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
|
|
1015
1072
|
// Esc / Ctrl+C abort the current generation only while busy; without this a
|
|
1016
1073
|
// long-running tool (Bash, npm, MCP) could not be interrupted — Esc did
|
|
@@ -1084,7 +1141,14 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1084
1141
|
ui.toolCall(name, toolArgs);
|
|
1085
1142
|
},
|
|
1086
1143
|
onToolResult: (result) => ui.toolResult(result),
|
|
1087
|
-
onAssistantMessage: (msg) =>
|
|
1144
|
+
onAssistantMessage: (msg) => {
|
|
1145
|
+
// Remember the last answer so /copy can put it on the clipboard
|
|
1146
|
+
// without re-reading the chat. Wrapped: a UI failure is swallowed
|
|
1147
|
+
// by agent-loop anyway.
|
|
1148
|
+
lastAssistantMessage = msg;
|
|
1149
|
+
opts.onAssistantFinal?.(msg);
|
|
1150
|
+
ui.assistant(msg);
|
|
1151
|
+
},
|
|
1088
1152
|
onWarning: (msg) => ui.warning(msg),
|
|
1089
1153
|
onChatReady,
|
|
1090
1154
|
debugLog: debug,
|
|
@@ -1216,9 +1280,6 @@ async function main() {
|
|
|
1216
1280
|
printHelp();
|
|
1217
1281
|
return;
|
|
1218
1282
|
}
|
|
1219
|
-
if (calibrate) {
|
|
1220
|
-
console.log(theme.warn(t('calibrate')));
|
|
1221
|
-
}
|
|
1222
1283
|
await cleanTmpDir();
|
|
1223
1284
|
let currentWorkdir;
|
|
1224
1285
|
let sandboxRoot;
|
|
@@ -1237,6 +1298,9 @@ async function main() {
|
|
|
1237
1298
|
dir: config.transcript.dir,
|
|
1238
1299
|
enabled: config.transcript.enabled,
|
|
1239
1300
|
sessionName: dirLabel(currentWorkdir),
|
|
1301
|
+
// In json/jsonl mode mirror every event to stdout as a JSON line, even
|
|
1302
|
+
// when the transcript FILE is disabled.
|
|
1303
|
+
onLine: jsonMode ? (line) => process.stdout.write(line) : null,
|
|
1240
1304
|
});
|
|
1241
1305
|
if (transcript.file) {
|
|
1242
1306
|
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
@@ -1258,6 +1322,8 @@ async function main() {
|
|
|
1258
1322
|
if (srv.error)
|
|
1259
1323
|
console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
|
|
1260
1324
|
}
|
|
1325
|
+
for (const w of st.warnings)
|
|
1326
|
+
console.error(theme.warn(w));
|
|
1261
1327
|
}
|
|
1262
1328
|
catch (e) {
|
|
1263
1329
|
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
@@ -1298,7 +1364,10 @@ async function main() {
|
|
|
1298
1364
|
}
|
|
1299
1365
|
// One-shot mode
|
|
1300
1366
|
if (task) {
|
|
1301
|
-
const tools = mod.createTools(currentWorkdir, {
|
|
1367
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
1368
|
+
undo,
|
|
1369
|
+
readOnly: planMode,
|
|
1370
|
+
});
|
|
1302
1371
|
if (mcpPool)
|
|
1303
1372
|
tools.push(...mcpPool.tools);
|
|
1304
1373
|
let freshChat = true;
|
|
@@ -1361,6 +1430,8 @@ async function main() {
|
|
|
1361
1430
|
const promptOnResume = () => resendPrompt || config.browser.resendPromptOnResume === true;
|
|
1362
1431
|
let lastChats = [];
|
|
1363
1432
|
let currentChatId = null;
|
|
1433
|
+
// The last task the operator sent (used by /retry). Plain text only.
|
|
1434
|
+
let lastTaskText = '';
|
|
1364
1435
|
let running = true;
|
|
1365
1436
|
// The agent's TodoWrite checklist, shared with the tool factory and
|
|
1366
1437
|
// persisted in the session JSON. A store (not module state) so /reload does
|
|
@@ -2153,6 +2224,36 @@ async function main() {
|
|
|
2153
2224
|
const lower = trimmed.toLowerCase();
|
|
2154
2225
|
if (['/exit', '/quit', 'exit', 'quit'].includes(lower))
|
|
2155
2226
|
break;
|
|
2227
|
+
// `!command` — run a shell command directly, without asking the model
|
|
2228
|
+
// (like Claude Code's bash mode). The same sandbox guard as the Bash tool
|
|
2229
|
+
// applies, so a direct command cannot leave the project either.
|
|
2230
|
+
if (trimmed.startsWith('!')) {
|
|
2231
|
+
const cmd = trimmed.slice(1).trim();
|
|
2232
|
+
if (!cmd) {
|
|
2233
|
+
console.error(theme.warn(t('shell.empty')));
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
try {
|
|
2237
|
+
assertCommandInsideRoot(currentWorkdir, cmd);
|
|
2238
|
+
}
|
|
2239
|
+
catch (e) {
|
|
2240
|
+
console.error(theme.error(e.message));
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
transcript.log('shell_command', { command: cmd });
|
|
2244
|
+
if (editor)
|
|
2245
|
+
editor.lock(t('shell.running'));
|
|
2246
|
+
try {
|
|
2247
|
+
const out = await runShell(currentWorkdir, cmd);
|
|
2248
|
+
console.log(theme.system(out));
|
|
2249
|
+
transcript.log('shell_result', { command: cmd, result: out });
|
|
2250
|
+
}
|
|
2251
|
+
finally {
|
|
2252
|
+
if (editor)
|
|
2253
|
+
editor.unlock();
|
|
2254
|
+
}
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2156
2257
|
if (lower === '/help' ||
|
|
2157
2258
|
lower === 'help' ||
|
|
2158
2259
|
lower.startsWith('/help ') ||
|
|
@@ -2711,6 +2812,28 @@ async function main() {
|
|
|
2711
2812
|
continue;
|
|
2712
2813
|
}
|
|
2713
2814
|
}
|
|
2815
|
+
if (lower === '/plan' || lower.startsWith('/plan ')) {
|
|
2816
|
+
const arg = trimmed.slice('/plan'.length).trim().toLowerCase();
|
|
2817
|
+
if (arg === '' || arg === 'toggle')
|
|
2818
|
+
planMode = !planMode;
|
|
2819
|
+
else if (arg === 'on' || arg === '1' || arg === 'true' || arg === 'вкл')
|
|
2820
|
+
planMode = true;
|
|
2821
|
+
else if (arg === 'off' ||
|
|
2822
|
+
arg === '0' ||
|
|
2823
|
+
arg === 'false' ||
|
|
2824
|
+
arg === 'выкл')
|
|
2825
|
+
planMode = false;
|
|
2826
|
+
else {
|
|
2827
|
+
console.error(theme.warn(t('msg.unknown_cmd', { v: trimmed })));
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
const line = t(planMode ? 'plan.on' : 'plan.off');
|
|
2831
|
+
if (editor)
|
|
2832
|
+
editor.printAbove(theme.system(line));
|
|
2833
|
+
else
|
|
2834
|
+
console.log(theme.system(line));
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2714
2837
|
if (lower === '/mcp') {
|
|
2715
2838
|
if (!mcpPool) {
|
|
2716
2839
|
console.log(theme.dim(t('mcp.none')));
|
|
@@ -2733,6 +2856,8 @@ async function main() {
|
|
|
2733
2856
|
theme.dim('(' + String(srv.tools.length) + ')'));
|
|
2734
2857
|
}
|
|
2735
2858
|
}
|
|
2859
|
+
for (const w of st.warnings)
|
|
2860
|
+
console.log(theme.warn(' ' + w));
|
|
2736
2861
|
continue;
|
|
2737
2862
|
}
|
|
2738
2863
|
if (lower === '/skills') {
|
|
@@ -2771,6 +2896,34 @@ async function main() {
|
|
|
2771
2896
|
show(t('memory.memory'), ctx.memory);
|
|
2772
2897
|
continue;
|
|
2773
2898
|
}
|
|
2899
|
+
// `/remember <text>` — append a durable note to the project MEMORY.md.
|
|
2900
|
+
// Claude Code has the same idea (# shortcut): a fact worth keeping across
|
|
2901
|
+
// sessions is written to disk instead of living only in this chat. We
|
|
2902
|
+
// write to the workdir's own MEMORY.md (the deepest file the loader reads)
|
|
2903
|
+
// and create it when missing.
|
|
2904
|
+
if (lower === '/remember' || lower.startsWith('/remember ')) {
|
|
2905
|
+
const note = trimmed.slice('/remember'.length).trim();
|
|
2906
|
+
if (!note) {
|
|
2907
|
+
console.error(theme.warn(t('remember.usage')));
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
const memFile = path.join(currentWorkdir, 'MEMORY.md');
|
|
2911
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
2912
|
+
let body;
|
|
2913
|
+
try {
|
|
2914
|
+
body = await fs.readFile(memFile, 'utf-8');
|
|
2915
|
+
}
|
|
2916
|
+
catch {
|
|
2917
|
+
body = '# MEMORY\n\nDurable notes accumulated across sessions.\n';
|
|
2918
|
+
}
|
|
2919
|
+
if (body.length && !body.endsWith(NL))
|
|
2920
|
+
body += NL;
|
|
2921
|
+
body += NL + '- (' + stamp + ') ' + note + NL;
|
|
2922
|
+
await fs.writeFile(memFile, body, 'utf-8');
|
|
2923
|
+
transcript.log('remember', { note, file: memFile });
|
|
2924
|
+
console.log(theme.assistant(t('remember.saved', { v: memFile })));
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2774
2927
|
if (lower === '/init' || lower.startsWith('/init ')) {
|
|
2775
2928
|
const force = lower.includes('--force');
|
|
2776
2929
|
const target = path.join(currentWorkdir, 'AGENTS.md');
|
|
@@ -2936,6 +3089,98 @@ async function main() {
|
|
|
2936
3089
|
console.log(theme.system(transcript.file || t('common.off')));
|
|
2937
3090
|
continue;
|
|
2938
3091
|
}
|
|
3092
|
+
if (lower === '/context') {
|
|
3093
|
+
// Show what buildSystemPrompt() injects (AGENTS/MEMORY/skills/commands)
|
|
3094
|
+
// so the operator can see why the context is large. Best-effort: a broken
|
|
3095
|
+
// file must not break the command.
|
|
3096
|
+
try {
|
|
3097
|
+
const { loadProjectContext } = await import('./context.js');
|
|
3098
|
+
const ctx = await loadProjectContext(currentWorkdir);
|
|
3099
|
+
const sysLen = mod.buildSystemPrompt({
|
|
3100
|
+
workdir: currentWorkdir,
|
|
3101
|
+
tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
|
|
3102
|
+
locale: currentLocale,
|
|
3103
|
+
context: ctx,
|
|
3104
|
+
}).length;
|
|
3105
|
+
console.log(theme.system(formatContextSources(ctx.agents, ctx.memory, ctx.skills, ctx.commands, { systemPromptChars: sysLen }, t)));
|
|
3106
|
+
}
|
|
3107
|
+
catch (e) {
|
|
3108
|
+
console.error(theme.error(e.message));
|
|
3109
|
+
}
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
if (lower === '/rename' || lower.startsWith('/rename ')) {
|
|
3113
|
+
const title = trimmed.slice('/rename'.length).trim();
|
|
3114
|
+
if (!title) {
|
|
3115
|
+
console.log(theme.dim(t('rename.usage')));
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
if (!currentChatId) {
|
|
3119
|
+
currentChatId = await browser.getCurrentChatId();
|
|
3120
|
+
}
|
|
3121
|
+
if (!currentChatId) {
|
|
3122
|
+
console.error(theme.error(t('chats.not_created')));
|
|
3123
|
+
continue;
|
|
3124
|
+
}
|
|
3125
|
+
saveLastChat(currentChatId, currentWorkdir, title);
|
|
3126
|
+
console.log(theme.assistant(t('rename.done', { v: title })));
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
if (lower === '/copy') {
|
|
3130
|
+
if (!lastAssistantMessage) {
|
|
3131
|
+
console.log(theme.dim(t('copy.none')));
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
const res = writeClipboardText(lastAssistantMessage);
|
|
3135
|
+
if (res.ok) {
|
|
3136
|
+
console.log(theme.assistant(t('copy.done', { tool: res.via })));
|
|
3137
|
+
}
|
|
3138
|
+
else {
|
|
3139
|
+
console.error(theme.warn(t('copy.failed', { tool: res.via })));
|
|
3140
|
+
}
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
if (lower === '/retry') {
|
|
3144
|
+
if (!lastTaskText) {
|
|
3145
|
+
console.log(theme.dim(t('retry.none')));
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
console.log(theme.system(t('retry.running')));
|
|
3149
|
+
// Re-send the last task into the SAME chat (no new chat, no system
|
|
3150
|
+
// prompt), so a truncated/empty answer can be retried cheaply.
|
|
3151
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
3152
|
+
undo,
|
|
3153
|
+
todos: todoStore,
|
|
3154
|
+
readOnly: planMode,
|
|
3155
|
+
});
|
|
3156
|
+
if (mcpPool)
|
|
3157
|
+
tools.push(...mcpPool.tools);
|
|
3158
|
+
if (editor)
|
|
3159
|
+
editor.busy = true;
|
|
3160
|
+
try {
|
|
3161
|
+
await runTask(browser, tools, lastTaskText, currentWorkdir, {
|
|
3162
|
+
transcript,
|
|
3163
|
+
freshChat: false,
|
|
3164
|
+
sendSystemPrompt: false,
|
|
3165
|
+
queue: pendingQueue,
|
|
3166
|
+
ui: editor || null,
|
|
3167
|
+
onChatReady: (chatId) => {
|
|
3168
|
+
if (chatId) {
|
|
3169
|
+
currentChatId = chatId;
|
|
3170
|
+
saveLastChat(chatId, currentWorkdir);
|
|
3171
|
+
}
|
|
3172
|
+
},
|
|
3173
|
+
contextLimit: config.ui.contextLimit,
|
|
3174
|
+
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3175
|
+
goal: sessionGoal,
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
finally {
|
|
3179
|
+
if (editor)
|
|
3180
|
+
editor.busy = false;
|
|
3181
|
+
}
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
2939
3184
|
if (lower === '/diff' || lower.startsWith('/diff ')) {
|
|
2940
3185
|
const staged = lower.indexOf('--staged') !== -1;
|
|
2941
3186
|
const { runGit } = await import('./gitTools.js');
|
|
@@ -2948,6 +3193,17 @@ async function main() {
|
|
|
2948
3193
|
console.log(theme.system(formatDiff(out, { maxLines: 400 }, t)));
|
|
2949
3194
|
continue;
|
|
2950
3195
|
}
|
|
3196
|
+
if (lower === '/diffstat') {
|
|
3197
|
+
const { runGit } = await import('./gitTools.js');
|
|
3198
|
+
const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
|
|
3199
|
+
if (probe.trim() !== 'true') {
|
|
3200
|
+
console.error(theme.error(t('diff.not_repo')));
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
const out = await runGit('git diff --stat', currentWorkdir, 20_000);
|
|
3204
|
+
console.log(theme.system(formatDiffStat(out, { maxLines: 60 }, t)));
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
2951
3207
|
if (lower === '/cost' || lower === '/usage') {
|
|
2952
3208
|
let stats = summarizeTranscript([]);
|
|
2953
3209
|
if (transcript.file) {
|
|
@@ -3037,15 +3293,6 @@ async function main() {
|
|
|
3037
3293
|
}, t)));
|
|
3038
3294
|
continue;
|
|
3039
3295
|
}
|
|
3040
|
-
if (lower === '/permissions' || lower === '/allowed-tools') {
|
|
3041
|
-
console.log(theme.system(renderPermissions({
|
|
3042
|
-
write: config.confirmation.write,
|
|
3043
|
-
edit: config.confirmation.edit,
|
|
3044
|
-
bash: config.confirmation.bash,
|
|
3045
|
-
alwaysConfirm: config.confirmation.alwaysConfirm,
|
|
3046
|
-
}, t)));
|
|
3047
|
-
continue;
|
|
3048
|
-
}
|
|
3049
3296
|
if (lower === '/add-dir' || lower.startsWith('/add-dir ')) {
|
|
3050
3297
|
const arg = trimmed.slice('/add-dir'.length).trim();
|
|
3051
3298
|
const res = resolveExtraDir(arg, currentWorkdir, t);
|
|
@@ -3128,7 +3375,15 @@ async function main() {
|
|
|
3128
3375
|
transcript.log('undo', { path: result.record.originalPath });
|
|
3129
3376
|
}
|
|
3130
3377
|
else {
|
|
3131
|
-
|
|
3378
|
+
// undo.ts returns machine codes ('empty'/'disabled') — localize here so
|
|
3379
|
+
// an English UI never shows a Russian reason.
|
|
3380
|
+
const reasonKey = result.reason === 'empty'
|
|
3381
|
+
? 'undo.reason_empty'
|
|
3382
|
+
: result.reason === 'disabled'
|
|
3383
|
+
? 'undo.reason_disabled'
|
|
3384
|
+
: null;
|
|
3385
|
+
const reason = reasonKey ? t(reasonKey) : String(result.reason ?? '');
|
|
3386
|
+
console.error(theme.error(t('undo.failed', { v: reason })));
|
|
3132
3387
|
}
|
|
3133
3388
|
continue;
|
|
3134
3389
|
}
|
|
@@ -3212,6 +3467,7 @@ async function main() {
|
|
|
3212
3467
|
}
|
|
3213
3468
|
// ---- Regular task (including in review mode) ----
|
|
3214
3469
|
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
3470
|
+
lastTaskText = taskText;
|
|
3215
3471
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3216
3472
|
await autoReload();
|
|
3217
3473
|
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
@@ -3221,7 +3477,11 @@ async function main() {
|
|
|
3221
3477
|
if (editor)
|
|
3222
3478
|
editor.setCommands(buildSlashCommands());
|
|
3223
3479
|
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
3224
|
-
const tools = mod.createTools(currentWorkdir, {
|
|
3480
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
3481
|
+
undo,
|
|
3482
|
+
todos: todoStore,
|
|
3483
|
+
readOnly: planMode,
|
|
3484
|
+
});
|
|
3225
3485
|
if (mcpPool)
|
|
3226
3486
|
tools.push(...mcpPool.tools);
|
|
3227
3487
|
if (editor)
|
|
@@ -3276,6 +3536,7 @@ async function main() {
|
|
|
3276
3536
|
contextLimit: config.ui.contextLimit,
|
|
3277
3537
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3278
3538
|
goal: sessionGoal,
|
|
3539
|
+
todosQuery: () => todosSummary(),
|
|
3279
3540
|
}, inputAttachments);
|
|
3280
3541
|
}
|
|
3281
3542
|
finally {
|
package/dist/input.js
CHANGED
|
@@ -1298,6 +1298,11 @@ export class LineEditor {
|
|
|
1298
1298
|
// dots) and keep it running until toolResult()/assistant()/stop(). Esc now
|
|
1299
1299
|
// really aborts the tool (the Bash child process is killed via the
|
|
1300
1300
|
// AbortSignal), so the "Esc — стоп" hint is shown.
|
|
1301
|
+
//
|
|
1302
|
+
// The tool status is NOT a send phase: clear the lifecycle prefix first,
|
|
1303
|
+
// otherwise the previous answer's "✓ done" sticks and the status reads
|
|
1304
|
+
// "✓ done: running Bash" on one line.
|
|
1305
|
+
this._sendState = '';
|
|
1301
1306
|
this._startAnimated(translate(this.locale)('spinner.running_tool', { name }));
|
|
1302
1307
|
}
|
|
1303
1308
|
toolResult(result) {
|
package/dist/mcp.js
CHANGED
|
@@ -16,6 +16,24 @@ function packageVersion() {
|
|
|
16
16
|
}
|
|
17
17
|
return '0.0.0';
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Flag stdio servers pinned to a floating version (`@latest` / `@next`).
|
|
21
|
+
* A floating version can change between runs and silently alter the tools the
|
|
22
|
+
* agent sees, so the operator is warned and told to pin an exact version.
|
|
23
|
+
* Pure; exported for tests.
|
|
24
|
+
*/
|
|
25
|
+
export function floatingVersionWarnings(servers) {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
28
|
+
if (cfg.disabled)
|
|
29
|
+
continue;
|
|
30
|
+
const args = [String(cfg.command || ''), ...(cfg.args || [])].join(' ');
|
|
31
|
+
if (/@(latest|next)\b/.test(args)) {
|
|
32
|
+
out.push(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
19
37
|
/** Names of the servers found in a config, before any connection. */
|
|
20
38
|
export async function listConfiguredServers(workdir) {
|
|
21
39
|
const files = await discoverConfigFiles(workdir);
|
|
@@ -298,6 +316,7 @@ export async function createMcpPool(opts) {
|
|
|
298
316
|
const config = await loadConfigFiles(files);
|
|
299
317
|
const servers = config.mcpServers || {};
|
|
300
318
|
const names = Object.keys(servers).filter((n) => !servers[n].disabled);
|
|
319
|
+
const floating = floatingVersionWarnings(servers);
|
|
301
320
|
const infos = [];
|
|
302
321
|
const connections = [];
|
|
303
322
|
const tools = [];
|
|
@@ -339,6 +358,7 @@ export async function createMcpPool(opts) {
|
|
|
339
358
|
status: () => ({
|
|
340
359
|
servers: infos,
|
|
341
360
|
toolCount: tools.length,
|
|
361
|
+
warnings: floating.map((n) => `MCP server "${n}" uses a floating version (@latest/@next); pin an exact version so the tool set does not change between runs.`),
|
|
342
362
|
}),
|
|
343
363
|
close: async () => {
|
|
344
364
|
if (closed)
|