zames_pro 2.55.0 → 2.59.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 +220 -0
- package/README.md +111 -2
- package/dist/agent-loop.js +67 -0
- package/dist/attachments.js +32 -0
- package/dist/commands.js +125 -0
- package/dist/config.js +3 -9
- package/dist/fsutil.js +18 -0
- package/dist/gitTools.js +37 -0
- package/dist/i18n.js +134 -16
- package/dist/index.js +285 -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, parseBacklogItems, nextBacklogItem, buildImprovePrompt, 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,61 @@ ${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')}
|
|
391
|
+
${t('help.cmd.improve')}
|
|
351
392
|
${t('help.cmd.debug_dom')}
|
|
352
393
|
${t('help.cmd.help')}
|
|
353
394
|
${t('help.cmd.exit')}
|
|
@@ -389,6 +430,10 @@ const SLASH_COMMANDS = [
|
|
|
389
430
|
{ name: '/resume', key: 'help.cmd.resume' },
|
|
390
431
|
{ name: '/resume-id', key: 'help.cmd.resume_id' },
|
|
391
432
|
{ name: '/last', key: 'help.cmd.last' },
|
|
433
|
+
{ name: '/retry', key: 'help.cmd.retry' },
|
|
434
|
+
{ name: '/rename', key: 'help.cmd.rename' },
|
|
435
|
+
{ name: '/context', key: 'help.cmd.context' },
|
|
436
|
+
{ name: '/copy', key: 'help.cmd.copy' },
|
|
392
437
|
{ name: '/chat', key: 'help.cmd.chat' },
|
|
393
438
|
{ name: '/cd', key: 'help.cmd.cd' },
|
|
394
439
|
{ name: '/pwd', key: 'help.cmd.pwd' },
|
|
@@ -398,17 +443,20 @@ const SLASH_COMMANDS = [
|
|
|
398
443
|
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
399
444
|
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
400
445
|
{ name: '/diff', key: 'help.cmd.diff' },
|
|
446
|
+
{ name: '/diffstat', key: 'help.cmd.diffstat' },
|
|
401
447
|
{ name: '/cost', key: 'help.cmd.cost' },
|
|
402
448
|
{ name: '/export', key: 'help.cmd.export' },
|
|
403
449
|
{ name: '/doctor', key: 'help.cmd.doctor' },
|
|
404
450
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
405
451
|
{ name: '/review', key: 'help.cmd.review' },
|
|
452
|
+
{ name: '/improve', key: 'help.cmd.improve' },
|
|
406
453
|
{ name: '/goal', key: 'help.cmd.goal' },
|
|
407
454
|
{ name: '/loop', key: 'help.cmd.loop' },
|
|
408
455
|
{ name: '/cron', key: 'help.cmd.cron' },
|
|
409
456
|
{ name: '/jobs', key: 'help.cmd.jobs' },
|
|
410
457
|
{ name: '/thinking', key: 'help.cmd.thinking' },
|
|
411
458
|
{ name: '/web', key: 'help.cmd.web' },
|
|
459
|
+
{ name: '/plan', key: 'help.cmd.plan' },
|
|
412
460
|
{ name: '/compact', key: 'help.cmd.compact' },
|
|
413
461
|
{ name: '/queue', key: 'help.cmd.queue' },
|
|
414
462
|
{ name: '/tasks', key: 'help.cmd.tasks' },
|
|
@@ -1008,7 +1056,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
1008
1056
|
}
|
|
1009
1057
|
// ---------- task runner ----------
|
|
1010
1058
|
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;
|
|
1059
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, } = opts;
|
|
1012
1060
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
1013
1061
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
1014
1062
|
// mode (pipes) — a regular spinner + watchInput.
|
|
@@ -1016,6 +1064,12 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1016
1064
|
browser._stopped = false;
|
|
1017
1065
|
browser._abort = false;
|
|
1018
1066
|
const ui = editor || mod.createSpinner(currentLocale);
|
|
1067
|
+
// Non-TTY parity: the ora spinner shows the same "tasks: 2/5" badge the
|
|
1068
|
+
// LineEditor shows (the editor gets it via onTasksQuery in main()).
|
|
1069
|
+
if (!editor && todosQuery && 'onTasksQuery' in ui) {
|
|
1070
|
+
;
|
|
1071
|
+
ui.onTasksQuery = todosQuery;
|
|
1072
|
+
}
|
|
1019
1073
|
// Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
|
|
1020
1074
|
// Esc / Ctrl+C abort the current generation only while busy; without this a
|
|
1021
1075
|
// long-running tool (Bash, npm, MCP) could not be interrupted — Esc did
|
|
@@ -1089,7 +1143,14 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1089
1143
|
ui.toolCall(name, toolArgs);
|
|
1090
1144
|
},
|
|
1091
1145
|
onToolResult: (result) => ui.toolResult(result),
|
|
1092
|
-
onAssistantMessage: (msg) =>
|
|
1146
|
+
onAssistantMessage: (msg) => {
|
|
1147
|
+
// Remember the last answer so /copy can put it on the clipboard
|
|
1148
|
+
// without re-reading the chat. Wrapped: a UI failure is swallowed
|
|
1149
|
+
// by agent-loop anyway.
|
|
1150
|
+
lastAssistantMessage = msg;
|
|
1151
|
+
opts.onAssistantFinal?.(msg);
|
|
1152
|
+
ui.assistant(msg);
|
|
1153
|
+
},
|
|
1093
1154
|
onWarning: (msg) => ui.warning(msg),
|
|
1094
1155
|
onChatReady,
|
|
1095
1156
|
debugLog: debug,
|
|
@@ -1221,9 +1282,6 @@ async function main() {
|
|
|
1221
1282
|
printHelp();
|
|
1222
1283
|
return;
|
|
1223
1284
|
}
|
|
1224
|
-
if (calibrate) {
|
|
1225
|
-
console.log(theme.warn(t('calibrate')));
|
|
1226
|
-
}
|
|
1227
1285
|
await cleanTmpDir();
|
|
1228
1286
|
let currentWorkdir;
|
|
1229
1287
|
let sandboxRoot;
|
|
@@ -1242,6 +1300,9 @@ async function main() {
|
|
|
1242
1300
|
dir: config.transcript.dir,
|
|
1243
1301
|
enabled: config.transcript.enabled,
|
|
1244
1302
|
sessionName: dirLabel(currentWorkdir),
|
|
1303
|
+
// In json/jsonl mode mirror every event to stdout as a JSON line, even
|
|
1304
|
+
// when the transcript FILE is disabled.
|
|
1305
|
+
onLine: jsonMode ? (line) => process.stdout.write(line) : null,
|
|
1245
1306
|
});
|
|
1246
1307
|
if (transcript.file) {
|
|
1247
1308
|
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
@@ -1263,6 +1324,8 @@ async function main() {
|
|
|
1263
1324
|
if (srv.error)
|
|
1264
1325
|
console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
|
|
1265
1326
|
}
|
|
1327
|
+
for (const w of st.warnings)
|
|
1328
|
+
console.error(theme.warn(w));
|
|
1266
1329
|
}
|
|
1267
1330
|
catch (e) {
|
|
1268
1331
|
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
@@ -1303,7 +1366,10 @@ async function main() {
|
|
|
1303
1366
|
}
|
|
1304
1367
|
// One-shot mode
|
|
1305
1368
|
if (task) {
|
|
1306
|
-
const tools = mod.createTools(currentWorkdir, {
|
|
1369
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
1370
|
+
undo,
|
|
1371
|
+
readOnly: planMode,
|
|
1372
|
+
});
|
|
1307
1373
|
if (mcpPool)
|
|
1308
1374
|
tools.push(...mcpPool.tools);
|
|
1309
1375
|
let freshChat = true;
|
|
@@ -1366,6 +1432,8 @@ async function main() {
|
|
|
1366
1432
|
const promptOnResume = () => resendPrompt || config.browser.resendPromptOnResume === true;
|
|
1367
1433
|
let lastChats = [];
|
|
1368
1434
|
let currentChatId = null;
|
|
1435
|
+
// The last task the operator sent (used by /retry). Plain text only.
|
|
1436
|
+
let lastTaskText = '';
|
|
1369
1437
|
let running = true;
|
|
1370
1438
|
// The agent's TodoWrite checklist, shared with the tool factory and
|
|
1371
1439
|
// persisted in the session JSON. A store (not module state) so /reload does
|
|
@@ -2746,6 +2814,28 @@ async function main() {
|
|
|
2746
2814
|
continue;
|
|
2747
2815
|
}
|
|
2748
2816
|
}
|
|
2817
|
+
if (lower === '/plan' || lower.startsWith('/plan ')) {
|
|
2818
|
+
const arg = trimmed.slice('/plan'.length).trim().toLowerCase();
|
|
2819
|
+
if (arg === '' || arg === 'toggle')
|
|
2820
|
+
planMode = !planMode;
|
|
2821
|
+
else if (arg === 'on' || arg === '1' || arg === 'true' || arg === 'вкл')
|
|
2822
|
+
planMode = true;
|
|
2823
|
+
else if (arg === 'off' ||
|
|
2824
|
+
arg === '0' ||
|
|
2825
|
+
arg === 'false' ||
|
|
2826
|
+
arg === 'выкл')
|
|
2827
|
+
planMode = false;
|
|
2828
|
+
else {
|
|
2829
|
+
console.error(theme.warn(t('msg.unknown_cmd', { v: trimmed })));
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
const line = t(planMode ? 'plan.on' : 'plan.off');
|
|
2833
|
+
if (editor)
|
|
2834
|
+
editor.printAbove(theme.system(line));
|
|
2835
|
+
else
|
|
2836
|
+
console.log(theme.system(line));
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2749
2839
|
if (lower === '/mcp') {
|
|
2750
2840
|
if (!mcpPool) {
|
|
2751
2841
|
console.log(theme.dim(t('mcp.none')));
|
|
@@ -2768,6 +2858,8 @@ async function main() {
|
|
|
2768
2858
|
theme.dim('(' + String(srv.tools.length) + ')'));
|
|
2769
2859
|
}
|
|
2770
2860
|
}
|
|
2861
|
+
for (const w of st.warnings)
|
|
2862
|
+
console.log(theme.warn(' ' + w));
|
|
2771
2863
|
continue;
|
|
2772
2864
|
}
|
|
2773
2865
|
if (lower === '/skills') {
|
|
@@ -2999,6 +3091,98 @@ async function main() {
|
|
|
2999
3091
|
console.log(theme.system(transcript.file || t('common.off')));
|
|
3000
3092
|
continue;
|
|
3001
3093
|
}
|
|
3094
|
+
if (lower === '/context') {
|
|
3095
|
+
// Show what buildSystemPrompt() injects (AGENTS/MEMORY/skills/commands)
|
|
3096
|
+
// so the operator can see why the context is large. Best-effort: a broken
|
|
3097
|
+
// file must not break the command.
|
|
3098
|
+
try {
|
|
3099
|
+
const { loadProjectContext } = await import('./context.js');
|
|
3100
|
+
const ctx = await loadProjectContext(currentWorkdir);
|
|
3101
|
+
const sysLen = mod.buildSystemPrompt({
|
|
3102
|
+
workdir: currentWorkdir,
|
|
3103
|
+
tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
|
|
3104
|
+
locale: currentLocale,
|
|
3105
|
+
context: ctx,
|
|
3106
|
+
}).length;
|
|
3107
|
+
console.log(theme.system(formatContextSources(ctx.agents, ctx.memory, ctx.skills, ctx.commands, { systemPromptChars: sysLen }, t)));
|
|
3108
|
+
}
|
|
3109
|
+
catch (e) {
|
|
3110
|
+
console.error(theme.error(e.message));
|
|
3111
|
+
}
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
if (lower === '/rename' || lower.startsWith('/rename ')) {
|
|
3115
|
+
const title = trimmed.slice('/rename'.length).trim();
|
|
3116
|
+
if (!title) {
|
|
3117
|
+
console.log(theme.dim(t('rename.usage')));
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
if (!currentChatId) {
|
|
3121
|
+
currentChatId = await browser.getCurrentChatId();
|
|
3122
|
+
}
|
|
3123
|
+
if (!currentChatId) {
|
|
3124
|
+
console.error(theme.error(t('chats.not_created')));
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
saveLastChat(currentChatId, currentWorkdir, title);
|
|
3128
|
+
console.log(theme.assistant(t('rename.done', { v: title })));
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
if (lower === '/copy') {
|
|
3132
|
+
if (!lastAssistantMessage) {
|
|
3133
|
+
console.log(theme.dim(t('copy.none')));
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
const res = writeClipboardText(lastAssistantMessage);
|
|
3137
|
+
if (res.ok) {
|
|
3138
|
+
console.log(theme.assistant(t('copy.done', { tool: res.via })));
|
|
3139
|
+
}
|
|
3140
|
+
else {
|
|
3141
|
+
console.error(theme.warn(t('copy.failed', { tool: res.via })));
|
|
3142
|
+
}
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
if (lower === '/retry') {
|
|
3146
|
+
if (!lastTaskText) {
|
|
3147
|
+
console.log(theme.dim(t('retry.none')));
|
|
3148
|
+
continue;
|
|
3149
|
+
}
|
|
3150
|
+
console.log(theme.system(t('retry.running')));
|
|
3151
|
+
// Re-send the last task into the SAME chat (no new chat, no system
|
|
3152
|
+
// prompt), so a truncated/empty answer can be retried cheaply.
|
|
3153
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
3154
|
+
undo,
|
|
3155
|
+
todos: todoStore,
|
|
3156
|
+
readOnly: planMode,
|
|
3157
|
+
});
|
|
3158
|
+
if (mcpPool)
|
|
3159
|
+
tools.push(...mcpPool.tools);
|
|
3160
|
+
if (editor)
|
|
3161
|
+
editor.busy = true;
|
|
3162
|
+
try {
|
|
3163
|
+
await runTask(browser, tools, lastTaskText, currentWorkdir, {
|
|
3164
|
+
transcript,
|
|
3165
|
+
freshChat: false,
|
|
3166
|
+
sendSystemPrompt: false,
|
|
3167
|
+
queue: pendingQueue,
|
|
3168
|
+
ui: editor || null,
|
|
3169
|
+
onChatReady: (chatId) => {
|
|
3170
|
+
if (chatId) {
|
|
3171
|
+
currentChatId = chatId;
|
|
3172
|
+
saveLastChat(chatId, currentWorkdir);
|
|
3173
|
+
}
|
|
3174
|
+
},
|
|
3175
|
+
contextLimit: config.ui.contextLimit,
|
|
3176
|
+
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3177
|
+
goal: sessionGoal,
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
finally {
|
|
3181
|
+
if (editor)
|
|
3182
|
+
editor.busy = false;
|
|
3183
|
+
}
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3002
3186
|
if (lower === '/diff' || lower.startsWith('/diff ')) {
|
|
3003
3187
|
const staged = lower.indexOf('--staged') !== -1;
|
|
3004
3188
|
const { runGit } = await import('./gitTools.js');
|
|
@@ -3011,6 +3195,17 @@ async function main() {
|
|
|
3011
3195
|
console.log(theme.system(formatDiff(out, { maxLines: 400 }, t)));
|
|
3012
3196
|
continue;
|
|
3013
3197
|
}
|
|
3198
|
+
if (lower === '/diffstat') {
|
|
3199
|
+
const { runGit } = await import('./gitTools.js');
|
|
3200
|
+
const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
|
|
3201
|
+
if (probe.trim() !== 'true') {
|
|
3202
|
+
console.error(theme.error(t('diff.not_repo')));
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
const out = await runGit('git diff --stat', currentWorkdir, 20_000);
|
|
3206
|
+
console.log(theme.system(formatDiffStat(out, { maxLines: 60 }, t)));
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3014
3209
|
if (lower === '/cost' || lower === '/usage') {
|
|
3015
3210
|
let stats = summarizeTranscript([]);
|
|
3016
3211
|
if (transcript.file) {
|
|
@@ -3143,6 +3338,58 @@ async function main() {
|
|
|
3143
3338
|
}
|
|
3144
3339
|
continue;
|
|
3145
3340
|
}
|
|
3341
|
+
if (lower === '/improve' || lower.startsWith('/improve ')) {
|
|
3342
|
+
// Backlog-driven self-improvement: pick the next open BACKLOG item (or a
|
|
3343
|
+
// specific id) and run the standard task loop on it. The agent reads
|
|
3344
|
+
// BACKLOG.md itself for the details, so a FRESH agent needs no external
|
|
3345
|
+
// context — this is the reproducible hand-off path.
|
|
3346
|
+
const arg = trimmed.slice('/improve'.length).trim();
|
|
3347
|
+
let backlogText;
|
|
3348
|
+
try {
|
|
3349
|
+
backlogText = await fs.readFile(path.join(currentWorkdir, 'BACKLOG.md'), 'utf-8');
|
|
3350
|
+
}
|
|
3351
|
+
catch {
|
|
3352
|
+
console.error(theme.warn(t('improve.no_backlog')));
|
|
3353
|
+
continue;
|
|
3354
|
+
}
|
|
3355
|
+
const items = parseBacklogItems(backlogText);
|
|
3356
|
+
const item = nextBacklogItem(items, arg || undefined);
|
|
3357
|
+
if (!item) {
|
|
3358
|
+
console.log(theme.dim(arg ? t('improve.not_found', { v: arg }) : t('improve.all_done')));
|
|
3359
|
+
continue;
|
|
3360
|
+
}
|
|
3361
|
+
console.log(theme.system(t('improve.start', { id: item.id, title: item.title })));
|
|
3362
|
+
const improveTools = mod.createTools(currentWorkdir, {
|
|
3363
|
+
undo,
|
|
3364
|
+
todos: todoStore,
|
|
3365
|
+
readOnly: planMode,
|
|
3366
|
+
});
|
|
3367
|
+
if (mcpPool)
|
|
3368
|
+
improveTools.push(...mcpPool.tools);
|
|
3369
|
+
if (editor)
|
|
3370
|
+
editor.busy = true;
|
|
3371
|
+
try {
|
|
3372
|
+
await runTask(browser, improveTools, buildImprovePrompt(item), currentWorkdir, {
|
|
3373
|
+
transcript,
|
|
3374
|
+
freshChat: false,
|
|
3375
|
+
sendSystemPrompt: false,
|
|
3376
|
+
queue: pendingQueue,
|
|
3377
|
+
ui: editor || null,
|
|
3378
|
+
onChatReady: (chatId) => {
|
|
3379
|
+
if (chatId) {
|
|
3380
|
+
currentChatId = chatId;
|
|
3381
|
+
saveLastChat(chatId, currentWorkdir);
|
|
3382
|
+
}
|
|
3383
|
+
},
|
|
3384
|
+
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
finally {
|
|
3388
|
+
if (editor)
|
|
3389
|
+
editor.busy = false;
|
|
3390
|
+
}
|
|
3391
|
+
continue;
|
|
3392
|
+
}
|
|
3146
3393
|
if (lower === '/review' || lower.startsWith('/review ')) {
|
|
3147
3394
|
const rest = trimmed.slice('/review'.length).trim();
|
|
3148
3395
|
const staged = rest.indexOf('--staged') !== -1;
|
|
@@ -3182,7 +3429,15 @@ async function main() {
|
|
|
3182
3429
|
transcript.log('undo', { path: result.record.originalPath });
|
|
3183
3430
|
}
|
|
3184
3431
|
else {
|
|
3185
|
-
|
|
3432
|
+
// undo.ts returns machine codes ('empty'/'disabled') — localize here so
|
|
3433
|
+
// an English UI never shows a Russian reason.
|
|
3434
|
+
const reasonKey = result.reason === 'empty'
|
|
3435
|
+
? 'undo.reason_empty'
|
|
3436
|
+
: result.reason === 'disabled'
|
|
3437
|
+
? 'undo.reason_disabled'
|
|
3438
|
+
: null;
|
|
3439
|
+
const reason = reasonKey ? t(reasonKey) : String(result.reason ?? '');
|
|
3440
|
+
console.error(theme.error(t('undo.failed', { v: reason })));
|
|
3186
3441
|
}
|
|
3187
3442
|
continue;
|
|
3188
3443
|
}
|
|
@@ -3266,6 +3521,7 @@ async function main() {
|
|
|
3266
3521
|
}
|
|
3267
3522
|
// ---- Regular task (including in review mode) ----
|
|
3268
3523
|
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
3524
|
+
lastTaskText = taskText;
|
|
3269
3525
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3270
3526
|
await autoReload();
|
|
3271
3527
|
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
@@ -3275,7 +3531,11 @@ async function main() {
|
|
|
3275
3531
|
if (editor)
|
|
3276
3532
|
editor.setCommands(buildSlashCommands());
|
|
3277
3533
|
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
3278
|
-
const tools = mod.createTools(currentWorkdir, {
|
|
3534
|
+
const tools = mod.createTools(currentWorkdir, {
|
|
3535
|
+
undo,
|
|
3536
|
+
todos: todoStore,
|
|
3537
|
+
readOnly: planMode,
|
|
3538
|
+
});
|
|
3279
3539
|
if (mcpPool)
|
|
3280
3540
|
tools.push(...mcpPool.tools);
|
|
3281
3541
|
if (editor)
|
|
@@ -3330,6 +3590,7 @@ async function main() {
|
|
|
3330
3590
|
contextLimit: config.ui.contextLimit,
|
|
3331
3591
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3332
3592
|
goal: sessionGoal,
|
|
3593
|
+
todosQuery: () => todosSummary(),
|
|
3333
3594
|
}, inputAttachments);
|
|
3334
3595
|
}
|
|
3335
3596
|
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
|
}
|