zames_pro 2.29.16 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.js +138 -12
- package/dist/browser.js +422 -145
- package/dist/commands.js +69 -0
- package/dist/compact.js +178 -0
- package/dist/config-menu.js +22 -5
- package/dist/config.js +43 -0
- package/dist/confirm.js +30 -6
- package/dist/context.js +3 -2
- package/dist/gitTools.js +3 -2
- package/dist/i18n.js +94 -8
- package/dist/index.js +217 -174
- package/dist/input.js +86 -11
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { isImageName } from './attachments.js';
|
|
2
3
|
// Helpers for the extra slash commands (/diff, /cost, /export, /doctor,
|
|
3
4
|
// /permissions, /review, /add-dir). Pure functions, unit-tested without a
|
|
4
5
|
// live agent/browser. index.ts only renders their output.
|
|
@@ -405,6 +406,74 @@ export function buildCompactCarryover(summary, task) {
|
|
|
405
406
|
return out;
|
|
406
407
|
}
|
|
407
408
|
// ---------- /review ----------
|
|
409
|
+
// ---------- Ctrl+C escalation ----------
|
|
410
|
+
// Decide what a Ctrl+C does while the agent is busy. The FIRST press aborts
|
|
411
|
+
// the currently running TOOL; a SECOND press within the window stops the WHOLE
|
|
412
|
+
// run (including the queue). Outside the window it is a fresh first press.
|
|
413
|
+
// Returns 'tool' | 'run'.
|
|
414
|
+
export function ctrlCEscalation(sinceLastMs, windowMs = 2000) {
|
|
415
|
+
return sinceLastMs < windowMs ? 'run' : 'tool';
|
|
416
|
+
}
|
|
417
|
+
// True when the queued message is a slash-command. It must NOT be merged into
|
|
418
|
+
// a task: the main loop has to execute it (e.g. /compact, /new).
|
|
419
|
+
export function isSlashCommand(text) {
|
|
420
|
+
return String(text ?? '')
|
|
421
|
+
.trim()
|
|
422
|
+
.startsWith('/');
|
|
423
|
+
}
|
|
424
|
+
function queuedIsImage(a) {
|
|
425
|
+
return (String(a.mime || '').startsWith('image/') ||
|
|
426
|
+
isImageName(String(a.name || '')));
|
|
427
|
+
}
|
|
428
|
+
// Merge a batch of queued messages into a single task. Marker numbering is
|
|
429
|
+
// PER-MESSAGE ([image#1]/[file#1] restart in every message), so a naive
|
|
430
|
+
// concatenation would produce duplicate markers for different files. We
|
|
431
|
+
// RENUMBER the markers across the batch and rebuild the attachments array in
|
|
432
|
+
// the same order, keeping the text markers the source of truth.
|
|
433
|
+
export function mergeMessages(msgs) {
|
|
434
|
+
const list = (msgs || []).filter((m) => String(m?.text ?? '').trim() !== '');
|
|
435
|
+
if (list.length === 0)
|
|
436
|
+
return { text: '', attachments: [] };
|
|
437
|
+
if (list.length === 1) {
|
|
438
|
+
return {
|
|
439
|
+
text: list[0].text,
|
|
440
|
+
attachments: (list[0].attachments || []).slice(),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
let imgOffset = 0;
|
|
444
|
+
let fileOffset = 0;
|
|
445
|
+
const attachments = [];
|
|
446
|
+
const parts = [];
|
|
447
|
+
for (const m of list) {
|
|
448
|
+
const atts = m.attachments || [];
|
|
449
|
+
const images = atts.filter(queuedIsImage);
|
|
450
|
+
const files = atts.filter((a) => !queuedIsImage(a));
|
|
451
|
+
let text = String(m.text);
|
|
452
|
+
text = text.replace(/\[image#(\d+)\]/g, (whole, n) => {
|
|
453
|
+
const i = Number(n);
|
|
454
|
+
if (!Number.isFinite(i) || i < 1 || i > images.length)
|
|
455
|
+
return whole;
|
|
456
|
+
return '[image#' + (imgOffset + i) + ']';
|
|
457
|
+
});
|
|
458
|
+
text = text.replace(/\[file#(\d+)\]/g, (whole, n) => {
|
|
459
|
+
const i = Number(n);
|
|
460
|
+
if (!Number.isFinite(i) || i < 1 || i > files.length)
|
|
461
|
+
return whole;
|
|
462
|
+
return '[file#' + (fileOffset + i) + ']';
|
|
463
|
+
});
|
|
464
|
+
imgOffset += images.length;
|
|
465
|
+
fileOffset += files.length;
|
|
466
|
+
attachments.push(...atts);
|
|
467
|
+
parts.push(text.trim());
|
|
468
|
+
}
|
|
469
|
+
const header = 'The operator sent ' +
|
|
470
|
+
list.length +
|
|
471
|
+
' messages while you were working. They are listed below in order — ' +
|
|
472
|
+
'treat them as one instruction.';
|
|
473
|
+
const text = header + NL + NL + parts.join(NL + NL + '---' + NL + NL);
|
|
474
|
+
return { text, attachments };
|
|
475
|
+
}
|
|
476
|
+
// ---------- /review ----------
|
|
408
477
|
export function buildReviewPrompt(focus, hasStaged = false) {
|
|
409
478
|
const scope = hasStaged ? 'staged' : 'uncommitted';
|
|
410
479
|
const f = String(focus ?? '').trim();
|
package/dist/compact.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { translate } from './i18n.js';
|
|
2
|
+
import { buildCompactPrompt, buildCompactCarryover, isUsableCompactSummary, trimRestoredMessages, formatRestoredHistory, } from './commands.js';
|
|
3
|
+
const NL = String.fromCharCode(10);
|
|
4
|
+
/** How many times the summary call is retried before the local fallback. */
|
|
5
|
+
export const COMPACT_SUMMARY_ATTEMPTS = 3;
|
|
6
|
+
/** How many restored messages the local fallback keeps. */
|
|
7
|
+
export const COMPACT_FALLBACK_LIMIT = 40;
|
|
8
|
+
/**
|
|
9
|
+
* Compact the CURRENT chat: summarize it (retrying a few times, with a LOCAL
|
|
10
|
+
* history fallback), open a NEW chat, resend the system prompt and post the
|
|
11
|
+
* summary as the carried-over context. Shared by the manual `/compact` command
|
|
12
|
+
* and the automatic between-tools trigger, so both paths stay identical.
|
|
13
|
+
*
|
|
14
|
+
* The caller owns the browser and the throttle; this function only sends.
|
|
15
|
+
*/
|
|
16
|
+
export async function performCompact(opts) {
|
|
17
|
+
const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, } = opts;
|
|
18
|
+
const t = translate(locale);
|
|
19
|
+
const log = (msg) => {
|
|
20
|
+
if (!quiet)
|
|
21
|
+
console.error(msg);
|
|
22
|
+
};
|
|
23
|
+
let currentChatId = opts.currentChatId;
|
|
24
|
+
if (!currentChatId) {
|
|
25
|
+
currentChatId = await browser.getCurrentChatId();
|
|
26
|
+
}
|
|
27
|
+
if (!currentChatId) {
|
|
28
|
+
log(t('compact.no_chat'));
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
chatId: null,
|
|
32
|
+
summaryChars: 0,
|
|
33
|
+
beforeTokens: null,
|
|
34
|
+
error: t('compact.no_chat'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const beforeTokens = browser.getLastTokenUsage
|
|
38
|
+
? browser.getLastTokenUsage()
|
|
39
|
+
: null;
|
|
40
|
+
if (ui && !skipUiLock)
|
|
41
|
+
ui.lock(t('msg.input_locked'));
|
|
42
|
+
try {
|
|
43
|
+
if (!quiet)
|
|
44
|
+
log(t('compact.start'));
|
|
45
|
+
// 1) Ask the OLD chat to summarize itself.
|
|
46
|
+
let summary = '';
|
|
47
|
+
let summaryErr = '';
|
|
48
|
+
for (let attempt = 1; attempt <= COMPACT_SUMMARY_ATTEMPTS; attempt++) {
|
|
49
|
+
let prompt = buildCompactPrompt(locale);
|
|
50
|
+
if (attempt > 1) {
|
|
51
|
+
prompt +=
|
|
52
|
+
locale === 'en'
|
|
53
|
+
? '\n\nIMPORTANT: reply with the summary as PLAIN TEXT only. Do NOT call any tools and do NOT output tool-call JSON/DSML.'
|
|
54
|
+
: '\n\nВАЖНО: ответь ТОЛЬКО текстом резюме. НЕ вызывай инструменты и НЕ выводи JSON/DSML вызова инструмента.';
|
|
55
|
+
}
|
|
56
|
+
let answer = '';
|
|
57
|
+
try {
|
|
58
|
+
answer = await browser.ask(prompt, {
|
|
59
|
+
agent: true,
|
|
60
|
+
timeout: Math.max(60_000, answerTimeoutMs),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
summaryErr = e.message;
|
|
65
|
+
}
|
|
66
|
+
const trimmed = String(answer || '').trim();
|
|
67
|
+
if (/^\(прервано пользователем\)$/.test(trimmed)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
chatId: currentChatId,
|
|
71
|
+
summaryChars: 0,
|
|
72
|
+
beforeTokens,
|
|
73
|
+
error: trimmed,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (!summaryErr && isUsableCompactSummary(trimmed)) {
|
|
77
|
+
summary = trimmed;
|
|
78
|
+
summaryErr = '';
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!summaryErr)
|
|
82
|
+
summaryErr = trimmed || t('common.unknown');
|
|
83
|
+
if (attempt < COMPACT_SUMMARY_ATTEMPTS) {
|
|
84
|
+
log(t('compact.summary_retry', {
|
|
85
|
+
v: summaryErr,
|
|
86
|
+
attempt: String(attempt),
|
|
87
|
+
max: String(COMPACT_SUMMARY_ATTEMPTS),
|
|
88
|
+
}));
|
|
89
|
+
await new Promise((r) => setTimeout(r, 3000 * attempt));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!summary) {
|
|
93
|
+
let fallback = '';
|
|
94
|
+
try {
|
|
95
|
+
const all = browser.fetchChatMessages && currentChatId
|
|
96
|
+
? await browser.fetchChatMessages(currentChatId).catch(() => [])
|
|
97
|
+
: [];
|
|
98
|
+
const msgs = trimRestoredMessages(all, 0);
|
|
99
|
+
if (msgs.length) {
|
|
100
|
+
fallback = formatRestoredHistory(msgs, { limit: fallbackLimit });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
fallback = String(fallback || '').trim();
|
|
105
|
+
if (fallback) {
|
|
106
|
+
summary = fallback;
|
|
107
|
+
log(t('compact.summary_fallback', { v: String(fallbackLimit) }));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const err = summaryErr || t('common.unknown');
|
|
111
|
+
log(t('compact.summary_failed', { v: err }));
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
chatId: currentChatId,
|
|
115
|
+
summaryChars: 0,
|
|
116
|
+
beforeTokens,
|
|
117
|
+
error: err,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
transcript?.log('compact_summary', {
|
|
122
|
+
chars: summary.length,
|
|
123
|
+
beforeTokens,
|
|
124
|
+
});
|
|
125
|
+
// 2) New chat + system prompt + the summary as the first message.
|
|
126
|
+
await browser.newChat();
|
|
127
|
+
await browser.ask(buildSystemPrompt({ workdir, tools, locale }), {
|
|
128
|
+
timeout: 60_000,
|
|
129
|
+
agent: false,
|
|
130
|
+
});
|
|
131
|
+
await browser.ask(buildCompactCarryover(summary, task), {
|
|
132
|
+
timeout: 60_000,
|
|
133
|
+
agent: false,
|
|
134
|
+
});
|
|
135
|
+
const newChatId = browser.getCurrentChatId
|
|
136
|
+
? await browser.getCurrentChatId()
|
|
137
|
+
: null;
|
|
138
|
+
if (!quiet) {
|
|
139
|
+
log(t('compact.done') +
|
|
140
|
+
NL +
|
|
141
|
+
t('compact.report', {
|
|
142
|
+
chars: summary.length,
|
|
143
|
+
tokens: beforeTokens === null
|
|
144
|
+
? t('common.unknown')
|
|
145
|
+
: String(beforeTokens),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
if (ui && !quiet) {
|
|
149
|
+
ui.printAbove(NL +
|
|
150
|
+
'--- compacted context ---' +
|
|
151
|
+
NL +
|
|
152
|
+
summary +
|
|
153
|
+
NL +
|
|
154
|
+
'--- end ---' +
|
|
155
|
+
NL);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
chatId: newChatId,
|
|
160
|
+
summaryChars: summary.length,
|
|
161
|
+
beforeTokens,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
log(e.message);
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
chatId: currentChatId,
|
|
169
|
+
summaryChars: 0,
|
|
170
|
+
beforeTokens,
|
|
171
|
+
error: e.message,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (ui && !skipUiLock)
|
|
176
|
+
ui.unlock();
|
|
177
|
+
}
|
|
178
|
+
}
|
package/dist/config-menu.js
CHANGED
|
@@ -105,11 +105,26 @@ export function runConfigMenu(opts) {
|
|
|
105
105
|
output.write(lines.join('\n') + '\n');
|
|
106
106
|
lastRows = lines.reduce((n, l) => n + rowsOf(l), 0);
|
|
107
107
|
};
|
|
108
|
-
|
|
108
|
+
// Erase the rendered menu block from the screen. Used on exit so the config
|
|
109
|
+
// list does not stay in the terminal history / scrollback (the operator
|
|
110
|
+
// asked for the menu to disappear after Esc).
|
|
111
|
+
const clearBlock = () => {
|
|
112
|
+
if (lastRows > 0) {
|
|
113
|
+
output.write(ESC + '[' + lastRows + 'A');
|
|
114
|
+
for (let i = 0; i < lastRows; i++) {
|
|
115
|
+
output.write(ESC + '[2K' + ESC + '[1B');
|
|
116
|
+
}
|
|
117
|
+
output.write(ESC + '[' + lastRows + 'A');
|
|
118
|
+
lastRows = 0;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const cleanup = (clearOnExit = false) => {
|
|
109
122
|
if (exited)
|
|
110
123
|
return;
|
|
111
124
|
exited = true;
|
|
112
125
|
input.removeListener('data', onData);
|
|
126
|
+
if (clearOnExit)
|
|
127
|
+
clearBlock();
|
|
113
128
|
if (input.setRawMode)
|
|
114
129
|
input.setRawMode(wasRaw || false);
|
|
115
130
|
output.write(ESC + '[?25h'); // restore the cursor
|
|
@@ -167,13 +182,14 @@ export function runConfigMenu(opts) {
|
|
|
167
182
|
break;
|
|
168
183
|
}
|
|
169
184
|
if (ch === ESC) {
|
|
185
|
+
// Cancel the edit (do NOT exit the menu) — Esc in the field is
|
|
186
|
+
// "abandon this value", not "close the config".
|
|
170
187
|
editing = false;
|
|
171
188
|
editBuf = '';
|
|
172
189
|
break;
|
|
173
190
|
}
|
|
174
191
|
if (ch === '\x03') {
|
|
175
|
-
cleanup();
|
|
176
|
-
output.write('\n');
|
|
192
|
+
cleanup(true);
|
|
177
193
|
return;
|
|
178
194
|
}
|
|
179
195
|
if (ch === '\x7f' || ch === '\b') {
|
|
@@ -205,8 +221,9 @@ export function runConfigMenu(opts) {
|
|
|
205
221
|
}
|
|
206
222
|
const ch = s[i];
|
|
207
223
|
if (ch === 'q' || ch === 'Q' || ch === '\x03' || ch === ESC) {
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
// Erase the menu block from the screen so the config list does not
|
|
225
|
+
// linger in the terminal history after Esc.
|
|
226
|
+
cleanup(true);
|
|
210
227
|
return;
|
|
211
228
|
}
|
|
212
229
|
if (ch === 'k') {
|
package/dist/config.js
CHANGED
|
@@ -48,6 +48,10 @@ export const DEFAULTS = {
|
|
|
48
48
|
incompleteWaitMs: 2000,
|
|
49
49
|
autoContinue: true,
|
|
50
50
|
continueMinGapMs: 1500,
|
|
51
|
+
askDeadlineMs: 240000,
|
|
52
|
+
maxAfterToolRetries: 6,
|
|
53
|
+
autoCompact: false,
|
|
54
|
+
autoCompactPct: 95,
|
|
51
55
|
resendPromptOnResume: false,
|
|
52
56
|
deepThinking: false,
|
|
53
57
|
webSearch: true,
|
|
@@ -59,6 +63,7 @@ export const DEFAULTS = {
|
|
|
59
63
|
},
|
|
60
64
|
ui: {
|
|
61
65
|
locale: DEFAULT_LOCALE,
|
|
66
|
+
contextLimit: 1_000_000,
|
|
62
67
|
},
|
|
63
68
|
};
|
|
64
69
|
export function loadConfig() {
|
|
@@ -274,6 +279,36 @@ export const CONFIG_SCHEMA = [
|
|
|
274
279
|
labelKey: 'cfg.f.browser_continueMinGapMs',
|
|
275
280
|
groupKey: 'cfg.group.browser',
|
|
276
281
|
},
|
|
282
|
+
{
|
|
283
|
+
path: 'browser.askDeadlineMs',
|
|
284
|
+
type: 'number',
|
|
285
|
+
min: 30000,
|
|
286
|
+
max: 1800000,
|
|
287
|
+
labelKey: 'cfg.f.browser_askDeadlineMs',
|
|
288
|
+
groupKey: 'cfg.group.browser',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
path: 'browser.maxAfterToolRetries',
|
|
292
|
+
type: 'number',
|
|
293
|
+
min: 0,
|
|
294
|
+
max: 50,
|
|
295
|
+
labelKey: 'cfg.f.browser_maxAfterToolRetries',
|
|
296
|
+
groupKey: 'cfg.group.browser',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
path: 'browser.autoCompact',
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
labelKey: 'cfg.f.browser_autoCompact',
|
|
302
|
+
groupKey: 'cfg.group.browser',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
path: 'browser.autoCompactPct',
|
|
306
|
+
type: 'number',
|
|
307
|
+
min: 50,
|
|
308
|
+
max: 100,
|
|
309
|
+
labelKey: 'cfg.f.browser_autoCompactPct',
|
|
310
|
+
groupKey: 'cfg.group.browser',
|
|
311
|
+
},
|
|
277
312
|
{
|
|
278
313
|
path: 'browser.resendPromptOnResume',
|
|
279
314
|
type: 'boolean',
|
|
@@ -292,6 +327,14 @@ export const CONFIG_SCHEMA = [
|
|
|
292
327
|
labelKey: 'cfg.f.browser_webSearch',
|
|
293
328
|
groupKey: 'cfg.group.browser',
|
|
294
329
|
},
|
|
330
|
+
{
|
|
331
|
+
path: 'ui.contextLimit',
|
|
332
|
+
type: 'number',
|
|
333
|
+
min: 10_000,
|
|
334
|
+
max: 10_000_000,
|
|
335
|
+
labelKey: 'cfg.f.ui_contextLimit',
|
|
336
|
+
groupKey: 'cfg.group.ui',
|
|
337
|
+
},
|
|
295
338
|
];
|
|
296
339
|
export function getConfigField(path) {
|
|
297
340
|
return CONFIG_SCHEMA.find((f) => f.path === path);
|
package/dist/confirm.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import * as readlinePromises from 'readline/promises';
|
|
2
2
|
import { theme } from './theme.js';
|
|
3
3
|
import { unifiedDiff, colorDiff } from './diff.js';
|
|
4
|
+
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
4
5
|
export class ConfirmManager {
|
|
5
6
|
settings;
|
|
6
7
|
alwaysConfirm;
|
|
7
8
|
allowedForSession;
|
|
8
|
-
|
|
9
|
+
locale;
|
|
10
|
+
constructor({ config, locale = DEFAULT_LOCALE, } = {}) {
|
|
9
11
|
const c = config || {};
|
|
12
|
+
this.locale = locale;
|
|
10
13
|
this.settings = {
|
|
11
14
|
write: c.write !== false,
|
|
12
15
|
edit: c.edit !== false,
|
|
13
16
|
bash: c.bash !== false,
|
|
14
17
|
};
|
|
15
|
-
|
|
18
|
+
// An invalid operator-supplied regex must NEVER crash the run. Fall back to
|
|
19
|
+
// a literal substring match (case-insensitive) for that pattern.
|
|
20
|
+
this.alwaysConfirm = (c.alwaysConfirm || []).map((p) => {
|
|
21
|
+
try {
|
|
22
|
+
return new RegExp(p, 'i');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return new RegExp(escapeRegExp(p), 'i');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
16
28
|
this.allowedForSession = new Set();
|
|
17
29
|
}
|
|
18
30
|
_matchesAlwaysConfirm(command) {
|
|
@@ -28,21 +40,27 @@ export class ConfirmManager {
|
|
|
28
40
|
return false;
|
|
29
41
|
return true;
|
|
30
42
|
}
|
|
43
|
+
// Hot-update the language (from /config lang). The manager holds the locale
|
|
44
|
+
// so its prompt follows the interface language without a restart.
|
|
45
|
+
setLocale(locale) {
|
|
46
|
+
this.locale = locale;
|
|
47
|
+
}
|
|
31
48
|
async ask({ kind, target, preview = null, message, }) {
|
|
32
49
|
if (!this.shouldAsk(kind, target ?? null))
|
|
33
50
|
return true;
|
|
34
51
|
if (preview) {
|
|
35
52
|
console.log(preview);
|
|
36
53
|
}
|
|
37
|
-
const
|
|
54
|
+
const t = translate(this.locale);
|
|
55
|
+
const label = message || t('confirm.ask_label') + ' ' + kind + '?';
|
|
38
56
|
const question = theme.warn(`${label} `) +
|
|
39
57
|
theme.system('[') +
|
|
40
58
|
theme.assistant('y') +
|
|
41
|
-
theme.system(' =
|
|
59
|
+
theme.system(' = ' + t('common.yes') + ', ') +
|
|
42
60
|
theme.error('n') +
|
|
43
|
-
theme.system(' =
|
|
61
|
+
theme.system(' = ' + t('common.no') + ', ') +
|
|
44
62
|
theme.user('a') +
|
|
45
|
-
theme.system(' =
|
|
63
|
+
theme.system(' = ' + t('confirm.hint') + '] ');
|
|
46
64
|
const answer = await this._prompt(question);
|
|
47
65
|
if (answer === 'a') {
|
|
48
66
|
if (kind !== 'bash' || !this._matchesAlwaysConfirm(target ?? '')) {
|
|
@@ -75,6 +93,12 @@ export class ConfirmManager {
|
|
|
75
93
|
}
|
|
76
94
|
}
|
|
77
95
|
}
|
|
96
|
+
// Escape a string so it can be used as a literal regex (used when an
|
|
97
|
+
// operator-supplied alwaysConfirm pattern is invalid and we fall back to a
|
|
98
|
+
// substring match).
|
|
99
|
+
function escapeRegExp(s) {
|
|
100
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, (m) => '\\' + m);
|
|
101
|
+
}
|
|
78
102
|
export function formatDiffPreview(oldText, newText, { label } = {}) {
|
|
79
103
|
const diff = unifiedDiff(oldText, newText, { label });
|
|
80
104
|
return colorDiff(diff);
|
package/dist/context.js
CHANGED
|
@@ -25,9 +25,10 @@ async function readIfFile(p, budget) {
|
|
|
25
25
|
if (!st.isFile())
|
|
26
26
|
return null;
|
|
27
27
|
const raw = await fs.readFile(p, 'utf-8');
|
|
28
|
+
// An EXISTING but empty file is still present: keep it so the prompt
|
|
29
|
+
// shows an (empty) AGENTS.md/MEMORY.md section instead of silently
|
|
30
|
+
// dropping the file. Only a read failure returns null.
|
|
28
31
|
const content = clipIf(path.basename(p), raw, budget);
|
|
29
|
-
if (!content)
|
|
30
|
-
return null;
|
|
31
32
|
return { path: p, content };
|
|
32
33
|
}
|
|
33
34
|
catch {
|
package/dist/gitTools.js
CHANGED
|
@@ -177,8 +177,9 @@ export function createGitTools(workdir) {
|
|
|
177
177
|
}
|
|
178
178
|
// We stage everything only if the index is empty (as the description promises).
|
|
179
179
|
const staged = await runGit('git diff --cached --name-only', workdir, 10_000);
|
|
180
|
-
const
|
|
181
|
-
|
|
180
|
+
const stagedTrimmed = staged.trim();
|
|
181
|
+
const hasStaged = stagedTrimmed !== '' &&
|
|
182
|
+
stagedTrimmed !== '(command produced no output)';
|
|
182
183
|
const addResult = hasStaged
|
|
183
184
|
? '(index is not empty — add -A skipped)'
|
|
184
185
|
: await runGit('git add -A', workdir, 20_000);
|