zames_pro 2.55.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 +107 -2
- package/dist/agent-loop.js +67 -0
- package/dist/attachments.js +32 -0
- package/dist/commands.js +52 -0
- package/dist/config.js +3 -9
- package/dist/fsutil.js +18 -0
- package/dist/gitTools.js +37 -0
- package/dist/i18n.js +114 -16
- package/dist/index.js +231 -24
- package/dist/mcp.js +20 -0
- package/dist/sessions.js +4 -8
- package/dist/spinner.js +42 -3
- package/dist/system-prompt.js +13 -0
- package/dist/tools.js +38 -5
- package/dist/transcript.js +14 -4
- package/dist/undo.js +8 -6
- package/dist/web.js +31 -0
- package/package.json +13 -4
- package/scripts/clean-dist.mjs +17 -0
- package/scripts/coverage-gate.mjs +83 -0
package/dist/index.js
CHANGED
|
@@ -11,14 +11,14 @@ 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, resolveExtraDir, buildReviewPrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
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';
|
|
@@ -27,6 +27,10 @@ import { assertCommandInsideRoot, runShell } from './shell.js';
|
|
|
27
27
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
28
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
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 = '';
|
|
30
34
|
// Floor for a /loop interval. A loop faster than the send throttle (15s)
|
|
31
35
|
// cannot do anything useful and would just hammer the rate limit, so we refuse
|
|
32
36
|
// it. It does NOT touch the throttle itself.
|
|
@@ -54,7 +58,7 @@ function getPositional() {
|
|
|
54
58
|
const positional = [];
|
|
55
59
|
for (let i = 0; i < args.length; i++) {
|
|
56
60
|
const a = args[i];
|
|
57
|
-
if (['--dir', '--task', '--max-iter', '--
|
|
61
|
+
if (['--dir', '--task', '--max-iter', '--chat', '--output-format'].includes(a)) {
|
|
58
62
|
i++;
|
|
59
63
|
continue;
|
|
60
64
|
}
|
|
@@ -88,7 +92,6 @@ const headless = hasFlag('--headed')
|
|
|
88
92
|
? false
|
|
89
93
|
: hasFlag('--headless') || config.headless;
|
|
90
94
|
const debug = hasFlag('--debug') || config.debug;
|
|
91
|
-
const calibrate = hasFlag('--calibrate');
|
|
92
95
|
const maxIterArg = getArg('--max-iter', null);
|
|
93
96
|
const maxIter = maxIterArg !== null ? Number(maxIterArg) : config.maxIterations;
|
|
94
97
|
const positional = getPositional();
|
|
@@ -103,6 +106,23 @@ const resendPrompt = hasFlag('--resend-prompt');
|
|
|
103
106
|
// --new-chat: kept for compatibility — this is the default behavior anyway.
|
|
104
107
|
const newChatFlag = hasFlag('--new-chat');
|
|
105
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
|
+
}
|
|
106
126
|
// ---------- session persistence ----------
|
|
107
127
|
// We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
|
|
108
128
|
// a restart and are available for explicit restore (--resume-last,
|
|
@@ -171,6 +191,7 @@ const RELOADABLE = [
|
|
|
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')}
|
|
@@ -312,42 +334,60 @@ ${theme.bold(t('help.while_working'))}
|
|
|
312
334
|
${t('help.key.bang')}
|
|
313
335
|
|
|
314
336
|
${theme.bold(t('help.commands'))}
|
|
337
|
+
|
|
338
|
+
${theme.bold(t('help.sec.session'))}
|
|
315
339
|
${t('help.cmd.new')}
|
|
316
340
|
${t('help.cmd.sessions')}
|
|
317
|
-
${t('help.cmd.resume_id')}
|
|
318
341
|
${t('help.cmd.chats')}
|
|
319
342
|
${t('help.cmd.resume')}
|
|
343
|
+
${t('help.cmd.resume_id')}
|
|
320
344
|
${t('help.cmd.last')}
|
|
345
|
+
${t('help.cmd.retry')}
|
|
346
|
+
${t('help.cmd.rename')}
|
|
347
|
+
|
|
348
|
+
${theme.bold(t('help.sec.workspace'))}
|
|
321
349
|
${t('help.cmd.chat')}
|
|
322
350
|
${t('help.cmd.cd')}
|
|
323
351
|
${t('help.cmd.pwd')}
|
|
324
352
|
${t('help.cmd.status')}
|
|
325
353
|
${t('help.cmd.reload')}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
${t('help.cmd.transcript')}
|
|
354
|
+
|
|
355
|
+
${theme.bold(t('help.sec.git'))}
|
|
329
356
|
${t('help.cmd.diff')}
|
|
330
|
-
${t('help.cmd.
|
|
331
|
-
|
|
332
|
-
|
|
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')}
|
|
344
|
-
|
|
345
|
-
|
|
370
|
+
|
|
371
|
+
${theme.bold(t('help.sec.context'))}
|
|
372
|
+
${t('help.cmd.context')}
|
|
346
373
|
${t('help.cmd.skills')}
|
|
347
374
|
${t('help.cmd.memory')}
|
|
348
375
|
${t('help.cmd.remember')}
|
|
349
376
|
${t('help.cmd.init')}
|
|
350
377
|
${t('help.cmd.mcp')}
|
|
378
|
+
${t('help.cmd.config')}
|
|
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')}
|
|
351
391
|
${t('help.cmd.debug_dom')}
|
|
352
392
|
${t('help.cmd.help')}
|
|
353
393
|
${t('help.cmd.exit')}
|
|
@@ -389,6 +429,10 @@ const SLASH_COMMANDS = [
|
|
|
389
429
|
{ name: '/resume', key: 'help.cmd.resume' },
|
|
390
430
|
{ name: '/resume-id', key: 'help.cmd.resume_id' },
|
|
391
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' },
|
|
392
436
|
{ name: '/chat', key: 'help.cmd.chat' },
|
|
393
437
|
{ name: '/cd', key: 'help.cmd.cd' },
|
|
394
438
|
{ name: '/pwd', key: 'help.cmd.pwd' },
|
|
@@ -398,6 +442,7 @@ const SLASH_COMMANDS = [
|
|
|
398
442
|
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
399
443
|
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
400
444
|
{ name: '/diff', key: 'help.cmd.diff' },
|
|
445
|
+
{ name: '/diffstat', key: 'help.cmd.diffstat' },
|
|
401
446
|
{ name: '/cost', key: 'help.cmd.cost' },
|
|
402
447
|
{ name: '/export', key: 'help.cmd.export' },
|
|
403
448
|
{ name: '/doctor', key: 'help.cmd.doctor' },
|
|
@@ -409,6 +454,7 @@ const SLASH_COMMANDS = [
|
|
|
409
454
|
{ name: '/jobs', key: 'help.cmd.jobs' },
|
|
410
455
|
{ name: '/thinking', key: 'help.cmd.thinking' },
|
|
411
456
|
{ name: '/web', key: 'help.cmd.web' },
|
|
457
|
+
{ name: '/plan', key: 'help.cmd.plan' },
|
|
412
458
|
{ name: '/compact', key: 'help.cmd.compact' },
|
|
413
459
|
{ name: '/queue', key: 'help.cmd.queue' },
|
|
414
460
|
{ name: '/tasks', key: 'help.cmd.tasks' },
|
|
@@ -1008,7 +1054,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
1008
1054
|
}
|
|
1009
1055
|
// ---------- task runner ----------
|
|
1010
1056
|
async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
|
|
1011
|
-
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;
|
|
1012
1058
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
1013
1059
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
1014
1060
|
// mode (pipes) — a regular spinner + watchInput.
|
|
@@ -1016,6 +1062,12 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1016
1062
|
browser._stopped = false;
|
|
1017
1063
|
browser._abort = false;
|
|
1018
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
|
+
}
|
|
1019
1071
|
// Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
|
|
1020
1072
|
// Esc / Ctrl+C abort the current generation only while busy; without this a
|
|
1021
1073
|
// long-running tool (Bash, npm, MCP) could not be interrupted — Esc did
|
|
@@ -1089,7 +1141,14 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1089
1141
|
ui.toolCall(name, toolArgs);
|
|
1090
1142
|
},
|
|
1091
1143
|
onToolResult: (result) => ui.toolResult(result),
|
|
1092
|
-
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
|
+
},
|
|
1093
1152
|
onWarning: (msg) => ui.warning(msg),
|
|
1094
1153
|
onChatReady,
|
|
1095
1154
|
debugLog: debug,
|
|
@@ -1221,9 +1280,6 @@ async function main() {
|
|
|
1221
1280
|
printHelp();
|
|
1222
1281
|
return;
|
|
1223
1282
|
}
|
|
1224
|
-
if (calibrate) {
|
|
1225
|
-
console.log(theme.warn(t('calibrate')));
|
|
1226
|
-
}
|
|
1227
1283
|
await cleanTmpDir();
|
|
1228
1284
|
let currentWorkdir;
|
|
1229
1285
|
let sandboxRoot;
|
|
@@ -1242,6 +1298,9 @@ async function main() {
|
|
|
1242
1298
|
dir: config.transcript.dir,
|
|
1243
1299
|
enabled: config.transcript.enabled,
|
|
1244
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,
|
|
1245
1304
|
});
|
|
1246
1305
|
if (transcript.file) {
|
|
1247
1306
|
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
@@ -1263,6 +1322,8 @@ async function main() {
|
|
|
1263
1322
|
if (srv.error)
|
|
1264
1323
|
console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
|
|
1265
1324
|
}
|
|
1325
|
+
for (const w of st.warnings)
|
|
1326
|
+
console.error(theme.warn(w));
|
|
1266
1327
|
}
|
|
1267
1328
|
catch (e) {
|
|
1268
1329
|
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
@@ -1303,7 +1364,10 @@ async function main() {
|
|
|
1303
1364
|
}
|
|
1304
1365
|
// One-shot mode
|
|
1305
1366
|
if (task) {
|
|
1306
|
-
const tools = mod.createTools(currentWorkdir, {
|
|
1367
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
1368
|
+
undo,
|
|
1369
|
+
readOnly: planMode,
|
|
1370
|
+
});
|
|
1307
1371
|
if (mcpPool)
|
|
1308
1372
|
tools.push(...mcpPool.tools);
|
|
1309
1373
|
let freshChat = true;
|
|
@@ -1366,6 +1430,8 @@ async function main() {
|
|
|
1366
1430
|
const promptOnResume = () => resendPrompt || config.browser.resendPromptOnResume === true;
|
|
1367
1431
|
let lastChats = [];
|
|
1368
1432
|
let currentChatId = null;
|
|
1433
|
+
// The last task the operator sent (used by /retry). Plain text only.
|
|
1434
|
+
let lastTaskText = '';
|
|
1369
1435
|
let running = true;
|
|
1370
1436
|
// The agent's TodoWrite checklist, shared with the tool factory and
|
|
1371
1437
|
// persisted in the session JSON. A store (not module state) so /reload does
|
|
@@ -2746,6 +2812,28 @@ async function main() {
|
|
|
2746
2812
|
continue;
|
|
2747
2813
|
}
|
|
2748
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
|
+
}
|
|
2749
2837
|
if (lower === '/mcp') {
|
|
2750
2838
|
if (!mcpPool) {
|
|
2751
2839
|
console.log(theme.dim(t('mcp.none')));
|
|
@@ -2768,6 +2856,8 @@ async function main() {
|
|
|
2768
2856
|
theme.dim('(' + String(srv.tools.length) + ')'));
|
|
2769
2857
|
}
|
|
2770
2858
|
}
|
|
2859
|
+
for (const w of st.warnings)
|
|
2860
|
+
console.log(theme.warn(' ' + w));
|
|
2771
2861
|
continue;
|
|
2772
2862
|
}
|
|
2773
2863
|
if (lower === '/skills') {
|
|
@@ -2999,6 +3089,98 @@ async function main() {
|
|
|
2999
3089
|
console.log(theme.system(transcript.file || t('common.off')));
|
|
3000
3090
|
continue;
|
|
3001
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
|
+
}
|
|
3002
3184
|
if (lower === '/diff' || lower.startsWith('/diff ')) {
|
|
3003
3185
|
const staged = lower.indexOf('--staged') !== -1;
|
|
3004
3186
|
const { runGit } = await import('./gitTools.js');
|
|
@@ -3011,6 +3193,17 @@ async function main() {
|
|
|
3011
3193
|
console.log(theme.system(formatDiff(out, { maxLines: 400 }, t)));
|
|
3012
3194
|
continue;
|
|
3013
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
|
+
}
|
|
3014
3207
|
if (lower === '/cost' || lower === '/usage') {
|
|
3015
3208
|
let stats = summarizeTranscript([]);
|
|
3016
3209
|
if (transcript.file) {
|
|
@@ -3182,7 +3375,15 @@ async function main() {
|
|
|
3182
3375
|
transcript.log('undo', { path: result.record.originalPath });
|
|
3183
3376
|
}
|
|
3184
3377
|
else {
|
|
3185
|
-
|
|
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 })));
|
|
3186
3387
|
}
|
|
3187
3388
|
continue;
|
|
3188
3389
|
}
|
|
@@ -3266,6 +3467,7 @@ async function main() {
|
|
|
3266
3467
|
}
|
|
3267
3468
|
// ---- Regular task (including in review mode) ----
|
|
3268
3469
|
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
3470
|
+
lastTaskText = taskText;
|
|
3269
3471
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3270
3472
|
await autoReload();
|
|
3271
3473
|
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
@@ -3275,7 +3477,11 @@ async function main() {
|
|
|
3275
3477
|
if (editor)
|
|
3276
3478
|
editor.setCommands(buildSlashCommands());
|
|
3277
3479
|
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
3278
|
-
const tools = mod.createTools(currentWorkdir, {
|
|
3480
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
3481
|
+
undo,
|
|
3482
|
+
todos: todoStore,
|
|
3483
|
+
readOnly: planMode,
|
|
3484
|
+
});
|
|
3279
3485
|
if (mcpPool)
|
|
3280
3486
|
tools.push(...mcpPool.tools);
|
|
3281
3487
|
if (editor)
|
|
@@ -3330,6 +3536,7 @@ async function main() {
|
|
|
3330
3536
|
contextLimit: config.ui.contextLimit,
|
|
3331
3537
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3332
3538
|
goal: sessionGoal,
|
|
3539
|
+
todosQuery: () => todosSummary(),
|
|
3333
3540
|
}, inputAttachments);
|
|
3334
3541
|
}
|
|
3335
3542
|
finally {
|
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)
|
package/dist/sessions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { writeJsonAtomic } from './fsutil.js';
|
|
4
5
|
// We store sessions (DeepSeek chats) in a separate folder so they aren't lost
|
|
5
6
|
// after a process restart. Each session is a separate JSON file
|
|
6
7
|
// <id>.json in ~/.zames/.sessions. last.json points to the last
|
|
@@ -8,11 +9,6 @@ import os from 'os';
|
|
|
8
9
|
const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
|
|
9
10
|
const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
|
|
10
11
|
export const SESSIONS_FORMAT_VERSION = 1;
|
|
11
|
-
function atomicWriteJson(file, value) {
|
|
12
|
-
const tmp = file + '.tmp-' + process.pid;
|
|
13
|
-
fs.writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf-8');
|
|
14
|
-
fs.renameSync(tmp, file);
|
|
15
|
-
}
|
|
16
12
|
function ensureDir() {
|
|
17
13
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
18
14
|
}
|
|
@@ -46,7 +42,7 @@ export function saveSession({ id, title = '', workdir = '', todos, }) {
|
|
|
46
42
|
const nextTodos = todos !== undefined ? todos : prev.todos;
|
|
47
43
|
if (nextTodos !== undefined)
|
|
48
44
|
data.todos = nextTodos;
|
|
49
|
-
|
|
45
|
+
writeJsonAtomic(file, data);
|
|
50
46
|
writeIndex({ id, workdir: data.workdir });
|
|
51
47
|
return data;
|
|
52
48
|
}
|
|
@@ -71,7 +67,7 @@ function writeIndex({ id, workdir }) {
|
|
|
71
67
|
index.byWorkdir[key] = id;
|
|
72
68
|
index.last = id;
|
|
73
69
|
index.updatedAt = new Date().toISOString();
|
|
74
|
-
|
|
70
|
+
writeJsonAtomic(INDEX_FILE, index);
|
|
75
71
|
}
|
|
76
72
|
catch { }
|
|
77
73
|
}
|
|
@@ -154,7 +150,7 @@ export function saveHistory(history) {
|
|
|
154
150
|
try {
|
|
155
151
|
ensureDir();
|
|
156
152
|
const trimmed = history.slice(-HISTORY_LIMIT);
|
|
157
|
-
|
|
153
|
+
writeJsonAtomic(HISTORY_FILE, trimmed);
|
|
158
154
|
}
|
|
159
155
|
catch { }
|
|
160
156
|
}
|
package/dist/spinner.js
CHANGED
|
@@ -27,6 +27,9 @@ export function renderDots(n) {
|
|
|
27
27
|
return theme.brown(d + DOTS_PAD.slice(d.length));
|
|
28
28
|
}
|
|
29
29
|
export function createSpinner(locale = 'ru') {
|
|
30
|
+
// Set by the caller (index.ts) to the same todos summary the LineEditor
|
|
31
|
+
// shows, so the non-TTY path has parity.
|
|
32
|
+
let onTasksQuery = null;
|
|
30
33
|
let spinner = null;
|
|
31
34
|
let dotTimer = null;
|
|
32
35
|
let dotPhase = 0;
|
|
@@ -37,6 +40,10 @@ export function createSpinner(locale = 'ru') {
|
|
|
37
40
|
// zero every second and look frozen on a single dot.
|
|
38
41
|
let animBase = '';
|
|
39
42
|
let animating = false;
|
|
43
|
+
// When the current animated phase began, so a long reasoning turn can show
|
|
44
|
+
// "12s" / "2m 05s" instead of looking identical to a hung one. Parity with
|
|
45
|
+
// the LineEditor status line (src/input.ts).
|
|
46
|
+
let animStart = 0;
|
|
40
47
|
const start = (text) => {
|
|
41
48
|
if (!spinner)
|
|
42
49
|
spinner = ora(text).start();
|
|
@@ -59,9 +66,34 @@ export function createSpinner(locale = 'ru') {
|
|
|
59
66
|
// "running" dot sequence. Shared by the thinking spinner and the send-pause
|
|
60
67
|
// indicator, so the pause is animated too (before, it was a static line and
|
|
61
68
|
// the dots did not move — the operator saw a frozen spinner).
|
|
69
|
+
// Elapsed label of the current animated phase ("0s", "45s", "2m 05s").
|
|
70
|
+
// Empty when nothing is animating. Localized units come from the catalog.
|
|
71
|
+
const elapsedLabel = () => {
|
|
72
|
+
if (!animating || !animStart)
|
|
73
|
+
return '';
|
|
74
|
+
const sec = Math.floor((Date.now() - animStart) / 1000);
|
|
75
|
+
const m = Math.floor(sec / 60);
|
|
76
|
+
const s = sec % 60;
|
|
77
|
+
const text = m > 0 ? m + 'm ' + String(s).padStart(2, '0') + 's' : s + 's';
|
|
78
|
+
return theme.dim(' · ' + text);
|
|
79
|
+
};
|
|
80
|
+
// Task-list badge, refreshed from onTasksQuery on every animation tick.
|
|
81
|
+
// Mirrors the LineEditor so the non-TTY spinner shows the same "tasks: 2/5".
|
|
82
|
+
const tasksBadge = () => {
|
|
83
|
+
if (!onTasksQuery)
|
|
84
|
+
return '';
|
|
85
|
+
try {
|
|
86
|
+
const s = onTasksQuery();
|
|
87
|
+
return s ? theme.system(s) + ' ' : '';
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
};
|
|
62
93
|
const startAnimated = (baseText) => {
|
|
63
94
|
animBase = theme.brown(stripEllipsis(baseText));
|
|
64
|
-
|
|
95
|
+
animStart = Date.now();
|
|
96
|
+
start(animBase + renderDots(0) + elapsedLabel() + tasksBadge() + HINT);
|
|
65
97
|
dotPhase = 0;
|
|
66
98
|
if (dotTimer)
|
|
67
99
|
clearInterval(dotTimer);
|
|
@@ -69,7 +101,8 @@ export function createSpinner(locale = 'ru') {
|
|
|
69
101
|
if (!spinner)
|
|
70
102
|
return;
|
|
71
103
|
dotPhase = (dotPhase + 1) % DOTS.length;
|
|
72
|
-
spinner.text =
|
|
104
|
+
spinner.text =
|
|
105
|
+
animBase + renderDots(dotPhase) + elapsedLabel() + tasksBadge() + HINT;
|
|
73
106
|
}, 400);
|
|
74
107
|
animating = true;
|
|
75
108
|
if (dotTimer.unref)
|
|
@@ -93,6 +126,12 @@ export function createSpinner(locale = 'ru') {
|
|
|
93
126
|
spinner.text = theme.prompt('✎ ') + pending + HINT;
|
|
94
127
|
};
|
|
95
128
|
return {
|
|
129
|
+
get onTasksQuery() {
|
|
130
|
+
return onTasksQuery;
|
|
131
|
+
},
|
|
132
|
+
set onTasksQuery(fn) {
|
|
133
|
+
onTasksQuery = fn;
|
|
134
|
+
},
|
|
96
135
|
thinking: () => {
|
|
97
136
|
if (pending) {
|
|
98
137
|
showPending();
|
|
@@ -112,7 +151,7 @@ export function createSpinner(locale = 'ru') {
|
|
|
112
151
|
// so the countdown refreshes without resetting the dots.
|
|
113
152
|
if (animating && dotTimer && spinner) {
|
|
114
153
|
animBase = theme.brown(stripEllipsis(label));
|
|
115
|
-
spinner.text = animBase + renderDots(dotPhase) + HINT;
|
|
154
|
+
spinner.text = animBase + renderDots(dotPhase) + elapsedLabel() + HINT;
|
|
116
155
|
return;
|
|
117
156
|
}
|
|
118
157
|
startAnimated(label);
|
package/dist/system-prompt.js
CHANGED
|
@@ -128,6 +128,19 @@ You have access to the following tools:
|
|
|
128
128
|
|
|
129
129
|
${toolDescriptions}
|
|
130
130
|
${gitSection}${attachSection}${contextSection}
|
|
131
|
+
## Choosing the right tool
|
|
132
|
+
|
|
133
|
+
Prefer the most specific tool; a wrong choice wastes a turn:
|
|
134
|
+
|
|
135
|
+
- Several small edits in ONE file -> MultiEdit (applied atomically), not
|
|
136
|
+
several Edit calls.
|
|
137
|
+
- Changes across SEVERAL files -> ApplyPatch (V4A), not a series of writes.
|
|
138
|
+
- One exact replacement -> Edit. Creating or overwriting a whole file -> Write.
|
|
139
|
+
- Searching file CONTENT -> Grep; finding files by NAME -> Glob; listing a
|
|
140
|
+
directory -> LS.
|
|
141
|
+
- Git operations -> the Git* tools, not Bash.
|
|
142
|
+
- Reading a file whose path you already know -> Read, not Bash cat.
|
|
143
|
+
|
|
131
144
|
## MCP tools
|
|
132
145
|
|
|
133
146
|
Some tools are provided by external MCP servers and their names look
|