agen-vektor 0.3.27 → 0.3.29

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.
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.takeCheckpoint = takeCheckpoint;
37
+ exports.listCheckpoints = listCheckpoints;
38
+ exports.restoreCheckpoint = restoreCheckpoint;
39
+ exports.pruneCheckpoints = pruneCheckpoints;
40
+ exports.resetCheckpoints = resetCheckpoints;
41
+ /**
42
+ * Checkpoints — git-based safety net (Freebuff/Codebuff parity).
43
+ *
44
+ * Every run, before the FIRST mutating tool call (write_file / edit_file /
45
+ * delete_file / apply_patch / shell) touches the project, the agent takes a
46
+ * CHECKPOINT: a full snapshot of the working tree stored in a SHADOW git
47
+ * repository under ~/.vector/checkpoints/<project>/.
48
+ *
49
+ * Design rules:
50
+ * - The USER'S repo is never touched: no commits, no branches, no refs, no
51
+ * hooks. The shadow repo is a SEPARATE GIT_DIR (own object store, index,
52
+ * refs) using the project tree as its work-tree. `git status` in the user's
53
+ * project stays pristine — verified by tests.
54
+ * - Each checkpoint is an INDEPENDENT root commit (git commit-tree, no
55
+ * parent) pinned by its own ref `refs/vector-cp/<ref>`. Unchanged files
56
+ * dedupe by content in the object store, so snapshots stay small — and
57
+ * pruning is a plain ref deletion, no history rewrite.
58
+ * - Restore = index reset to the snapshot ref + `checkout <ref> -- .` +
59
+ * resurrection of files deleted after the snapshot. The user's git history
60
+ * is untouched (their unstaged changes are replaced — they can still diff
61
+ * via their own git BEFORE restoring; /undo warns in the TUI).
62
+ * - Bounded storage: keep the newest MAX_CHECKPOINTS refs, delete the rest.
63
+ * - Everything is best-effort: a missing git binary or a read-only home must
64
+ * NEVER break a run. All failures resolve to null/empty and the run
65
+ * proceeds without a safety net.
66
+ */
67
+ const node_child_process_1 = require("node:child_process");
68
+ const fs = __importStar(require("node:fs"));
69
+ const path = __importStar(require("node:path"));
70
+ const paths_1 = require("../utils/paths");
71
+ const MAX_CHECKPOINTS = 30;
72
+ const REF_PREFIX = 'refs/vector-cp/';
73
+ const GIT = 'git';
74
+ // Monotonic per-process sequence: same-second snapshots must keep their
75
+ // creation order in listings (restore-by-index!), so the ref suffix is a
76
+ // zero-padded base36 counter instead of random chars — ref-name sort then
77
+ // equals reverse-creation order everywhere.
78
+ let cpSeq = 0;
79
+ function gitDirFor(cwd) {
80
+ // Stable per project path — two projects never share a shadow repo.
81
+ const slug = cwd.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(-80) || 'project';
82
+ return path.join((0, paths_1.getVectorDir)(), 'checkpoints', slug);
83
+ }
84
+ function git(gitDir, args, workTree) {
85
+ const r = (0, node_child_process_1.spawnSync)(GIT, [`--git-dir=${gitDir}`, ...(workTree ? [`--work-tree=${workTree}`] : []), ...args], { encoding: 'utf8', timeout: 20_000 });
86
+ return { ok: r.status === 0, out: (r.stdout || '') + (r.stderr || '') };
87
+ }
88
+ function ensureShadowRepo(gitDir) {
89
+ try {
90
+ if (!fs.existsSync(path.join(gitDir, 'HEAD'))) {
91
+ fs.mkdirSync(gitDir, { recursive: true });
92
+ // Fixed identity — nothing from the user's git config leaks in.
93
+ if (!git(gitDir, ['init', '--quiet']).ok)
94
+ return false;
95
+ git(gitDir, ['config', 'user.email', 'checkpoints@vectorhead.local']);
96
+ git(gitDir, ['config', 'user.name', 'VectorHead Checkpoints']);
97
+ git(gitDir, ['config', 'commit.gpgsign', 'false']);
98
+ git(gitDir, ['config', 'core.autocrlf', 'false']);
99
+ git(gitDir, ['config', 'gc.auto', '0']);
100
+ }
101
+ return fs.existsSync(path.join(gitDir, 'HEAD'));
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ /**
108
+ * Take a checkpoint. Returns null when snapshotting is impossible — callers
109
+ * must treat that as "no safety net", never as an error.
110
+ */
111
+ function takeCheckpoint(cwd, reason, task = '') {
112
+ try {
113
+ if (!fs.existsSync(cwd))
114
+ return null;
115
+ const gitDir = gitDirFor(cwd);
116
+ if (!ensureShadowRepo(gitDir))
117
+ return null;
118
+ // -A stages the full tree incl. deletions and untracked files. A
119
+ // .gitignore in the user's project is respected (their intent), but
120
+ // that also means ignored files are NOT protected by checkpoints —
121
+ // documented trade-off, same as Claude Code.
122
+ if (!git(gitDir, ['add', '-A', '--'], cwd).ok)
123
+ return null;
124
+ const tree = git(gitDir, ['write-tree']);
125
+ if (!tree.ok)
126
+ return null;
127
+ const treeHash = tree.out.trim().split('\n')[0].trim();
128
+ if (!/^[0-9a-f]{40}$/.test(treeHash))
129
+ return null;
130
+ const stamp = new Date();
131
+ const pad = (n) => String(n).padStart(2, '0');
132
+ const t = `${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}` +
133
+ `-${pad(stamp.getHours())}${pad(stamp.getMinutes())}${pad(stamp.getSeconds())}`;
134
+ const suffix = (cpSeq++).toString(36).padStart(4, '0');
135
+ const ref = `cp-${t}-${suffix}`;
136
+ const msg = `checkpoint ${ref} (${reason}) ${task.slice(0, 80)}`.trim();
137
+ // Independent root commit — commit-tree gives full control over parents.
138
+ const commit = git(gitDir, ['commit-tree', treeHash, '-m', msg]);
139
+ if (!commit.ok)
140
+ return null;
141
+ const hash = commit.out.trim().split('\n')[0].trim();
142
+ if (!/^[0-9a-f]{40}$/.test(hash))
143
+ return null;
144
+ if (!git(gitDir, ['update-ref', `${REF_PREFIX}${ref}`, hash]).ok)
145
+ return null;
146
+ return { ref, time: stamp.toISOString(), reason, task: task.slice(0, 80), hash };
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ /** Newest-first list of surviving checkpoints. */
153
+ function listCheckpoints(cwd) {
154
+ try {
155
+ const gitDir = gitDirFor(cwd);
156
+ if (!fs.existsSync(path.join(gitDir, 'HEAD')))
157
+ return [];
158
+ const r = git(gitDir, [
159
+ 'for-each-ref',
160
+ `--format=%(refname)%09%(contents:subject)%09%(committerdate:iso-strict)`,
161
+ REF_PREFIX,
162
+ ]);
163
+ if (!r.ok)
164
+ return [];
165
+ const out = [];
166
+ for (const line of r.out.split('\n')) {
167
+ if (line.trim() === '')
168
+ continue;
169
+ const [refname = '', subject = '', time = ''] = line.split('\t');
170
+ const ref = refname.replace(REF_PREFIX, '');
171
+ const m = subject.match(/^(cp-[0-9]{8}-[0-9]{6}-[a-z0-9]+) \(([^)]*)\) ?(.*)$/);
172
+ const hash = git(gitDir, ['rev-parse', `${REF_PREFIX}${ref}`]).out.trim();
173
+ if (!/^[0-9a-f]{40}$/.test(hash))
174
+ continue;
175
+ out.push({
176
+ ref: m ? m[1] : ref,
177
+ time: time.trim(),
178
+ reason: m ? m[2] : '',
179
+ task: m ? m[3] : subject,
180
+ hash,
181
+ });
182
+ }
183
+ // Same-second snapshots tie on committerdate — the ref name embeds a
184
+ // fixed-width timestamp, so sort on THAT (newest first) in JS instead of
185
+ // relying on git tie-breaking.
186
+ out.sort((a, b) => (a.ref < b.ref ? 1 : a.ref > b.ref ? -1 : 0));
187
+ return out;
188
+ }
189
+ catch {
190
+ return [];
191
+ }
192
+ }
193
+ /**
194
+ * Restore the working tree to a checkpoint (index into the newest-first
195
+ * list, an exact ref suffix, or a full hash). Files deleted AFTER the
196
+ * snapshot are resurrected; files the snapshot never tracked are left
197
+ * alone. Returns a user-facing message either way.
198
+ */
199
+ function restoreCheckpoint(cwd, refOrIndex) {
200
+ const list = listCheckpoints(cwd);
201
+ if (list.length === 0)
202
+ return { ok: false, message: 'No checkpoints yet.' };
203
+ let cp;
204
+ if (typeof refOrIndex === 'number') {
205
+ if (refOrIndex < 0 || refOrIndex >= list.length)
206
+ return { ok: false, message: 'Checkpoint index out of range.' };
207
+ cp = list[refOrIndex];
208
+ }
209
+ else {
210
+ cp = list.find((c) => c.ref === refOrIndex || c.hash === refOrIndex);
211
+ if (!cp)
212
+ return { ok: false, message: `Unknown checkpoint "${refOrIndex}".` };
213
+ }
214
+ const gitDir = gitDirFor(cwd);
215
+ const target = cp; // bounds-checked above
216
+ // index := snapshot tree, then materialize the whole tree over the
217
+ // work-tree (restores modified AND deleted files).
218
+ if (!git(gitDir, ['read-tree', '--reset', '-u', target.hash], cwd).ok) {
219
+ return { ok: false, message: `restore failed: read-tree ${target.ref}` };
220
+ }
221
+ const checkout = git(gitDir, ['checkout', target.hash, '--', '.'], cwd);
222
+ if (!checkout.ok) {
223
+ return { ok: false, message: `restore failed: ${checkout.out.trim().slice(0, 200)}` };
224
+ }
225
+ // read-tree -u handles deletions of tracked files already; walk the
226
+ // snapshot tree to be extra safe about paths git skipped.
227
+ const ls = git(gitDir, ['ls-tree', '-r', '--name-only', target.hash]);
228
+ if (ls.ok) {
229
+ for (const f of ls.out.split('\n')) {
230
+ const p = f.trim();
231
+ if (p && !fs.existsSync(path.join(cwd, p))) {
232
+ git(gitDir, ['checkout', target.hash, '--', p], cwd);
233
+ }
234
+ }
235
+ }
236
+ return { ok: true, message: `Restored to checkpoint ${target.ref} (${target.time}).` };
237
+ }
238
+ /** Keep only the newest MAX_CHECKPOINTS refs (best-effort, storage bound). */
239
+ function pruneCheckpoints(cwd) {
240
+ try {
241
+ const gitDir = gitDirFor(cwd);
242
+ if (!fs.existsSync(path.join(gitDir, 'HEAD')))
243
+ return;
244
+ const list = listCheckpoints(cwd);
245
+ for (const cp of list.slice(MAX_CHECKPOINTS)) {
246
+ git(gitDir, ['update-ref', '-d', `${REF_PREFIX}${cp.ref}`]);
247
+ }
248
+ // Drop unreferenced objects occasionally — cheap, bounded.
249
+ if (list.length > MAX_CHECKPOINTS)
250
+ git(gitDir, ['gc', '--quiet', '--prune=now']);
251
+ }
252
+ catch {
253
+ /* best-effort */
254
+ }
255
+ }
256
+ /** Test helper: wipe the shadow repo for a project. */
257
+ function resetCheckpoints(cwd) {
258
+ try {
259
+ fs.rmSync(gitDirFor(cwd), { recursive: true, force: true });
260
+ }
261
+ catch {
262
+ /* ignore */
263
+ }
264
+ }
@@ -15,6 +15,7 @@ exports.estimateContextSize = estimateContextSize;
15
15
  */
16
16
  const provider_1 = require("../providers/provider");
17
17
  const context_1 = require("./context");
18
+ const checkpoints_1 = require("./checkpoints");
18
19
  const light_1 = require("./light");
19
20
  const prompts_1 = require("./prompts");
20
21
  const rules_1 = require("./rules");
@@ -139,6 +140,22 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
139
140
  let stopped = false;
140
141
  let finalContent = '';
141
142
  let lastReasoning = '';
143
+ // ─── Checkpoints (git-based safety net, Freebuff/Codebuff parity) ───
144
+ // Armed lazily: the snapshot is taken right before the FIRST mutating
145
+ // tool call of the run (never for pure read/chat runs), once per run.
146
+ // Best-effort: null = no net, the run proceeds regardless.
147
+ const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'delete_file', 'apply_patch', 'shell', 'git', 'run_in_background']);
148
+ let checkpointTaken = false;
149
+ const maybeCheckpoint = (toolName) => {
150
+ if (checkpointTaken || !MUTATING_TOOLS.has(toolName))
151
+ return;
152
+ checkpointTaken = true;
153
+ const cp = (0, checkpoints_1.takeCheckpoint)(cwd, toolName, userRequest);
154
+ if (cp) {
155
+ callbacks.onStatus?.(`Checkpoint siap (${cp.ref})`);
156
+ callbacks.onActivity?.('checkpoint', `safety snapshot ${cp.ref}`);
157
+ }
158
+ };
142
159
  const checkAbort = () => {
143
160
  if (signal?.aborted) {
144
161
  aborted = true;
@@ -420,6 +437,11 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
420
437
  for (const batch of batches) {
421
438
  checkAbort();
422
439
  const parallel = batch.length > 1 && batch.every((tc) => PARALLEL_TOOLS.has(tc.name));
440
+ // Safety net BEFORE the first mutating call of this run — armed
441
+ // once, lazily; parallel batches are read-only by construction and
442
+ // never trigger it.
443
+ if (!parallel)
444
+ maybeCheckpoint(batch[0].name);
423
445
  toolCalls += batch.length;
424
446
  if (!parallel) {
425
447
  callbacks.onStatus?.(`Running ${batch[0].name}`);
@@ -67,7 +67,11 @@ exports.DEFAULT_CONFIG = {
67
67
  // so the guardrail floor stays intact. Opt back into prompting via
68
68
  // VECTOR_ASK=1 or the /mode toggle in the TUI.
69
69
  permissionMode: 'yolo',
70
- maxIterations: 25,
70
+ // 50 (dulu 25): task panjang sering mati di tengah — "runs out of
71
+ // iterations" — padahal agen masih di jalur yang benar. Naik tanpa
72
+ // risiko: iteration = 1 langkah agen (LLM call + tools), bukan 1 API
73
+ // key query; kuota gateway dijaga rate-limit per-request di sisi gateway.
74
+ maxIterations: 50,
71
75
  maxRetries: 3,
72
76
  requestTimeoutMs: 120_000,
73
77
  extraModels: {},
package/dist/tools/e2b.js CHANGED
@@ -335,6 +335,90 @@ ${lines.join('\n')}`,
335
335
  };
336
336
  },
337
337
  },
338
+ {
339
+ definition: {
340
+ name: 'e2b_screenshot',
341
+ description: 'Take a screenshot of a URL with headless Chromium INSIDE a persistent e2b cloud session (from e2b_cloud create) and get it back as a downloadable gateway link (render_ui button opens it). Requires Playwright+Chromium installed in the session (npx playwright install --with-deps chromium). The target URL is fetched FROM INSIDE the sandbox — use http://localhost:<port> for dev servers running in this session. The sandbox needs internet unless the target is localhost.',
342
+ parameters: {
343
+ type: 'object',
344
+ properties: {
345
+ id: { type: 'string', description: 'Session id from e2b_cloud create' },
346
+ url: { type: 'string', description: 'URL to capture, e.g. http://localhost:3000 (default)' },
347
+ wait_ms: { type: 'number', description: 'Milliseconds to wait for page load before capturing (default 1500)' },
348
+ full_page: { type: 'boolean', description: 'Capture full scrollable page (default false)' },
349
+ },
350
+ required: ['id'],
351
+ },
352
+ },
353
+ async execute(args, ctx) {
354
+ const id = String(args.id || '');
355
+ const target = String(args.url || 'http://localhost:3000').trim();
356
+ if (!/^[A-Za-z0-9-]{8,80}$/.test(id))
357
+ return { output: 'ERROR: invalid session id' };
358
+ if (!/^https?:\/\//.test(target) || target.includes("'"))
359
+ return { output: 'ERROR: url harus http(s) tanpa quote' };
360
+ const waitMs = Math.min(15_000, Math.max(0, Number(args.wait_ms) || 1500));
361
+ const fullPage = args.full_page === true;
362
+ // Dua langkah: (1) exec script Playwright → simpan PNG di /home/user/, (2) beri
363
+ // link download via route /v1/e2b/file (gateway stream PNG → tombol render_ui
364
+ // di TUI membukanya di browser). Script memakai flag jitless WAJIB (gotcha V8
365
+ // OOM di sandbox kecil) dan hanya setelah marker __VECTOR_DONE_ hasil poll.
366
+ // PENTING: script DITULIS ke /home/user (bukan /tmp) — ESM resolve
367
+ // `import 'playwright'` relatif thd lokasi file, dan package terinstall
368
+ // di /home/user/node_modules (npm i tanpa -g).
369
+ const script = [
370
+ 'mkdir -p /home/user',
371
+ `cat > /home/user/vector-shot.mjs <<\'EOF\'`,
372
+ `import { chromium } from 'playwright';`,
373
+ `const b = await chromium.launch({ args: ['--no-sandbox','--disable-dev-shm-usage','--disable-gpu','--js-flags=--jitless'] });`,
374
+ `const p = await b.newPage();`,
375
+ `try {`,
376
+ ` await p.goto(${JSON.stringify(target)}, { waitUntil: 'load', timeout: 20000 });`,
377
+ ` await p.waitForTimeout(${waitMs});`,
378
+ ` await p.screenshot({ path: '/home/user/vector-preview.png', fullPage: ${fullPage ? 'true' : 'false'} });`,
379
+ ` console.log('SHOT_OK');`,
380
+ `} catch (e) { console.log('SHOT_ERR ' + e.message); await b.close(); process.exit(3); }`,
381
+ `await b.close();`,
382
+ `EOF`,
383
+ `cd /home/user && node /home/user/vector-shot.mjs`,
384
+ ].join('\n');
385
+ ctx.onActivity?.('e2b', `screenshot ${target}`);
386
+ // Poll exec manual di sini (bukan e2bExec blocking): kita butuh exit code.
387
+ const started = await fetch(GW_BASE + '/v1/e2b/exec?id=' + encodeURIComponent(id), {
388
+ method: 'POST',
389
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'VectorHead-AI-Agent/0.1' },
390
+ body: JSON.stringify({ command: script }),
391
+ signal: AbortSignal.timeout(30_000),
392
+ });
393
+ const sdata = (await started.json().catch(() => null));
394
+ if (!started.ok || !sdata?.jobId) {
395
+ return { output: `ERROR: e2b exec gagal — ${(0, credentials_1.redact)(sdata?.error || 'HTTP ' + started.status)}`, summary: 'screenshot exec error' };
396
+ }
397
+ const t0 = Date.now();
398
+ while (Date.now() - t0 < 90_000) {
399
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
400
+ const jr = await fetch(GW_BASE + '/v1/e2b/job?id=' + encodeURIComponent(id) + '&job=' + sdata.jobId, {
401
+ signal: AbortSignal.timeout(15_000),
402
+ });
403
+ const j = (await jr.json().catch(() => null));
404
+ if (j?.status === 'done') {
405
+ if (j.exitCode !== 0 || !(j.stdout || '').includes('SHOT_OK')) {
406
+ const why = (j.stderr || j.stdout || '').trim().slice(0, 300);
407
+ return { output: `ERROR: screenshot gagal (exit ${j.exitCode})\n${(0, credentials_1.redact)(why)}` + (why.includes('Cannot find package') ? '\n→ Playwright belum terpasang di sesi ini: e2b_exec {command: "npm i playwright && npx playwright install --with-deps chromium"}' : ''), summary: 'screenshot error' };
408
+ }
409
+ const link = `${GW_BASE}/v1/e2b/file?id=${encodeURIComponent(id)}&path=%2Fhome%2Fuser%2Fvector-preview.png`;
410
+ return {
411
+ output: `screenshot tersimpan: /home/user/vector-preview.png (${target})\nlink: ${link}`,
412
+ summary: `e2b screenshot ${target}`,
413
+ data: { widget: { type: 'button', text: '🖼 Lihat screenshot', link }, screenshot: link },
414
+ };
415
+ }
416
+ if (j?.status === 'error')
417
+ break;
418
+ }
419
+ return { output: 'ERROR: screenshot timeout (job tidak selesai dalam 90s)', summary: 'screenshot timeout' };
420
+ },
421
+ },
338
422
  {
339
423
  definition: {
340
424
  name: 'e2b_download',
package/dist/tui/app.js CHANGED
@@ -59,6 +59,7 @@ const commands_1 = require("./commands");
59
59
  const suggest_1 = require("./suggest");
60
60
  const mcp_1 = require("../tools/mcp");
61
61
  const provider_1 = require("../providers/provider");
62
+ const checkpoints_1 = require("../agent/checkpoints");
62
63
  const planner_1 = require("../agent/planner");
63
64
  const extras_1 = require("../tools/extras");
64
65
  const session_1 = require("../agent/session");
@@ -1762,7 +1763,13 @@ class App {
1762
1763
  this.runFollowupMsgs.push(followupMsg);
1763
1764
  }
1764
1765
  }
1765
- if (tool === 'render_ui' && data && data.widget) {
1766
+ // Tombol UI: tool render_ui — dan kini juga tool e2b (screenshot/
1767
+ // preview) — bisa mengembalikan data.widget {type:'button',text,link}.
1768
+ // Whitelist supaya tool pihak ketiga (MCP) tidak bisa memicu tombol
1769
+ // (surface prompt-injection: deskripsi/hasil tool = untrusted data).
1770
+ if ((tool === 'render_ui' || tool.startsWith('e2b_')) &&
1771
+ data &&
1772
+ data.widget) {
1766
1773
  const w = data.widget;
1767
1774
  this.activeUiButton = w;
1768
1775
  this.messages.push({ kind: 'ui', content: w.text, ui: w, ts: Date.now() });
@@ -2163,6 +2170,17 @@ class App {
2163
2170
  case '/session':
2164
2171
  this.modal = { type: 'session', selected: 0, start: 0 };
2165
2172
  break;
2173
+ case '/undo': {
2174
+ // Freebuff/Codebuff-style safety net: pick a checkpoint (auto taken
2175
+ // before the first mutating tool call of each run) and restore it.
2176
+ const checkpoints = (0, checkpoints_1.listCheckpoints)(this.agent.cwd);
2177
+ if (checkpoints.length === 0) {
2178
+ this.addSystem('Belum ada checkpoint — snapshot dibuat otomatis sebelum tool menulis pertama kali di sebuah run.');
2179
+ break;
2180
+ }
2181
+ this.modal = { type: 'undo', selected: 0, start: 0, checkpoints };
2182
+ break;
2183
+ }
2166
2184
  case '/settings':
2167
2185
  this.modal = { type: 'settings' };
2168
2186
  break;
@@ -2414,8 +2432,11 @@ class App {
2414
2432
  // Esc still closes. Changes persist via saveConfig immediately.
2415
2433
  {
2416
2434
  const cfg = this.agent.config;
2435
+ // Cap 999 (dulu 200): task besar (refactor menyeluruh, migrasi,
2436
+ // riset panjang) butuh budget lebih — 200 sering masih kurang.
2437
+ const cap = 999;
2417
2438
  const step = (d) => {
2418
- const next = Math.min(200, Math.max(1, cfg.maxIterations + d));
2439
+ const next = Math.min(cap, Math.max(1, cfg.maxIterations + d));
2419
2440
  if (next === cfg.maxIterations)
2420
2441
  return;
2421
2442
  cfg.maxIterations = next;
@@ -2451,7 +2472,7 @@ class App {
2451
2472
  else if (ev.name === 'char' && ev.char && /[0-9]/.test(ev.char)) {
2452
2473
  // Number keys type the exact value (not digit-append — no
2453
2474
  // visible field, so append would be invisible state).
2454
- const next = Math.min(200, Math.max(1, Number(ev.char)));
2475
+ const next = Math.min(cap, Math.max(1, Number(ev.char)));
2455
2476
  if (next !== cfg.maxIterations) {
2456
2477
  cfg.maxIterations = next;
2457
2478
  try {
@@ -2527,6 +2548,41 @@ class App {
2527
2548
  this.modal = { type: 'none' };
2528
2549
  });
2529
2550
  break;
2551
+ case 'undo': {
2552
+ const cps = modal.checkpoints;
2553
+ await this.handleSelector(modal, ev, cps.map((c) => ({
2554
+ label: `${c.time.replace('T', ' ').slice(0, 19)} — ${c.task || c.reason}`,
2555
+ value: c.ref,
2556
+ hint: c.reason,
2557
+ })), async (sel) => {
2558
+ this.modal = { type: 'none' };
2559
+ if (this.running) {
2560
+ this.addSystem('Tidak bisa restore saat agen sedang jalan — hentikan dulu (Esc).');
2561
+ return;
2562
+ }
2563
+ // Confirm: restore MENIMPA perubahan yang belum di-commit.
2564
+ this.modal = {
2565
+ type: 'confirm',
2566
+ title: 'Restore checkpoint',
2567
+ body: [
2568
+ `Kembalikan seluruh file project ke snapshot ${sel.value}?`,
2569
+ '',
2570
+ 'Perubahan yang BELUM di-commit akan tertimpa (riwayat git kamu tetap utuh —',
2571
+ 'restore hanya menyentuh working tree).',
2572
+ ],
2573
+ focused: 'cancel',
2574
+ resolve: async (ok) => {
2575
+ this.modal = { type: 'none' };
2576
+ if (!ok)
2577
+ return;
2578
+ const r = (0, checkpoints_1.restoreCheckpoint)(this.agent.cwd, sel.value);
2579
+ this.addSystem(r.ok ? `⏪ ${r.message}` : `! ${r.message}`);
2580
+ this.markDirty();
2581
+ },
2582
+ };
2583
+ });
2584
+ break;
2585
+ }
2530
2586
  case 'session': {
2531
2587
  const sessions = (0, session_1.listSessions)();
2532
2588
  await this.handleSelector(modal, ev, sessions.map((s) => ({ label: s.name, value: s.name, hint: `${s.messageCount} msgs` })), async (s) => {
@@ -3324,7 +3380,7 @@ class App {
3324
3380
  label('Mode', this.agent.config.permissionMode),
3325
3381
  // LIVE-EDITABLE: +/- / ↑↓ / PgUp-PgDn step the budget, digits
3326
3382
  // type an exact value — persisted to ~/.vector/config.json.
3327
- label('Max iterations', `${this.agent.config.maxIterations} ${theme_1.THEME.muted}(+/- / ↑↓ / PgUp-PgDn / angka)${theme_1.THEME.reset}`),
3383
+ label('Max iterations', `${this.agent.config.maxIterations} ${theme_1.THEME.muted}(+/- / ↑↓ / PgUp-PgDn / angka · cap 999)${theme_1.THEME.reset}`),
3328
3384
  label('Max retries', String(this.agent.config.maxRetries)),
3329
3385
  label('API URL', this.agent.config.apiUrl || '(default)'),
3330
3386
  label('Key configured', (0, credentials_1.hasApiKey)(this.agent.config.provider) ? 'yes' : 'no'),
@@ -3414,6 +3470,22 @@ class App {
3414
3470
  cols,
3415
3471
  });
3416
3472
  }
3473
+ case 'undo': {
3474
+ return (0, components_1.renderSelector)({
3475
+ title: 'Restore checkpoint',
3476
+ options: m.checkpoints.length > 0
3477
+ ? m.checkpoints.map((c) => ({
3478
+ label: `${c.time.replace('T', ' ').slice(0, 19)} — ${c.task || c.reason}`,
3479
+ value: c.ref,
3480
+ hint: c.reason,
3481
+ }))
3482
+ : [{ label: '(no checkpoints yet)', value: '' }],
3483
+ selected: m.selected,
3484
+ start: m.start,
3485
+ rows,
3486
+ cols,
3487
+ });
3488
+ }
3417
3489
  case 'theme': {
3418
3490
  const themes = (0, themes_1.listThemes)();
3419
3491
  return (0, components_1.renderSelector)({
@@ -23,6 +23,7 @@ exports.COMMANDS = [
23
23
  { id: 'model', description: 'select model for the active provider' },
24
24
  { id: 'theme', description: 'select theme (OpenCode-style)' },
25
25
  { id: 'session', description: 'manage sessions (list / switch)' },
26
+ { id: 'undo', description: 'restore project files to a checkpoint (auto-snapshot before edits)' },
26
27
  { id: 'session-name', description: 'set session name (arg optional)' },
27
28
  { id: 'settings', description: 'view settings (provider, model, limits, files)' },
28
29
  { id: 'mcp', description: 'MCP servers — probe config, list tools per server' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agen-vektor",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "description": "VectorHead (agen-vektor) — AI Coding Agent CLI/TUI for Linux & Termux. Multi-provider, tool calling, session, permission system.",
5
5
  "type": "commonjs",
6
6
  "bin": {