zames_pro 2.61.0 → 2.63.1
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 +83 -2
- package/README.md +62 -13
- package/dist/agent-loop.js +54 -2
- package/dist/checkpoint.js +197 -0
- package/dist/commands.js +135 -0
- package/dist/config.js +20 -0
- package/dist/context.js +88 -1
- package/dist/i18n.js +89 -0
- package/dist/index.js +232 -12
- package/dist/input.js +10 -3
- package/dist/path-token.js +75 -0
- package/dist/permissions.js +98 -0
- package/dist/spinner.js +4 -1
- package/dist/system-prompt.js +15 -0
- package/logo-small.jpg +0 -0
- package/package.json +3 -2
- package/scripts/coverage-gate.mjs +2 -0
package/dist/index.js
CHANGED
|
@@ -17,11 +17,13 @@ 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
|
+
import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
|
|
20
21
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
21
|
-
import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, isDevOnlyCommand, parseBacklogItems, nextBacklogItem, buildImprovePrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
22
|
+
import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, isDevOnlyCommand, parseBacklogItems, nextBacklogItem, buildImprovePrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, expandCommandArgs, missingCommandArgs, } from './commands.js';
|
|
22
23
|
import { performCompact } from './compact.js';
|
|
23
24
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
24
25
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
26
|
+
import { extractAtFileRefs } from './path-token.js';
|
|
25
27
|
import { closeWeb } from './web.js';
|
|
26
28
|
import { assertCommandInsideRoot, runShell } from './shell.js';
|
|
27
29
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
@@ -188,6 +190,7 @@ const RELOADABLE = [
|
|
|
188
190
|
'self-review',
|
|
189
191
|
'diff',
|
|
190
192
|
'undo',
|
|
193
|
+
'checkpoint',
|
|
191
194
|
'transcript',
|
|
192
195
|
'spinner',
|
|
193
196
|
'config',
|
|
@@ -431,17 +434,20 @@ ${theme.bold(t('help.sec.files'))}
|
|
|
431
434
|
${t('help.cmd.transcript')}
|
|
432
435
|
${t('help.cmd.undo')}
|
|
433
436
|
${t('help.cmd.undo_list')}
|
|
437
|
+
${t('help.cmd.rewind')}
|
|
438
|
+
${t('help.cmd.rewind_list')}
|
|
434
439
|
${t('help.cmd.export')}
|
|
435
440
|
${t('help.cmd.cost')}
|
|
436
441
|
${t('help.cmd.copy')}
|
|
437
442
|
${t('help.cmd.doctor')}
|
|
438
443
|
${t('help.cmd.add_dir')}
|
|
439
444
|
${t('help.cmd.review')}
|
|
440
|
-
${devHelp}${dynamicCommands.length ? '\n' + theme.bold(t('help.skills')) + '\n' + dynamicCommands.map((d) => ' ' + d.name.padEnd(24) + ' ' + d.description).join('\n') : ''}
|
|
445
|
+
${devHelp}${dynamicCommands.length ? '\n' + theme.bold(t('help.skills')) + '\n' + dynamicCommands.map((d) => ' ' + (d.hint ? d.name + ' ' + d.hint : d.name).padEnd(24) + ' ' + d.description).join('\n') : ''}
|
|
441
446
|
|
|
442
447
|
${theme.bold(t('help.files'))}
|
|
443
448
|
${t('help.files.logs')} ${config.transcript.dir}
|
|
444
449
|
${t('help.files.undo')} ~/.zames/undo
|
|
450
|
+
${t('help.files.checkpoints')} ~/.zames/checkpoints
|
|
445
451
|
${t('help.files.sessions')} ${sessionsDir()}
|
|
446
452
|
${t('help.files.profile')} ~/.zames/profile
|
|
447
453
|
${t('help.files.snapshots')} ~/.zames/snapshots
|
|
@@ -484,6 +490,8 @@ const SLASH_COMMANDS = [
|
|
|
484
490
|
{ name: '/reload', key: 'help.cmd.reload' },
|
|
485
491
|
{ name: '/undo', key: 'help.cmd.undo' },
|
|
486
492
|
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
493
|
+
{ name: '/rewind', key: 'help.cmd.rewind' },
|
|
494
|
+
{ name: '/rewind-list', key: 'help.cmd.rewind_list' },
|
|
487
495
|
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
488
496
|
{ name: '/diff', key: 'help.cmd.diff' },
|
|
489
497
|
{ name: '/diffstat', key: 'help.cmd.diffstat' },
|
|
@@ -539,9 +547,12 @@ async function refreshDynamicCommands(workdir) {
|
|
|
539
547
|
}
|
|
540
548
|
const cmds = await loadCommands(workdir);
|
|
541
549
|
for (const c of cmds) {
|
|
550
|
+
// B7: carry the declared argument hint as a separate field so the hint is
|
|
551
|
+
// SHOWN in the suggest list but never inserted into the input line.
|
|
542
552
|
out.push({
|
|
543
553
|
name: '/' + c.name,
|
|
544
554
|
description: c.description || t('help.cmd.custom'),
|
|
555
|
+
hint: c.argumentHint,
|
|
545
556
|
});
|
|
546
557
|
}
|
|
547
558
|
}
|
|
@@ -551,16 +562,22 @@ async function refreshDynamicCommands(workdir) {
|
|
|
551
562
|
dynamicCommands = out;
|
|
552
563
|
}
|
|
553
564
|
// Expand a slash target (skill or custom command) into a task string.
|
|
554
|
-
// Returns null when the name is neither a skill nor a custom command.
|
|
565
|
+
// Returns null when the name is neither a skill nor a custom command. When a
|
|
566
|
+
// custom command declares mandatory `arguments:` and some are missing, returns
|
|
567
|
+
// `{ missing }` so the caller can print the usage hint instead of sending an
|
|
568
|
+
// incomplete prompt.
|
|
555
569
|
async function expandSlashTarget(workdir, name, rest) {
|
|
556
570
|
try {
|
|
557
571
|
const { loadSkills, loadCommands, skillBody } = await import('./context.js');
|
|
558
572
|
const commands = await loadCommands(workdir);
|
|
559
573
|
const cmd = commands.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
560
574
|
if (cmd) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
575
|
+
// B7: refuse to run when declared positional arguments are missing.
|
|
576
|
+
const missing = missingCommandArgs(cmd.arguments, rest);
|
|
577
|
+
if (missing.length)
|
|
578
|
+
return { missing };
|
|
579
|
+
const body = expandCommandArgs(cmd.body, rest, cmd.arguments);
|
|
580
|
+
return { text: body || rest };
|
|
564
581
|
}
|
|
565
582
|
const skills = await loadSkills(workdir);
|
|
566
583
|
const skill = skills.find((s) => s.name.toLowerCase() === name.toLowerCase() && s.userInvokable);
|
|
@@ -583,7 +600,7 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
583
600
|
const extra = rest
|
|
584
601
|
? '\n\nAdditional instructions from the operator: ' + rest
|
|
585
602
|
: '';
|
|
586
|
-
return header + '\n\n' + body + extra;
|
|
603
|
+
return { text: header + '\n\n' + body + extra };
|
|
587
604
|
}
|
|
588
605
|
}
|
|
589
606
|
catch {
|
|
@@ -643,6 +660,55 @@ async function resolveAttachPath(workdir, raw) {
|
|
|
643
660
|
}
|
|
644
661
|
return null;
|
|
645
662
|
}
|
|
663
|
+
// Per-file and total caps for `@path` inlining (B5). A reference should SAVE
|
|
664
|
+
// round-trips, not flood the context: a huge file is truncated with a note
|
|
665
|
+
// telling the agent to read the rest itself.
|
|
666
|
+
const AT_REF_FILE_CAP = 60 * 1024;
|
|
667
|
+
const AT_REF_TOTAL_CAP = 200 * 1024;
|
|
668
|
+
// Inline `@path` references found in a task text (B5). Returns the augmented
|
|
669
|
+
// text plus the list of files actually inlined (for the operator notice).
|
|
670
|
+
// Unknown/unreadable references are left in place untouched — they may be part
|
|
671
|
+
// of the prose; we never fail the task over a missing file.
|
|
672
|
+
async function inlineAtRefs(workdir, text) {
|
|
673
|
+
const refs = extractAtFileRefs(text);
|
|
674
|
+
if (!refs.length)
|
|
675
|
+
return { text, inlined: [] };
|
|
676
|
+
const blocks = [];
|
|
677
|
+
const inlined = [];
|
|
678
|
+
let total = 0;
|
|
679
|
+
for (const ref of refs) {
|
|
680
|
+
if (total >= AT_REF_TOTAL_CAP)
|
|
681
|
+
break;
|
|
682
|
+
const file = await resolveAttachPath(workdir, ref);
|
|
683
|
+
if (!file)
|
|
684
|
+
continue;
|
|
685
|
+
let raw;
|
|
686
|
+
try {
|
|
687
|
+
raw = await fs.readFile(file, 'utf-8');
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
let body = raw;
|
|
693
|
+
let note = '';
|
|
694
|
+
if (body.length > AT_REF_FILE_CAP) {
|
|
695
|
+
body = body.slice(0, AT_REF_FILE_CAP);
|
|
696
|
+
note = NL + NL + '(truncated at ' + AT_REF_FILE_CAP + ' chars)';
|
|
697
|
+
}
|
|
698
|
+
total += body.length;
|
|
699
|
+
inlined.push(path.relative(workdir, file) || file);
|
|
700
|
+
blocks.push('### ' + ref + NL + '```' + NL + body + NL + '```' + note);
|
|
701
|
+
}
|
|
702
|
+
if (!blocks.length)
|
|
703
|
+
return { text, inlined: [] };
|
|
704
|
+
const section = NL +
|
|
705
|
+
NL +
|
|
706
|
+
'## Files referenced with @ in the task' +
|
|
707
|
+
NL +
|
|
708
|
+
NL +
|
|
709
|
+
blocks.join(NL + NL);
|
|
710
|
+
return { text: text + section, inlined };
|
|
711
|
+
}
|
|
646
712
|
async function cleanTmpDir() {
|
|
647
713
|
try {
|
|
648
714
|
await fs.rm(TMP_DIR, { recursive: true, force: true });
|
|
@@ -1101,6 +1167,27 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
1101
1167
|
}
|
|
1102
1168
|
}
|
|
1103
1169
|
// ---------- task runner ----------
|
|
1170
|
+
// Ask the operator to approve a tool call flagged by an `ask` permission rule
|
|
1171
|
+
// (C1). While the LineEditor owns the terminal it must be paused first, or the
|
|
1172
|
+
// confirmation prompt and the input line fight for the same keys. A non-TTY run
|
|
1173
|
+
// has no way to answer, so it DENIES (never silently allows a guarded call).
|
|
1174
|
+
async function askOperatorConfirm(editor, question) {
|
|
1175
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
1176
|
+
return false;
|
|
1177
|
+
if (editor)
|
|
1178
|
+
editor.pause();
|
|
1179
|
+
try {
|
|
1180
|
+
const a = await promptOnce(question + ' [y/N] ');
|
|
1181
|
+
return /^(y|yes|д|да)$/i.test(a.trim());
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
finally {
|
|
1187
|
+
if (editor)
|
|
1188
|
+
editor.resume();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1104
1191
|
async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
|
|
1105
1192
|
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, selfImprovement = false, } = opts;
|
|
1106
1193
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
@@ -1198,6 +1285,19 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1198
1285
|
ui.assistant(msg);
|
|
1199
1286
|
},
|
|
1200
1287
|
onWarning: (msg) => ui.warning(msg),
|
|
1288
|
+
onAskPermission: async (info) => {
|
|
1289
|
+
// C1: a rule with action "ask". Show the reason and wait for the
|
|
1290
|
+
// operator. In non-TTY this denies (see askOperatorConfirm).
|
|
1291
|
+
ui.warning(t('perm.ask', { tool: info.tool, reason: info.reason }));
|
|
1292
|
+
const ok = await askOperatorConfirm(editor || null, t('perm.confirm'));
|
|
1293
|
+
ui.warning(ok ? t('perm.allowed') : t('perm.denied'));
|
|
1294
|
+
transcript?.log('permission_ask', {
|
|
1295
|
+
tool: info.tool,
|
|
1296
|
+
reason: info.reason,
|
|
1297
|
+
allowed: ok,
|
|
1298
|
+
});
|
|
1299
|
+
return ok;
|
|
1300
|
+
},
|
|
1201
1301
|
onChatReady,
|
|
1202
1302
|
debugLog: debug,
|
|
1203
1303
|
locale: currentLocale,
|
|
@@ -1355,6 +1455,7 @@ async function main() {
|
|
|
1355
1455
|
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
1356
1456
|
}
|
|
1357
1457
|
const undo = new UndoStore(config.undo);
|
|
1458
|
+
const checkpoints = new CheckpointStore(config.checkpoint);
|
|
1358
1459
|
// MCP servers: optional external tool providers (e.g. @playwright/mcp).
|
|
1359
1460
|
let mcpPool = null;
|
|
1360
1461
|
try {
|
|
@@ -3142,6 +3243,9 @@ async function main() {
|
|
|
3142
3243
|
console.log(theme.system(t('status.undo', {
|
|
3143
3244
|
v: config.undo.enabled ? t('common.on') : t('common.off'),
|
|
3144
3245
|
})));
|
|
3246
|
+
console.log(theme.system(t('status.checkpoint', {
|
|
3247
|
+
v: config.checkpoint.enabled ? t('common.on') : t('common.off'),
|
|
3248
|
+
})));
|
|
3145
3249
|
console.log(theme.system(t('status.transcript', {
|
|
3146
3250
|
v: transcript.file || t('common.off'),
|
|
3147
3251
|
})));
|
|
@@ -3597,6 +3701,86 @@ async function main() {
|
|
|
3597
3701
|
}
|
|
3598
3702
|
continue;
|
|
3599
3703
|
}
|
|
3704
|
+
if (lower === '/rewind-list' || lower === '/checkpoints') {
|
|
3705
|
+
const list = await checkpoints.list(10);
|
|
3706
|
+
if (!list.length) {
|
|
3707
|
+
console.log(theme.system(t('rewind.empty')));
|
|
3708
|
+
}
|
|
3709
|
+
else {
|
|
3710
|
+
console.log(theme.system(t('rewind.list_title')));
|
|
3711
|
+
list.forEach((r, i) => {
|
|
3712
|
+
const stamp = new Date(r.stamp).toLocaleString();
|
|
3713
|
+
const label = r.label ? ' [' + t('rewind.label_backup') + ']' : '';
|
|
3714
|
+
console.log(theme.system(` ${i + 1}. ${stamp} ${r.workdir}${label}`));
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
if (lower === '/rewind' || lower.startsWith('/rewind ')) {
|
|
3720
|
+
const selector = trimmed.slice('/rewind'.length).trim() || '1';
|
|
3721
|
+
if (editor)
|
|
3722
|
+
editor.lock(t('msg.input_locked'));
|
|
3723
|
+
try {
|
|
3724
|
+
const list = await checkpoints.list(10);
|
|
3725
|
+
const target = pickCheckpoint(list, selector);
|
|
3726
|
+
if (!target) {
|
|
3727
|
+
console.error(theme.error(t('rewind.failed', { v: selector })));
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
// Confirmation: a rewind overwrites the working tree, so it must never
|
|
3731
|
+
// happen on a muscle-memory Enter.
|
|
3732
|
+
const stamp = new Date(target.stamp).toLocaleString();
|
|
3733
|
+
let answer = 'n';
|
|
3734
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3735
|
+
// promptOnce reads stdin itself. While the LineEditor owns the
|
|
3736
|
+
// terminal we must hand it over first (the same pause/resume the
|
|
3737
|
+
// /config menu uses), or both readers fight for the same keys.
|
|
3738
|
+
if (editor)
|
|
3739
|
+
editor.pause();
|
|
3740
|
+
try {
|
|
3741
|
+
answer = await promptOnce(theme.warn(t('rewind.confirm', { v: stamp })) + ' [y/N] ');
|
|
3742
|
+
}
|
|
3743
|
+
finally {
|
|
3744
|
+
if (editor)
|
|
3745
|
+
editor.resume();
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
else if (trimmed.indexOf('--yes') !== -1) {
|
|
3749
|
+
// Non-TTY (piped): do not guess. Require an explicit --yes.
|
|
3750
|
+
answer = 'y';
|
|
3751
|
+
}
|
|
3752
|
+
if (!/^(y|yes|д|да)$/i.test(answer.trim())) {
|
|
3753
|
+
console.log(theme.system(t('rewind.cancelled')));
|
|
3754
|
+
continue;
|
|
3755
|
+
}
|
|
3756
|
+
const res = await checkpoints.restore(selector);
|
|
3757
|
+
if (res.ok && res.record) {
|
|
3758
|
+
console.log(theme.assistant(t('rewind.reverted', { v: String(res.record.stamp) })));
|
|
3759
|
+
if (res.backup) {
|
|
3760
|
+
console.log(theme.system(t('rewind.backup', { v: String(res.backup.stamp) })));
|
|
3761
|
+
}
|
|
3762
|
+
transcript.log('rewind', { stamp: res.record.stamp });
|
|
3763
|
+
}
|
|
3764
|
+
else {
|
|
3765
|
+
const reasonKey = res.reason === 'not_found'
|
|
3766
|
+
? 'rewind.reason_not_found'
|
|
3767
|
+
: res.reason === 'archive_missing'
|
|
3768
|
+
? 'rewind.reason_archive_missing'
|
|
3769
|
+
: res.reason === 'extract_failed'
|
|
3770
|
+
? 'rewind.reason_extract_failed'
|
|
3771
|
+
: res.reason === 'disabled'
|
|
3772
|
+
? 'rewind.reason_disabled'
|
|
3773
|
+
: null;
|
|
3774
|
+
const reason = reasonKey ? t(reasonKey) : String(res.reason ?? '');
|
|
3775
|
+
console.error(theme.error(t('rewind.failed', { v: reason })));
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
finally {
|
|
3779
|
+
if (editor)
|
|
3780
|
+
editor.unlock();
|
|
3781
|
+
}
|
|
3782
|
+
continue;
|
|
3783
|
+
}
|
|
3600
3784
|
if (lower === '/debug-dom') {
|
|
3601
3785
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3602
3786
|
const file = path.join(config.transcript.dir, `dom-${stamp}.html`);
|
|
@@ -3652,10 +3836,21 @@ async function main() {
|
|
|
3652
3836
|
// task. Skills are instructions the agent follows; custom commands are
|
|
3653
3837
|
// prompt templates with $ARGUMENTS / {{args}} placeholders.
|
|
3654
3838
|
let expandedTask = null;
|
|
3839
|
+
let missingArgs = null;
|
|
3655
3840
|
if (lower.startsWith('/')) {
|
|
3656
3841
|
const name = trimmed.slice(1).split(/\s+/)[0];
|
|
3657
3842
|
const rest = trimmed.slice(1 + name.length).trim();
|
|
3658
|
-
|
|
3843
|
+
const res = await expandSlashTarget(currentWorkdir, name, rest);
|
|
3844
|
+
if (res && 'missing' in res)
|
|
3845
|
+
missingArgs = res.missing;
|
|
3846
|
+
else if (res)
|
|
3847
|
+
expandedTask = res.text;
|
|
3848
|
+
}
|
|
3849
|
+
// B7: a custom command with mandatory `arguments:` refuses to run until the
|
|
3850
|
+
// operator supplies them (by position).
|
|
3851
|
+
if (missingArgs) {
|
|
3852
|
+
console.error(theme.error(t('msg.missing_args', { v: missingArgs.join(', ') })));
|
|
3853
|
+
continue;
|
|
3659
3854
|
}
|
|
3660
3855
|
if (lower.startsWith('/') && expandedTask === null) {
|
|
3661
3856
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
@@ -3663,7 +3858,20 @@ async function main() {
|
|
|
3663
3858
|
}
|
|
3664
3859
|
// ---- Regular task (including in review mode) ----
|
|
3665
3860
|
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
3666
|
-
|
|
3861
|
+
// B5: inline any `@path` references into the task so the agent does not
|
|
3862
|
+
// spend a round-trip reading a file the operator already pointed at. Only
|
|
3863
|
+
// existing files are inlined; a bare `@name` in prose is left untouched.
|
|
3864
|
+
let finalTaskText = taskText;
|
|
3865
|
+
{
|
|
3866
|
+
const refRes = await inlineAtRefs(currentWorkdir, taskText);
|
|
3867
|
+
if (refRes.inlined.length) {
|
|
3868
|
+
finalTaskText = refRes.text;
|
|
3869
|
+
console.log(theme.system(t('msg.at_refs', { n: refRes.inlined.length }) +
|
|
3870
|
+
' ' +
|
|
3871
|
+
refRes.inlined.join(', ')));
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
lastTaskText = finalTaskText;
|
|
3667
3875
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3668
3876
|
await autoReload();
|
|
3669
3877
|
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
@@ -3672,7 +3880,19 @@ async function main() {
|
|
|
3672
3880
|
await refreshDynamicCommands(currentWorkdir);
|
|
3673
3881
|
if (editor)
|
|
3674
3882
|
editor.setCommands(buildSlashCommands());
|
|
3675
|
-
transcript.log('user_task', {
|
|
3883
|
+
transcript.log('user_task', {
|
|
3884
|
+
task: finalTaskText,
|
|
3885
|
+
workdir: currentWorkdir,
|
|
3886
|
+
});
|
|
3887
|
+
// BACKLOG B3: snapshot the working tree BEFORE the task so /rewind can
|
|
3888
|
+
// undo the whole task in one step. Best-effort: create() never throws, so
|
|
3889
|
+
// a missing tar cannot abort the task it was meant to protect. Skipped
|
|
3890
|
+
// while reviewing a snapshot (workdir is a throwaway copy).
|
|
3891
|
+
if (!reviewMode) {
|
|
3892
|
+
const cp = await checkpoints.create(currentWorkdir);
|
|
3893
|
+
if (cp)
|
|
3894
|
+
transcript.log('checkpoint', { stamp: cp.stamp });
|
|
3895
|
+
}
|
|
3676
3896
|
const tools = mod.createTools(currentWorkdir, {
|
|
3677
3897
|
undo,
|
|
3678
3898
|
todos: todoStore,
|
|
@@ -3683,7 +3903,7 @@ async function main() {
|
|
|
3683
3903
|
if (editor)
|
|
3684
3904
|
editor.busy = true;
|
|
3685
3905
|
try {
|
|
3686
|
-
await runTask(browser, tools,
|
|
3906
|
+
await runTask(browser, tools, finalTaskText, currentWorkdir, {
|
|
3687
3907
|
transcript,
|
|
3688
3908
|
freshChat: freshChatNext,
|
|
3689
3909
|
sendSystemPrompt: sendSystemPromptNext,
|
|
@@ -3709,7 +3929,7 @@ async function main() {
|
|
|
3709
3929
|
currentChatId,
|
|
3710
3930
|
workdir: currentWorkdir,
|
|
3711
3931
|
locale: currentLocale,
|
|
3712
|
-
task:
|
|
3932
|
+
task: finalTaskText,
|
|
3713
3933
|
transcript,
|
|
3714
3934
|
ui: editor,
|
|
3715
3935
|
buildSystemPrompt: mod.buildSystemPrompt,
|
package/dist/input.js
CHANGED
|
@@ -692,10 +692,11 @@ export class LineEditor {
|
|
|
692
692
|
const shown = sugg.slice(off, off + pageSize);
|
|
693
693
|
this._suggestOffset = off;
|
|
694
694
|
this._suggestCount = sugg.length;
|
|
695
|
-
const
|
|
695
|
+
const label = (c) => c.hint ? c.name + ' ' + c.hint : c.name;
|
|
696
|
+
const maxName = Math.max(...shown.map((c) => label(c).length));
|
|
696
697
|
const lines = shown.map((c, i) => {
|
|
697
698
|
const selected = off + i === this._suggestSelected && this._suggestSelected > 0;
|
|
698
|
-
const name = theme.prompt(c.
|
|
699
|
+
const name = theme.prompt(label(c).padEnd(maxName));
|
|
699
700
|
const desc = theme.dim(' ' + c.description);
|
|
700
701
|
return (selected ? theme.prompt(' ▸ ') : ' ') + name + desc;
|
|
701
702
|
});
|
|
@@ -1266,7 +1267,13 @@ export class LineEditor {
|
|
|
1266
1267
|
const label = translate(this.locale)('spinner.pause', { n: seconds });
|
|
1267
1268
|
if (this._animating && this._dotTimer) {
|
|
1268
1269
|
this._thinkBase = theme.brown(stripEllipsis(label));
|
|
1269
|
-
|
|
1270
|
+
// Keep the SAME tail as the dot timer renders (elapsed + hint). Omitting
|
|
1271
|
+
// the elapsed label here made it blink once per second: the timer showed
|
|
1272
|
+
// "· 1m 50s" and the per-second sendPause update erased it.
|
|
1273
|
+
this.setStatus(this._thinkBase +
|
|
1274
|
+
this._dots(this._dotPhase) +
|
|
1275
|
+
this._elapsedLabel() +
|
|
1276
|
+
this._hint());
|
|
1270
1277
|
return;
|
|
1271
1278
|
}
|
|
1272
1279
|
this._startAnimated(label);
|
package/dist/path-token.js
CHANGED
|
@@ -51,6 +51,81 @@ function stripPunct(tok) {
|
|
|
51
51
|
t = t.slice(1);
|
|
52
52
|
return t;
|
|
53
53
|
}
|
|
54
|
+
// Characters that may directly precede an '@' for it to count as a file
|
|
55
|
+
// reference rather than a decorator/email. Without this guard `user@host` and
|
|
56
|
+
// `@Component` would be misread as paths.
|
|
57
|
+
function isRefBoundary(ch) {
|
|
58
|
+
return (ch === ' ' ||
|
|
59
|
+
ch === '\t' ||
|
|
60
|
+
ch === '\n' ||
|
|
61
|
+
ch === '\r' ||
|
|
62
|
+
ch === '(' ||
|
|
63
|
+
ch === '[' ||
|
|
64
|
+
ch === '{' ||
|
|
65
|
+
ch === String.fromCharCode(34) ||
|
|
66
|
+
ch === String.fromCharCode(39) ||
|
|
67
|
+
ch === '<');
|
|
68
|
+
}
|
|
69
|
+
// End of a reference token: whitespace, a closing bracket/quote, or an
|
|
70
|
+
// angle bracket (so `@a.ts>` stops at `>`).
|
|
71
|
+
function isRefEnd(ch) {
|
|
72
|
+
return (ch === ' ' ||
|
|
73
|
+
ch === '\t' ||
|
|
74
|
+
ch === '\n' ||
|
|
75
|
+
ch === '\r' ||
|
|
76
|
+
ch === ')' ||
|
|
77
|
+
ch === ']' ||
|
|
78
|
+
ch === '}' ||
|
|
79
|
+
ch === String.fromCharCode(34) ||
|
|
80
|
+
ch === String.fromCharCode(39) ||
|
|
81
|
+
ch === '<' ||
|
|
82
|
+
ch === '>');
|
|
83
|
+
}
|
|
84
|
+
// Strip trailing punctuation a path may have picked up from prose, e.g.
|
|
85
|
+
// "see @src/a.ts, please" -> `src/a.ts`.
|
|
86
|
+
function stripRefPunct(tok) {
|
|
87
|
+
let t = tok;
|
|
88
|
+
while (t.length && ',.;:!?'.includes(t[t.length - 1]))
|
|
89
|
+
t = t.slice(0, -1);
|
|
90
|
+
return t;
|
|
91
|
+
}
|
|
92
|
+
// Find all `@path` references in a task text (B5). Returns the unique paths in
|
|
93
|
+
// order of appearance. A reference is an '@' at a word boundary followed by a
|
|
94
|
+
// token that looks like a file name/path (a known extension). Implemented
|
|
95
|
+
// without regular expressions, like extractPathToken, to keep the source free
|
|
96
|
+
// of escaping pitfalls.
|
|
97
|
+
export function extractAtFileRefs(text) {
|
|
98
|
+
const s = String(text || '');
|
|
99
|
+
const chars = Array.from(s);
|
|
100
|
+
const n = chars.length;
|
|
101
|
+
const out = [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
let i = 0;
|
|
104
|
+
while (i < n) {
|
|
105
|
+
if (chars[i] !== '@') {
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const prev = i > 0 ? chars[i - 1] : '';
|
|
110
|
+
if (prev && !isRefBoundary(prev)) {
|
|
111
|
+
i++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
let j = i + 1;
|
|
115
|
+
let tok = '';
|
|
116
|
+
while (j < n && !isRefEnd(chars[j])) {
|
|
117
|
+
tok += chars[j];
|
|
118
|
+
j++;
|
|
119
|
+
}
|
|
120
|
+
const cleaned = stripRefPunct(tok);
|
|
121
|
+
if (cleaned && looksLikeToken(cleaned) && !seen.has(cleaned)) {
|
|
122
|
+
seen.add(cleaned);
|
|
123
|
+
out.push(cleaned);
|
|
124
|
+
}
|
|
125
|
+
i = j > i + 1 ? j : i + 1;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
54
129
|
function looksLikeToken(tok) {
|
|
55
130
|
const t = String(tok || '');
|
|
56
131
|
if (!t || t.length > 500)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function permissionsPath(workdir) {
|
|
4
|
+
return path.join(workdir, '.zames', 'permissions.json');
|
|
5
|
+
}
|
|
6
|
+
const ACTIONS = ['allow', 'deny', 'ask'];
|
|
7
|
+
function normAltion(v) {
|
|
8
|
+
if (typeof v !== 'string')
|
|
9
|
+
return null;
|
|
10
|
+
const s = v.toLowerCase();
|
|
11
|
+
return ACTIONS.includes(s) ? s : null;
|
|
12
|
+
}
|
|
13
|
+
/** Parse the JSON of a permissions file into a policy, or null when empty/broken. */
|
|
14
|
+
export function parsePermissions(raw) {
|
|
15
|
+
if (!raw || typeof raw !== 'object')
|
|
16
|
+
return null;
|
|
17
|
+
const obj = raw;
|
|
18
|
+
const defAction = normAltion(obj['default']) ?? 'allow';
|
|
19
|
+
const rules = Array.isArray(obj['rules']) ? obj['rules'] : [];
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const item of rules) {
|
|
22
|
+
if (!item || typeof item !== 'object')
|
|
23
|
+
continue;
|
|
24
|
+
const e = item;
|
|
25
|
+
const action = normAltion(e['action']);
|
|
26
|
+
if (!action)
|
|
27
|
+
continue;
|
|
28
|
+
out.push({
|
|
29
|
+
action,
|
|
30
|
+
tool: typeof e['tool'] === 'string' ? e['tool'] : undefined,
|
|
31
|
+
command: typeof e['command'] === 'string' ? e['command'] : undefined,
|
|
32
|
+
path: typeof e['path'] === 'string' ? e['path'] : undefined,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!out.length && defAction === 'allow')
|
|
36
|
+
return null;
|
|
37
|
+
return { default: defAction, rules: out };
|
|
38
|
+
}
|
|
39
|
+
/** Read `.zames/permissions.json`. Missing/broken => null (no policy). */
|
|
40
|
+
export function loadPermissions(workdir) {
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = fs.readFileSync(permissionsPath(workdir), 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return parsePermissions(JSON.parse(raw));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function safeRegex(source) {
|
|
56
|
+
try {
|
|
57
|
+
return new RegExp(source);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// A file path may arrive in any of these keys depending on the tool.
|
|
64
|
+
// We collect them all and match the path regex against ANY of them.
|
|
65
|
+
const PATH_KEYS = ['path', 'file_path', 'notebook_path', 'from', 'to'];
|
|
66
|
+
export function decidePermission(policy, tool, args) {
|
|
67
|
+
if (!policy)
|
|
68
|
+
return { action: 'allow', reason: 'no policy' };
|
|
69
|
+
const a = (args ?? {});
|
|
70
|
+
const command = typeof a['command'] === 'string' ? a['command'] : '';
|
|
71
|
+
const paths = PATH_KEYS.map((k) => a[k]).filter((v) => typeof v === 'string');
|
|
72
|
+
for (const r of policy.rules) {
|
|
73
|
+
if (r.tool) {
|
|
74
|
+
const re = safeRegex(r.tool);
|
|
75
|
+
if (!re || !re.test(tool))
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (r.command) {
|
|
79
|
+
const re = safeRegex(r.command);
|
|
80
|
+
if (!re || !command || !re.test(command))
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (r.path) {
|
|
84
|
+
const re = safeRegex(r.path);
|
|
85
|
+
if (!re || !paths.some((p) => re.test(p)))
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const desc = [
|
|
89
|
+
r.tool ? 'tool=' + r.tool : '',
|
|
90
|
+
r.command ? 'command=' + r.command : '',
|
|
91
|
+
r.path ? 'path=' + r.path : '',
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(' ');
|
|
95
|
+
return { action: r.action, reason: 'rule ' + desc };
|
|
96
|
+
}
|
|
97
|
+
return { action: policy.default, reason: 'default' };
|
|
98
|
+
}
|
package/dist/spinner.js
CHANGED
|
@@ -151,7 +151,10 @@ export function createSpinner(locale = 'ru') {
|
|
|
151
151
|
// so the countdown refreshes without resetting the dots.
|
|
152
152
|
if (animating && dotTimer && spinner) {
|
|
153
153
|
animBase = theme.brown(stripEllipsis(label));
|
|
154
|
-
|
|
154
|
+
// Same tail as the timer renders (elapsed + tasks + hint), or the
|
|
155
|
+
// per-second update blinks the elapsed label / task badge away.
|
|
156
|
+
spinner.text =
|
|
157
|
+
animBase + renderDots(dotPhase) + elapsedLabel() + tasksBadge() + HINT;
|
|
155
158
|
return;
|
|
156
159
|
}
|
|
157
160
|
startAnimated(label);
|
package/dist/system-prompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { translate } from './i18n.js';
|
|
2
3
|
// The dev-mode BACKLOG instruction. English (agent-facing) like the rest of the
|
|
3
4
|
// prompt. Deliberately narrow: one short note at most, and only when the idea
|
|
@@ -45,6 +46,20 @@ export function renderContextSection(context) {
|
|
|
45
46
|
f.content +
|
|
46
47
|
'\n';
|
|
47
48
|
}
|
|
49
|
+
// B6: nested AGENTS.md/MEMORY.md from the directories this task touches.
|
|
50
|
+
// Deliberately a SEPARATE section: these apply only to part of the tree and
|
|
51
|
+
// must not be mistaken for project-wide rules.
|
|
52
|
+
for (const f of context.scopedAgents || []) {
|
|
53
|
+
out +=
|
|
54
|
+
'\n## Scoped instructions (' +
|
|
55
|
+
f.path +
|
|
56
|
+
')\n\n' +
|
|
57
|
+
'These rules apply ONLY to files under ' +
|
|
58
|
+
path.dirname(f.path) +
|
|
59
|
+
' (the current task touches that directory). Follow them for those files, in addition to the project instructions above.\n\n' +
|
|
60
|
+
f.content +
|
|
61
|
+
'\n';
|
|
62
|
+
}
|
|
48
63
|
if (context.memory.length) {
|
|
49
64
|
out +=
|
|
50
65
|
'\nWhen you learn a durable fact about this project or the operator preferences (build quirks, ' +
|
package/logo-small.jpg
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.63.1",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright: reads and edits files, runs commands, commits to git.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"README.md",
|
|
13
13
|
"CHANGELOG.md",
|
|
14
14
|
"LICENSE",
|
|
15
|
-
"logo.jpg"
|
|
15
|
+
"logo.jpg",
|
|
16
|
+
"logo-small.jpg"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"start": "node ./dist/index.js",
|