agen-vektor 0.3.27 → 0.3.28

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/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");
@@ -2163,6 +2164,17 @@ class App {
2163
2164
  case '/session':
2164
2165
  this.modal = { type: 'session', selected: 0, start: 0 };
2165
2166
  break;
2167
+ case '/undo': {
2168
+ // Freebuff/Codebuff-style safety net: pick a checkpoint (auto taken
2169
+ // before the first mutating tool call of each run) and restore it.
2170
+ const checkpoints = (0, checkpoints_1.listCheckpoints)(this.agent.cwd);
2171
+ if (checkpoints.length === 0) {
2172
+ this.addSystem('Belum ada checkpoint — snapshot dibuat otomatis sebelum tool menulis pertama kali di sebuah run.');
2173
+ break;
2174
+ }
2175
+ this.modal = { type: 'undo', selected: 0, start: 0, checkpoints };
2176
+ break;
2177
+ }
2166
2178
  case '/settings':
2167
2179
  this.modal = { type: 'settings' };
2168
2180
  break;
@@ -2414,8 +2426,11 @@ class App {
2414
2426
  // Esc still closes. Changes persist via saveConfig immediately.
2415
2427
  {
2416
2428
  const cfg = this.agent.config;
2429
+ // Cap 999 (dulu 200): task besar (refactor menyeluruh, migrasi,
2430
+ // riset panjang) butuh budget lebih — 200 sering masih kurang.
2431
+ const cap = 999;
2417
2432
  const step = (d) => {
2418
- const next = Math.min(200, Math.max(1, cfg.maxIterations + d));
2433
+ const next = Math.min(cap, Math.max(1, cfg.maxIterations + d));
2419
2434
  if (next === cfg.maxIterations)
2420
2435
  return;
2421
2436
  cfg.maxIterations = next;
@@ -2451,7 +2466,7 @@ class App {
2451
2466
  else if (ev.name === 'char' && ev.char && /[0-9]/.test(ev.char)) {
2452
2467
  // Number keys type the exact value (not digit-append — no
2453
2468
  // visible field, so append would be invisible state).
2454
- const next = Math.min(200, Math.max(1, Number(ev.char)));
2469
+ const next = Math.min(cap, Math.max(1, Number(ev.char)));
2455
2470
  if (next !== cfg.maxIterations) {
2456
2471
  cfg.maxIterations = next;
2457
2472
  try {
@@ -2527,6 +2542,41 @@ class App {
2527
2542
  this.modal = { type: 'none' };
2528
2543
  });
2529
2544
  break;
2545
+ case 'undo': {
2546
+ const cps = modal.checkpoints;
2547
+ await this.handleSelector(modal, ev, cps.map((c) => ({
2548
+ label: `${c.time.replace('T', ' ').slice(0, 19)} — ${c.task || c.reason}`,
2549
+ value: c.ref,
2550
+ hint: c.reason,
2551
+ })), async (sel) => {
2552
+ this.modal = { type: 'none' };
2553
+ if (this.running) {
2554
+ this.addSystem('Tidak bisa restore saat agen sedang jalan — hentikan dulu (Esc).');
2555
+ return;
2556
+ }
2557
+ // Confirm: restore MENIMPA perubahan yang belum di-commit.
2558
+ this.modal = {
2559
+ type: 'confirm',
2560
+ title: 'Restore checkpoint',
2561
+ body: [
2562
+ `Kembalikan seluruh file project ke snapshot ${sel.value}?`,
2563
+ '',
2564
+ 'Perubahan yang BELUM di-commit akan tertimpa (riwayat git kamu tetap utuh —',
2565
+ 'restore hanya menyentuh working tree).',
2566
+ ],
2567
+ focused: 'cancel',
2568
+ resolve: async (ok) => {
2569
+ this.modal = { type: 'none' };
2570
+ if (!ok)
2571
+ return;
2572
+ const r = (0, checkpoints_1.restoreCheckpoint)(this.agent.cwd, sel.value);
2573
+ this.addSystem(r.ok ? `⏪ ${r.message}` : `! ${r.message}`);
2574
+ this.markDirty();
2575
+ },
2576
+ };
2577
+ });
2578
+ break;
2579
+ }
2530
2580
  case 'session': {
2531
2581
  const sessions = (0, session_1.listSessions)();
2532
2582
  await this.handleSelector(modal, ev, sessions.map((s) => ({ label: s.name, value: s.name, hint: `${s.messageCount} msgs` })), async (s) => {
@@ -3324,7 +3374,7 @@ class App {
3324
3374
  label('Mode', this.agent.config.permissionMode),
3325
3375
  // LIVE-EDITABLE: +/- / ↑↓ / PgUp-PgDn step the budget, digits
3326
3376
  // 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}`),
3377
+ label('Max iterations', `${this.agent.config.maxIterations} ${theme_1.THEME.muted}(+/- / ↑↓ / PgUp-PgDn / angka · cap 999)${theme_1.THEME.reset}`),
3328
3378
  label('Max retries', String(this.agent.config.maxRetries)),
3329
3379
  label('API URL', this.agent.config.apiUrl || '(default)'),
3330
3380
  label('Key configured', (0, credentials_1.hasApiKey)(this.agent.config.provider) ? 'yes' : 'no'),
@@ -3414,6 +3464,22 @@ class App {
3414
3464
  cols,
3415
3465
  });
3416
3466
  }
3467
+ case 'undo': {
3468
+ return (0, components_1.renderSelector)({
3469
+ title: 'Restore checkpoint',
3470
+ options: m.checkpoints.length > 0
3471
+ ? m.checkpoints.map((c) => ({
3472
+ label: `${c.time.replace('T', ' ').slice(0, 19)} — ${c.task || c.reason}`,
3473
+ value: c.ref,
3474
+ hint: c.reason,
3475
+ }))
3476
+ : [{ label: '(no checkpoints yet)', value: '' }],
3477
+ selected: m.selected,
3478
+ start: m.start,
3479
+ rows,
3480
+ cols,
3481
+ });
3482
+ }
3417
3483
  case 'theme': {
3418
3484
  const themes = (0, themes_1.listThemes)();
3419
3485
  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.28",
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": {