zames_pro 2.29.16 → 2.32.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/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.
@@ -168,6 +169,15 @@ export function renderDoctor(d) {
168
169
  row(!!d.clipboardTool, 'clipboard', d.clipboardTool || 'no tool found (image paste disabled)');
169
170
  row(true, 'mcp', d.mcpServers + ' server(s), ' + d.mcpTools + ' tool(s)');
170
171
  row(d.transcriptOk, 'transcript', d.transcriptOk ? 'on' : 'off');
172
+ if (typeof d.contextLimit === 'number' && d.contextLimit > 0) {
173
+ row(true, 'context', d.contextLimit.toLocaleString('en-US') + ' tokens');
174
+ }
175
+ if (d.gitOk && typeof d.hasOrigin === 'boolean') {
176
+ row(true, 'git remote', d.hasOrigin ? 'origin configured' : 'no origin');
177
+ }
178
+ if (typeof d.minSendIntervalMs === 'number') {
179
+ row(d.minSendIntervalMs > 0, 'send pause', Math.round(d.minSendIntervalMs / 1000) + 's between agent sends');
180
+ }
171
181
  return 'Doctor:' + NL + rows.join(NL);
172
182
  }
173
183
  export function renderPermissions(p) {
@@ -405,6 +415,74 @@ export function buildCompactCarryover(summary, task) {
405
415
  return out;
406
416
  }
407
417
  // ---------- /review ----------
418
+ // ---------- Ctrl+C escalation ----------
419
+ // Decide what a Ctrl+C does while the agent is busy. The FIRST press aborts
420
+ // the currently running TOOL; a SECOND press within the window stops the WHOLE
421
+ // run (including the queue). Outside the window it is a fresh first press.
422
+ // Returns 'tool' | 'run'.
423
+ export function ctrlCEscalation(sinceLastMs, windowMs = 2000) {
424
+ return sinceLastMs < windowMs ? 'run' : 'tool';
425
+ }
426
+ // True when the queued message is a slash-command. It must NOT be merged into
427
+ // a task: the main loop has to execute it (e.g. /compact, /new).
428
+ export function isSlashCommand(text) {
429
+ return String(text ?? '')
430
+ .trim()
431
+ .startsWith('/');
432
+ }
433
+ function queuedIsImage(a) {
434
+ return (String(a.mime || '').startsWith('image/') ||
435
+ isImageName(String(a.name || '')));
436
+ }
437
+ // Merge a batch of queued messages into a single task. Marker numbering is
438
+ // PER-MESSAGE ([image#1]/[file#1] restart in every message), so a naive
439
+ // concatenation would produce duplicate markers for different files. We
440
+ // RENUMBER the markers across the batch and rebuild the attachments array in
441
+ // the same order, keeping the text markers the source of truth.
442
+ export function mergeMessages(msgs) {
443
+ const list = (msgs || []).filter((m) => String(m?.text ?? '').trim() !== '');
444
+ if (list.length === 0)
445
+ return { text: '', attachments: [] };
446
+ if (list.length === 1) {
447
+ return {
448
+ text: list[0].text,
449
+ attachments: (list[0].attachments || []).slice(),
450
+ };
451
+ }
452
+ let imgOffset = 0;
453
+ let fileOffset = 0;
454
+ const attachments = [];
455
+ const parts = [];
456
+ for (const m of list) {
457
+ const atts = m.attachments || [];
458
+ const images = atts.filter(queuedIsImage);
459
+ const files = atts.filter((a) => !queuedIsImage(a));
460
+ let text = String(m.text);
461
+ text = text.replace(/\[image#(\d+)\]/g, (whole, n) => {
462
+ const i = Number(n);
463
+ if (!Number.isFinite(i) || i < 1 || i > images.length)
464
+ return whole;
465
+ return '[image#' + (imgOffset + i) + ']';
466
+ });
467
+ text = text.replace(/\[file#(\d+)\]/g, (whole, n) => {
468
+ const i = Number(n);
469
+ if (!Number.isFinite(i) || i < 1 || i > files.length)
470
+ return whole;
471
+ return '[file#' + (fileOffset + i) + ']';
472
+ });
473
+ imgOffset += images.length;
474
+ fileOffset += files.length;
475
+ attachments.push(...atts);
476
+ parts.push(text.trim());
477
+ }
478
+ const header = 'The operator sent ' +
479
+ list.length +
480
+ ' messages while you were working. They are listed below in order — ' +
481
+ 'treat them as one instruction.';
482
+ const text = header + NL + NL + parts.join(NL + NL + '---' + NL + NL);
483
+ return { text, attachments };
484
+ }
485
+ // ---------- /review ----------
408
486
  export function buildReviewPrompt(focus, hasStaged = false) {
409
487
  const scope = hasStaged ? 'staged' : 'uncommitted';
410
488
  const f = String(focus ?? '').trim();
@@ -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
+ }
@@ -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
- const cleanup = () => {
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
- cleanup();
209
- output.write('\n');
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
- constructor({ config } = {}) {
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
- this.alwaysConfirm = (c.alwaysConfirm || []).map((p) => new RegExp(p, 'i'));
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 label = message || `Разрешить ${kind}?`;
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 hasStaged = !/^\s*/.test(staged) &&
181
- staged.trim() !== '(command produced no output)';
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);