wholestack 0.4.0 → 0.5.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.
@@ -39,7 +39,22 @@ function renderTodos(todos) {
39
39
  }
40
40
  line();
41
41
  }
42
- function banner() {
42
+ var SHIMMER = [44, 44, 45, 80, 81, 117, 147, 176, 176, 147, 117, 81, 45, 44];
43
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
44
+ function shimmerRow(row, rowIdx, phase) {
45
+ let out = "";
46
+ for (let col = 0; col < row.length; col++) {
47
+ const ch = row[col];
48
+ if (ch === " ") {
49
+ out += ch;
50
+ continue;
51
+ }
52
+ const code = SHIMMER[(col + rowIdx + phase) % SHIMMER.length];
53
+ out += `\x1B[1;38;5;${code}m${ch}\x1B[0m`;
54
+ }
55
+ return out;
56
+ }
57
+ async function banner() {
43
58
  const art = [
44
59
  "\u2590\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u258C",
45
60
  "\u2590 .----------------. \u258C",
@@ -62,13 +77,55 @@ function banner() {
62
77
  if (i === 8) return " " + c.dim("/build for full stack apps");
63
78
  return "";
64
79
  };
80
+ const renderFinal = () => {
81
+ art.forEach((row, i) => {
82
+ const colored = zRows.has(i) ? c.bold(c.cyan(row)) : c.cyan(row);
83
+ line(" " + colored + side(i));
84
+ });
85
+ };
65
86
  line();
66
- art.forEach((row, i) => {
67
- const colored = zRows.has(i) ? c.bold(c.cyan(row)) : c.cyan(row);
68
- line(" " + colored + side(i));
69
- });
87
+ const animate = TTY && useColor && !process.env.ZETA_NO_ANIM;
88
+ if (!animate) {
89
+ renderFinal();
90
+ line();
91
+ return;
92
+ }
93
+ const FRAME_MS = 70;
94
+ const MAX_FRAMES = 900;
95
+ const stdin2 = process.stdin;
96
+ let stop = false;
97
+ const onKey = () => {
98
+ stop = true;
99
+ };
100
+ const rawCapable = typeof stdin2.setRawMode === "function";
101
+ if (rawCapable) stdin2.setRawMode(true);
102
+ stdin2.resume();
103
+ stdin2.once("data", onKey);
104
+ process.stdout.write("\x1B[?25l");
105
+ try {
106
+ for (let f = 0; f < MAX_FRAMES && !stop; f++) {
107
+ if (f > 0) process.stdout.write(`\x1B[${art.length}A`);
108
+ art.forEach((row, i) => {
109
+ process.stdout.write("\r " + shimmerRow(row, i, f) + "\x1B[K\n");
110
+ });
111
+ await sleep(FRAME_MS);
112
+ }
113
+ process.stdout.write(`\x1B[${art.length}A`);
114
+ art.forEach((row, i) => {
115
+ const colored = zRows.has(i) ? c.bold(c.cyan(row)) : c.cyan(row);
116
+ process.stdout.write("\r " + colored + side(i) + "\x1B[K\n");
117
+ });
118
+ } finally {
119
+ process.stdout.write("\x1B[?25h");
120
+ stdin2.removeListener("data", onKey);
121
+ if (rawCapable) stdin2.setRawMode(false);
122
+ stdin2.pause();
123
+ }
70
124
  line();
71
125
  }
126
+ function bell() {
127
+ if (TTY && !process.env.ZETA_NO_BELL) process.stderr.write("\x07");
128
+ }
72
129
  function fmtTokens(n) {
73
130
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
74
131
  if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
@@ -112,26 +169,6 @@ function wrapPlain(text, width) {
112
169
  }
113
170
  return out;
114
171
  }
115
- function responseBox(text, title = "Zeta-G1.0") {
116
- const inner = boxInnerWidth();
117
- const textW = inner - 2;
118
- const rows = wrapPlain(text, textW);
119
- const label = ` ${title} `;
120
- const rem = Math.max(0, inner - label.length);
121
- const lft = Math.floor(rem / 2);
122
- const top = "\u256D" + "\u2500".repeat(lft) + label + "\u2500".repeat(rem - lft) + "\u256E";
123
- const blank = "\u2502" + " ".repeat(inner) + "\u2502";
124
- const bottom = "\u2570" + "\u2500".repeat(inner) + "\u256F";
125
- line();
126
- line(" " + c.cyan(top));
127
- line(" " + c.cyan(blank));
128
- for (const r of rows) {
129
- line(" " + c.cyan("\u2502") + " " + r.padEnd(textW) + " " + c.cyan("\u2502"));
130
- }
131
- line(" " + c.cyan(blank));
132
- line(" " + c.cyan(bottom));
133
- line();
134
- }
135
172
  function userBox(text) {
136
173
  const inner = boxInnerWidth();
137
174
  const textW = inner - 2;
@@ -158,23 +195,166 @@ function reasoningHeader() {
158
195
  line();
159
196
  line(" " + c.magenta("\u273B ") + c.dim(c.italic("thinking")));
160
197
  }
161
- var Spinner = class _Spinner {
162
- static frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
163
- i = 0;
198
+ var SWIPE = [
199
+ 238,
200
+ 239,
201
+ 240,
202
+ 242,
203
+ 244,
204
+ 246,
205
+ 248,
206
+ 250,
207
+ 252,
208
+ 254,
209
+ 255,
210
+ 87,
211
+ 51,
212
+ 45,
213
+ 44,
214
+ 45,
215
+ 51,
216
+ 87,
217
+ 255,
218
+ 254,
219
+ 252,
220
+ 250,
221
+ 248,
222
+ 246,
223
+ 244,
224
+ 242,
225
+ 240,
226
+ 239
227
+ ];
228
+ var TRUECOLOR = /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
229
+ function swipeRgb(t) {
230
+ const stops = [
231
+ [60, 64, 74],
232
+ [0, 200, 212],
233
+ [235, 242, 255],
234
+ [0, 200, 212],
235
+ [60, 64, 74]
236
+ ];
237
+ const x = t % 1 * (stops.length - 1);
238
+ const i = Math.floor(x);
239
+ const f = x - i;
240
+ const a = stops[i];
241
+ const b = stops[Math.min(i + 1, stops.length - 1)];
242
+ return [
243
+ Math.round(a[0] + (b[0] - a[0]) * f),
244
+ Math.round(a[1] + (b[1] - a[1]) * f),
245
+ Math.round(a[2] + (b[2] - a[2]) * f)
246
+ ];
247
+ }
248
+ function swipeAnsi(p) {
249
+ if (TRUECOLOR) {
250
+ const [r, g, b] = swipeRgb((p % SWIPE.length + SWIPE.length) % SWIPE.length / SWIPE.length);
251
+ return `\x1B[38;2;${r};${g};${b}m`;
252
+ }
253
+ return `\x1B[38;5;${SWIPE[(p % SWIPE.length + SWIPE.length) % SWIPE.length]}m`;
254
+ }
255
+ function shimmerText(text, phase) {
256
+ if (!useColor) return text;
257
+ let out = "";
258
+ for (let i = 0; i < text.length; i++) {
259
+ const ch = text[i];
260
+ if (ch === " ") {
261
+ out += ch;
262
+ continue;
263
+ }
264
+ out += swipeAnsi(i + phase) + ch + "\x1B[0m";
265
+ }
266
+ return out;
267
+ }
268
+ var THINKING_WORDS = [
269
+ "thinking",
270
+ "cooking",
271
+ "vibin",
272
+ "whippin",
273
+ "stirring the pot",
274
+ "noodlin",
275
+ "scheming",
276
+ "wiring it up",
277
+ "summoning",
278
+ "locking in",
279
+ "manifesting",
280
+ "crunching",
281
+ "galaxy braining",
282
+ "percolating",
283
+ "marinating",
284
+ "conjuring",
285
+ "untangling",
286
+ "threading the needle",
287
+ "herding electrons",
288
+ "spelunking the stack",
289
+ "chiseling",
290
+ "brewing",
291
+ "plotting",
292
+ "tinkering",
293
+ "compiling thoughts",
294
+ "doing the thing"
295
+ ];
296
+ var THINKING_WORDS_YOLO = [
297
+ "makin babies",
298
+ "stressin",
299
+ "sending it",
300
+ "full send",
301
+ "cooking with gas",
302
+ "going feral",
303
+ "yeetin code",
304
+ "gambling",
305
+ "raw-doggin prod",
306
+ "no cap buildin",
307
+ "speedrunnin",
308
+ "touchin grass",
309
+ "zootin",
310
+ "catchin a fade",
311
+ "standin on business"
312
+ ];
313
+ function thinkingWords(yolo = false) {
314
+ return yolo ? [...THINKING_WORDS, ...THINKING_WORDS_YOLO] : THINKING_WORDS;
315
+ }
316
+ var Spinner = class {
317
+ phase = 0;
318
+ ticks = 0;
164
319
  timer = null;
165
320
  text = "";
166
- start(text) {
321
+ phrases = null;
322
+ phraseIdx = 0;
323
+ hint = "";
324
+ startedAt = 0;
325
+ /**
326
+ * @param text fixed label (used when no `phrases`)
327
+ * @param opts.phrases rotate through these instead of a fixed label
328
+ * @param opts.hint dim suffix, e.g. "esc to interrupt"
329
+ */
330
+ start(text, opts = {}) {
167
331
  if (!TTY) return;
168
332
  this.text = text;
169
- this.timer = setInterval(() => this.tick(), 90);
333
+ this.phrases = opts.phrases && opts.phrases.length ? opts.phrases : null;
334
+ this.hint = opts.hint ?? "";
335
+ this.phraseIdx = this.phrases ? Date.now() % this.phrases.length : 0;
336
+ this.startedAt = Date.now();
337
+ this.timer = setInterval(() => this.tick(), 80);
170
338
  this.tick();
171
339
  }
172
340
  update(text) {
173
341
  this.text = text;
174
342
  }
343
+ label() {
344
+ if (this.phrases) return this.phrases[this.phraseIdx % this.phrases.length] + "\u2026";
345
+ return this.text;
346
+ }
175
347
  tick() {
176
- const frame = _Spinner.frames[this.i = (this.i + 1) % _Spinner.frames.length];
177
- process.stderr.write(`\r ${c.cyan(frame)} ${c.dim(this.text)}\x1B[K`);
348
+ this.ticks++;
349
+ if (this.phrases && this.ticks % 30 === 0) {
350
+ this.phraseIdx = (this.phraseIdx + 1) % this.phrases.length;
351
+ }
352
+ this.phase = (this.phase + SWIPE.length - 1) % SWIPE.length;
353
+ const dot = swipeAnsi(this.phase) + "\u2726\x1B[0m";
354
+ const secs = Math.floor((Date.now() - this.startedAt) / 1e3);
355
+ const elapsed = secs >= 1 ? c.dim(` ${secs}s`) : "";
356
+ const tail2 = this.hint ? " " + c.dim(`(${this.hint})`) : "";
357
+ process.stderr.write(`\r ${dot} ${shimmerText(this.label(), this.phase)}${elapsed}${tail2}\x1B[K`);
178
358
  }
179
359
  stop() {
180
360
  if (this.timer) {
@@ -721,10 +901,111 @@ function tail(s) {
721
901
  return s.slice(-4e3);
722
902
  }
723
903
 
904
+ // src/tasks.ts
905
+ import { spawn as spawn4 } from "child_process";
906
+ var MAX_OUTPUT_CHARS = 64 * 1024;
907
+ var TaskManager = class {
908
+ seq = 0;
909
+ map = /* @__PURE__ */ new Map();
910
+ /** Start a detached-from-the-REPL background command. Returns the task id. */
911
+ start(command, args, cwd) {
912
+ const id = `t${++this.seq}`;
913
+ const display = [command, ...args].join(" ");
914
+ const child = spawn4(command, args, {
915
+ cwd,
916
+ stdio: ["ignore", "pipe", "pipe"],
917
+ // Own process group so we can signal the whole tree on kill.
918
+ detached: false
919
+ });
920
+ const task = {
921
+ id,
922
+ display,
923
+ status: "running",
924
+ exitCode: null,
925
+ startedAt: Date.now(),
926
+ endedAt: null,
927
+ announced: false,
928
+ child,
929
+ buffer: ""
930
+ };
931
+ const append = (chunk) => {
932
+ task.buffer += chunk.toString();
933
+ if (task.buffer.length > MAX_OUTPUT_CHARS) {
934
+ task.buffer = task.buffer.slice(task.buffer.length - MAX_OUTPUT_CHARS);
935
+ }
936
+ };
937
+ child.stdout?.on("data", append);
938
+ child.stderr?.on("data", append);
939
+ child.on("error", (e) => {
940
+ task.buffer += `
941
+ [spawn error] ${e.message}
942
+ `;
943
+ task.status = "failed";
944
+ task.exitCode = task.exitCode ?? -1;
945
+ task.endedAt = Date.now();
946
+ });
947
+ child.on("close", (code) => {
948
+ task.exitCode = code;
949
+ task.status = code === 0 ? "exited" : "failed";
950
+ task.endedAt = Date.now();
951
+ });
952
+ this.map.set(id, task);
953
+ return this.toRecord(task);
954
+ }
955
+ toRecord(t) {
956
+ const { child: _child, buffer: _buffer, ...rec } = t;
957
+ return { ...rec };
958
+ }
959
+ get(id) {
960
+ const t = this.map.get(id);
961
+ if (!t) return null;
962
+ return { record: this.toRecord(t), output: t.buffer };
963
+ }
964
+ list() {
965
+ return [...this.map.values()].map((t) => this.toRecord(t));
966
+ }
967
+ /** Tasks that finished since the last call and haven't been announced yet. */
968
+ drainCompleted() {
969
+ const done = [];
970
+ for (const t of this.map.values()) {
971
+ if (t.status !== "running" && !t.announced) {
972
+ t.announced = true;
973
+ done.push(this.toRecord(t));
974
+ }
975
+ }
976
+ return done;
977
+ }
978
+ kill(id) {
979
+ const t = this.map.get(id);
980
+ if (!t) return false;
981
+ if (t.status === "running") {
982
+ try {
983
+ t.child.kill("SIGTERM");
984
+ } catch {
985
+ }
986
+ }
987
+ return true;
988
+ }
989
+ killAll() {
990
+ for (const t of this.map.values()) {
991
+ if (t.status === "running") {
992
+ try {
993
+ t.child.kill("SIGTERM");
994
+ } catch {
995
+ }
996
+ }
997
+ }
998
+ }
999
+ runtimeSeconds(t) {
1000
+ return Math.round(((t.endedAt ?? Date.now()) - t.startedAt) / 1e3);
1001
+ }
1002
+ };
1003
+ var tasks = new TaskManager();
1004
+
724
1005
  // src/tools.ts
725
1006
  import { tool } from "ai";
726
1007
  import { z } from "zod";
727
- import { spawn as spawn4 } from "child_process";
1008
+ import { spawn as spawn5 } from "child_process";
728
1009
  import { readFile as readFile2, writeFile, mkdir, readdir, stat } from "fs/promises";
729
1010
  import { resolve as resolve2, dirname as dirname2, relative, join as join4, sep as sep2 } from "path";
730
1011
  import fg from "fast-glob";
@@ -739,7 +1020,7 @@ function inside(ctx, p) {
739
1020
  }
740
1021
  function runShell(cmd, args, opts) {
741
1022
  return new Promise((res) => {
742
- const child = spawn4(cmd, args, { cwd: opts.cwd, shell: false });
1023
+ const child = spawn5(cmd, args, { cwd: opts.cwd, shell: false });
743
1024
  let out = "";
744
1025
  const timer = setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs);
745
1026
  const onAbort = () => child.kill("SIGKILL");
@@ -772,7 +1053,7 @@ async function search(ctx, pattern, searchPath, globPat, ignoreCase, maxResults,
772
1053
  if (globPat) args.push("--glob", globPat);
773
1054
  for (const ig of IGNORE) args.push("--glob", "!" + ig);
774
1055
  args.push("-e", pattern, abs);
775
- const child = spawn4("rg", args, { cwd: ctx.cwd });
1056
+ const child = spawn5("rg", args, { cwd: ctx.cwd });
776
1057
  const onAbort = () => child.kill("SIGKILL");
777
1058
  signal?.addEventListener("abort", onAbort, { once: true });
778
1059
  let out = "";
@@ -1237,19 +1518,77 @@ function buildTools(ctx) {
1237
1518
  }
1238
1519
  return { ok: false, error: r.error, log: r.log };
1239
1520
  }
1521
+ }),
1522
+ run_background: tool({
1523
+ description: "Start a long-running command in the BACKGROUND (builds, test watchers, long scripts) and keep working. Returns a task id immediately \u2014 DO NOT wait. Poll it later with task_output, or just continue; finished tasks are reported back automatically. Use this instead of run_command when a job takes more than a few seconds or should run while you do other work. Gated by the permission layer like run_command.",
1524
+ inputSchema: z.object({
1525
+ command: z.string().describe("The executable, e.g. pnpm, npm, forge, node, vitest."),
1526
+ args: z.array(z.string()).default([])
1527
+ }),
1528
+ execute: async ({ command, args }) => {
1529
+ const display = [command, ...args].join(" ");
1530
+ if (await permissions.requestCommand(`(background) ${display}`) === "deny") {
1531
+ return {
1532
+ ok: false,
1533
+ declined: true,
1534
+ error: permissions.isPlan() ? permissions.planRefusal() : `declined: \`${display}\` \u2014 approve it, switch /mode, or re-run with --yes.`
1535
+ };
1536
+ }
1537
+ const rec = tasks.start(command, args, ctx.cwd);
1538
+ toolLine("run_background", c.dim(`${rec.id} \xB7 ${display}`));
1539
+ return {
1540
+ ok: true,
1541
+ taskId: rec.id,
1542
+ status: rec.status,
1543
+ note: `started in background as ${rec.id}; poll with task_output or keep working`
1544
+ };
1545
+ }
1546
+ }),
1547
+ task_output: tool({
1548
+ description: "Check a background task started with run_background: its status (running|exited|failed), exit code, runtime, and latest output. Pass no id to list all tasks. Use after starting a background job to see whether it finished and what it produced.",
1549
+ inputSchema: z.object({
1550
+ id: z.string().optional().describe("Task id (e.g. t1). Omit to list every task."),
1551
+ tail: z.number().int().min(200).max(2e4).default(4e3).describe("Max output chars to return.")
1552
+ }),
1553
+ execute: async ({ id, tail: tail2 }) => {
1554
+ if (!id) {
1555
+ return {
1556
+ ok: true,
1557
+ tasks: tasks.list().map((t) => ({
1558
+ id: t.id,
1559
+ status: t.status,
1560
+ exitCode: t.exitCode,
1561
+ seconds: tasks.runtimeSeconds(t),
1562
+ command: t.display
1563
+ }))
1564
+ };
1565
+ }
1566
+ const found = tasks.get(id);
1567
+ if (!found) return { ok: false, error: `no task ${id}` };
1568
+ toolLine("task_output", c.dim(`${id} \xB7 ${found.record.status}`));
1569
+ return {
1570
+ ok: true,
1571
+ id,
1572
+ status: found.record.status,
1573
+ exitCode: found.record.exitCode,
1574
+ seconds: tasks.runtimeSeconds(found.record),
1575
+ command: found.record.display,
1576
+ output: found.output.slice(-tail2)
1577
+ };
1578
+ }
1240
1579
  })
1241
1580
  };
1242
1581
  }
1243
1582
 
1244
1583
  // src/hooks.ts
1245
- import { spawn as spawn5 } from "child_process";
1584
+ import { spawn as spawn6 } from "child_process";
1246
1585
  import { readFileSync, existsSync as existsSync4 } from "fs";
1247
1586
  import { homedir } from "os";
1248
1587
  import { join as join5 } from "path";
1249
1588
  var HOOK_TIMEOUT_MS = 15e3;
1250
1589
  function runOne(def, event, payload, cwd) {
1251
1590
  return new Promise((res) => {
1252
- const child = spawn5("sh", ["-c", def.command], { cwd });
1591
+ const child = spawn6("sh", ["-c", def.command], { cwd });
1253
1592
  let stdout2 = "";
1254
1593
  let stderr = "";
1255
1594
  const timer = setTimeout(() => child.kill("SIGKILL"), HOOK_TIMEOUT_MS);
@@ -1472,10 +1811,14 @@ var OPENROUTER_URL = "https://openrouter.ai/api/v1";
1472
1811
  var CEREBRAS_KEY = "CEREBRAS_API_KEY";
1473
1812
  var KEY_ENV = "OPENROUTER_API_KEY";
1474
1813
  var DEFAULT_CUSTOM_MODEL = "anthropic/claude-sonnet-4.6";
1814
+ var VISION_MODEL = process.env.ZETA_VISION_MODEL?.trim() || "anthropic/claude-sonnet-4.6";
1475
1815
  var MODELS = /* @__PURE__ */ new Map([
1476
1816
  ["zeta-g1-lite", { modelId: "gpt-oss-120b", label: "Zeta-G1.0 Lite", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }],
1477
1817
  ["zeta-g1", { modelId: "zai-glm-4.7", label: "Zeta-G1.0", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }],
1478
- ["zeta-g1-max", { modelId: "zai-glm-4.7", label: "Zeta-G1.0 MAX", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }]
1818
+ ["zeta-g1-max", { modelId: "zai-glm-4.7", label: "Zeta-G1.0 MAX", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }],
1819
+ // The vision tier — accepts image input (screenshots, mocks, diagrams). Routed
1820
+ // over OpenRouter because the Cerebras tiers are text-only.
1821
+ ["zeta-g1-vision", { modelId: VISION_MODEL, label: "Zeta-G1.0 Vision", keyEnv: KEY_ENV, baseURL: OPENROUTER_URL, contextWindow: 2e5, thinking: null }]
1479
1822
  ]);
1480
1823
  var MODEL_KEYS = [...MODELS.keys()];
1481
1824
  function registerCustom(id) {
@@ -1510,6 +1853,9 @@ function resolveModelKey(raw) {
1510
1853
  lite: "zeta-g1-lite",
1511
1854
  max: "zeta-g1-max",
1512
1855
  pro: "zeta-g1-max",
1856
+ vision: "zeta-g1-vision",
1857
+ vis: "zeta-g1-vision",
1858
+ image: "zeta-g1-vision",
1513
1859
  // legacy convenience — resolve silently, but only the Zeta label is shown
1514
1860
  opus: "zeta-g1-max",
1515
1861
  sonnet: "zeta-g1",
@@ -1531,6 +1877,9 @@ function modelLabel(key) {
1531
1877
  function modelId(key) {
1532
1878
  return spec(key).modelId;
1533
1879
  }
1880
+ function visionCapable(key) {
1881
+ return key === "zeta-g1-vision" || key.startsWith("custom:");
1882
+ }
1534
1883
  function modelContextWindow(key) {
1535
1884
  return spec(key).contextWindow;
1536
1885
  }
@@ -1747,6 +2096,35 @@ var ROLE_PLANNER = [
1747
2096
  "name the files and seams involved, and call out unknowns and risks up front.",
1748
2097
  "Propose the smallest plan that fully covers the goal."
1749
2098
  ].join("\n");
2099
+ var ROLE_NZT_48 = [
2100
+ "ROLE \u2014 NZT-48 (web3 special operations):",
2101
+ "You are NZT-48 for this session: a web3 special-ops agent \u2014 sharper than an",
2102
+ "arrow, precise and tactical with every move. Voice: precise, tactical,",
2103
+ "economical. State the objective, the move, and the evidence \u2014 nothing else.",
2104
+ "",
2105
+ "Drive the CLI's web3 commands (audit / prove / verify) and the existing",
2106
+ "Foundry \xB7 Slither \xB7 Halmos \xB7 firewall harness \u2014 do not reinvent the prover.",
2107
+ "",
2108
+ "FOUR MISSIONS \u2014 run the one the request calls for, end to end:",
2109
+ "1. AUDIT & PROVE \u2014 discover every contract and ALL candidate invariants (never",
2110
+ " just the first); prove each with the real harness; fold verdicts honestly",
2111
+ " (proven / refuted / degraded); emit the signed bundle. Degraded is NOT a",
2112
+ " pass.",
2113
+ "2. EXPLOIT RECON (defensive) \u2014 map the attack surface (reentrancy, access",
2114
+ " control, overflow, oracle/price manipulation, unchecked calls, value",
2115
+ " extraction, proxy traps). Report severity + location + exploit path + fix.",
2116
+ " Hunt weaknesses to CLOSE them; never weaponize.",
2117
+ "3. BUILD & SHIP \u2014 idea \u2192 ISL \u2192 Solidity \u2192 proven \u2192 deploy-ready, inside the",
2118
+ " deterministic provable fragment. Run forge build/test, Slither, Halmos.",
2119
+ " Never weaken a gate to force green \u2014 self-heal the spec instead.",
2120
+ "4. MONITOR & RESPOND \u2014 re-prove only changed contracts on .sol PRs; report",
2121
+ " SHIP / NO_SHIP; surface anomalies with evidence, never act destructively.",
2122
+ "",
2123
+ "RULES OF ENGAGEMENT: truth over optics (a refuted invariant honestly reported",
2124
+ "is a win; fake-success is the one unforgivable error). One contract per proof",
2125
+ "file; honor non-vacuity. Defensive only \u2014 refuse mass targeting, live attacks,",
2126
+ "or anything outside an authorized engagement."
2127
+ ].join("\n");
1750
2128
  var DEFAULT_REGISTRY = (() => {
1751
2129
  const r = new PromptRegistry();
1752
2130
  r.registerSystem("zeta-g", ZETA_G_SYSTEM);
@@ -1754,6 +2132,7 @@ var DEFAULT_REGISTRY = (() => {
1754
2132
  r.registerOverlay({ id: "verify", body: VERIFY_OVERLAY });
1755
2133
  r.registerRole({ role: "coder", body: ROLE_CODER });
1756
2134
  r.registerRole({ role: "planner", body: ROLE_PLANNER });
2135
+ r.registerRole({ role: "nzt-48", body: ROLE_NZT_48 });
1757
2136
  return r;
1758
2137
  })();
1759
2138
 
@@ -2377,6 +2756,208 @@ import {
2377
2756
  streamText,
2378
2757
  stepCountIs
2379
2758
  } from "ai";
2759
+
2760
+ // src/markdown.ts
2761
+ var useColor2 = process.stdout.isTTY && !process.env.NO_COLOR;
2762
+ var truecolor = /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
2763
+ function fg2(r, g, b, c256) {
2764
+ if (!useColor2) return "";
2765
+ return truecolor ? `\x1B[38;2;${r};${g};${b}m` : `\x1B[38;5;${c256}m`;
2766
+ }
2767
+ var RESET = useColor2 ? "\x1B[0m" : "";
2768
+ var SY = {
2769
+ keyword: fg2(198, 120, 221, 176),
2770
+ // violet
2771
+ string: fg2(152, 195, 121, 114),
2772
+ // green
2773
+ number: fg2(209, 154, 102, 173),
2774
+ // orange
2775
+ comment: fg2(106, 115, 125, 244),
2776
+ // gray
2777
+ fn: fg2(97, 175, 239, 75),
2778
+ // blue
2779
+ punct: fg2(160, 168, 180, 247),
2780
+ type: fg2(229, 192, 123, 180)
2781
+ // yellow
2782
+ };
2783
+ var KEYWORDS = /\b(const|let|var|function|return|if|else|for|while|switch|case|break|continue|new|class|extends|import|export|from|default|async|await|try|catch|finally|throw|typeof|instanceof|in|of|this|super|yield|static|public|private|protected|readonly|interface|type|enum|namespace|implements|as|void|null|undefined|true|false|def|elif|lambda|pass|with|fn|let|mut|pub|use|struct|impl|match|where|select|insert|update|delete|create|table|primary|key|foreign|references|contract|mapping|address|uint256|require|emit|modifier|memory|storage|payable)\b/g;
2784
+ function highlightLine(src, _lang) {
2785
+ if (!useColor2) return src;
2786
+ const cm = src.match(/^(\s*)(\/\/.*|#.*)$/);
2787
+ if (cm) return cm[1] + SY.comment + cm[2] + RESET;
2788
+ let out = "";
2789
+ let i = 0;
2790
+ while (i < src.length) {
2791
+ const ch = src[i];
2792
+ if (ch === '"' || ch === "'" || ch === "`") {
2793
+ let j = i + 1;
2794
+ while (j < src.length && src[j] !== ch) {
2795
+ if (src[j] === "\\") j++;
2796
+ j++;
2797
+ }
2798
+ out += SY.string + src.slice(i, Math.min(j + 1, src.length)) + RESET;
2799
+ i = j + 1;
2800
+ continue;
2801
+ }
2802
+ if (/[0-9]/.test(ch) && (i === 0 || /[^A-Za-z0-9_]/.test(src[i - 1]))) {
2803
+ let j = i;
2804
+ while (j < src.length && /[0-9a-fx._]/i.test(src[j])) j++;
2805
+ out += SY.number + src.slice(i, j) + RESET;
2806
+ i = j;
2807
+ continue;
2808
+ }
2809
+ if (/[A-Za-z_$]/.test(ch)) {
2810
+ let j = i;
2811
+ while (j < src.length && /[A-Za-z0-9_$]/.test(src[j])) j++;
2812
+ const word = src.slice(i, j);
2813
+ KEYWORDS.lastIndex = 0;
2814
+ if (KEYWORDS.test(word)) out += SY.keyword + word + RESET;
2815
+ else if (src[j] === "(") out += SY.fn + word + RESET;
2816
+ else if (/^[A-Z]/.test(word)) out += SY.type + word + RESET;
2817
+ else out += word;
2818
+ i = j;
2819
+ continue;
2820
+ }
2821
+ out += ch;
2822
+ i++;
2823
+ }
2824
+ return out;
2825
+ }
2826
+ function inline(s) {
2827
+ if (!useColor2) return s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
2828
+ let out = s;
2829
+ out = out.replace(/`([^`]+)`/g, (_m, code) => fg2(229, 192, 123, 180) + code + RESET);
2830
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_m, t) => "\x1B[1m" + t + "\x1B[22m");
2831
+ out = out.replace(/(^|[^*])\*([^*]+)\*/g, (_m, p, t) => p + "\x1B[3m" + t + "\x1B[23m");
2832
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => "\x1B[4m" + t + "\x1B[24m" + c.dim(` (${u})`));
2833
+ return out;
2834
+ }
2835
+ function renderMarkdown(md, width = 76) {
2836
+ const lines = md.replace(/\r/g, "").split("\n");
2837
+ const out = [];
2838
+ let inCode = false;
2839
+ let codeLang = "";
2840
+ for (let li = 0; li < lines.length; li++) {
2841
+ const raw = lines[li];
2842
+ if (!inCode && raw.includes("|") && li + 1 < lines.length && /^\s*\|?[\s:|-]*-{2,}[\s:|-]*\|?\s*$/.test(lines[li + 1]) && lines[li + 1].includes("-")) {
2843
+ const header = splitTableRow(raw);
2844
+ li++;
2845
+ const body = [];
2846
+ while (li + 1 < lines.length && lines[li + 1].includes("|") && lines[li + 1].trim()) {
2847
+ li++;
2848
+ body.push(splitTableRow(lines[li]));
2849
+ }
2850
+ for (const tl of renderTable(header, body, width)) out.push(tl);
2851
+ continue;
2852
+ }
2853
+ const fence = raw.match(/^\s*```(\w*)\s*$/);
2854
+ if (fence) {
2855
+ if (!inCode) {
2856
+ inCode = true;
2857
+ codeLang = fence[1] || "";
2858
+ out.push(c.dim(" \u250C" + (codeLang ? ` ${codeLang} ` : "\u2500").padEnd(Math.min(width, 40), "\u2500")));
2859
+ } else {
2860
+ inCode = false;
2861
+ out.push(c.dim(" \u2514" + "\u2500".repeat(Math.min(width, 38))));
2862
+ }
2863
+ continue;
2864
+ }
2865
+ if (inCode) {
2866
+ out.push(c.dim(" \u2502 ") + highlightLine(raw, codeLang));
2867
+ continue;
2868
+ }
2869
+ const h = raw.match(/^(#{1,6})\s+(.*)$/);
2870
+ if (h) {
2871
+ const txt = inline(h[2]);
2872
+ out.push("");
2873
+ out.push(" " + (useColor2 ? "\x1B[1m" : "") + (h[1].length <= 2 ? c.cyan(txt) : txt) + RESET);
2874
+ continue;
2875
+ }
2876
+ if (/^\s*([-*_])\1{2,}\s*$/.test(raw)) {
2877
+ out.push(" " + c.dim("\u2500".repeat(Math.min(width, 40))));
2878
+ continue;
2879
+ }
2880
+ const q = raw.match(/^\s*>\s?(.*)$/);
2881
+ if (q) {
2882
+ out.push(" " + c.dim("\u258F ") + c.italic(inline(q[1])));
2883
+ continue;
2884
+ }
2885
+ const ul = raw.match(/^(\s*)[-*+]\s+(.*)$/);
2886
+ const ol = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
2887
+ if (ul) {
2888
+ out.push(" " + ul[1] + c.cyan("\u2022 ") + inline(ul[2]));
2889
+ continue;
2890
+ }
2891
+ if (ol) {
2892
+ out.push(" " + ol[1] + c.cyan(ol[2] + ". ") + inline(ol[3]));
2893
+ continue;
2894
+ }
2895
+ if (!raw.trim()) {
2896
+ out.push("");
2897
+ continue;
2898
+ }
2899
+ for (const w of wrap2(inline(raw), width)) out.push(" " + w);
2900
+ }
2901
+ return out;
2902
+ }
2903
+ function vlen(s) {
2904
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
2905
+ }
2906
+ function splitTableRow(row) {
2907
+ let s = row.trim();
2908
+ if (s.startsWith("|")) s = s.slice(1);
2909
+ if (s.endsWith("|")) s = s.slice(0, -1);
2910
+ return s.split("|").map((cell) => inline(cell.trim()));
2911
+ }
2912
+ function renderTable(header, body, width) {
2913
+ const cols = Math.max(header.length, ...body.map((r) => r.length));
2914
+ const rows = [header, ...body].map((r) => {
2915
+ const cp = r.slice();
2916
+ while (cp.length < cols) cp.push("");
2917
+ return cp;
2918
+ });
2919
+ let w = Array.from({ length: cols }, (_, ci) => Math.max(...rows.map((r) => vlen(r[ci]))));
2920
+ const overhead = cols * 3 + 1;
2921
+ const budget = Math.max(cols * 3, width - overhead);
2922
+ const total = w.reduce((a, b) => a + b, 0);
2923
+ if (total > budget) w = w.map((x) => Math.max(3, Math.floor(x / total * budget)));
2924
+ const pad = (s, n) => {
2925
+ const v = vlen(s);
2926
+ if (v > n) {
2927
+ const plain = s.replace(/\x1b\[[0-9;]*m/g, "");
2928
+ return plain.slice(0, Math.max(0, n - 1)) + "\u2026";
2929
+ }
2930
+ return s + " ".repeat(n - v);
2931
+ };
2932
+ const bar = (l, m, r) => c.dim(" " + l + w.map((n) => "\u2500".repeat(n + 2)).join(m) + r);
2933
+ const rowLine = (r, head) => " " + c.dim("\u2502") + r.map((cell, ci) => " " + (head ? c.bold(c.cyan(pad(cell, w[ci]))) : pad(cell, w[ci])) + " ").join(c.dim("\u2502")) + c.dim("\u2502");
2934
+ const out = [bar("\u250C", "\u252C", "\u2510"), rowLine(rows[0], true), bar("\u251C", "\u253C", "\u2524")];
2935
+ for (const r of rows.slice(1)) out.push(rowLine(r, false));
2936
+ out.push(bar("\u2514", "\u2534", "\u2518"));
2937
+ return out;
2938
+ }
2939
+ function wrap2(s, width) {
2940
+ const words = s.split(/(\s+)/);
2941
+ const rows = [];
2942
+ let cur = "";
2943
+ let len = 0;
2944
+ const vlen2 = (x) => x.replace(/\x1b\[[0-9;]*m/g, "").length;
2945
+ for (const w of words) {
2946
+ const wl = vlen2(w);
2947
+ if (len + wl > width && cur) {
2948
+ rows.push(cur);
2949
+ cur = w.trimStart();
2950
+ len = vlen2(cur);
2951
+ } else {
2952
+ cur += w;
2953
+ len += wl;
2954
+ }
2955
+ }
2956
+ if (cur.trim()) rows.push(cur);
2957
+ return rows.length ? rows : [""];
2958
+ }
2959
+
2960
+ // src/agent.ts
2380
2961
  var BASE_SYSTEM = `You are Zeta-G, a terminal coding agent for the ZETA engine.
2381
2962
 
2382
2963
  ZETA turns a plain-language idea into a real, verified application: it writes
@@ -2528,6 +3109,10 @@ var Agent = class {
2528
3109
  replaceHistory(messages) {
2529
3110
  this.history = messages;
2530
3111
  }
3112
+ /** A shallow copy of the current message history (for /export, inspection). */
3113
+ snapshot() {
3114
+ return [...this.history];
3115
+ }
2531
3116
  /** % of the model's context window the current history occupies. */
2532
3117
  contextPct() {
2533
3118
  return Math.min(100, estimateTokens(this.history) / this.contextWindow * 100);
@@ -2536,7 +3121,7 @@ var Agent = class {
2536
3121
  const native = this.toolNames.filter((t) => !isMcpTool(t));
2537
3122
  const mcp = this.toolNames.filter((t) => isMcpTool(t));
2538
3123
  const blocks = [
2539
- DEFAULT_REGISTRY.composeSystem({ id: "zeta-g", overlayIds: [] }),
3124
+ DEFAULT_REGISTRY.composeSystem({ id: "zeta-g", role: this.opts.persona, overlayIds: [] }),
2540
3125
  this.policy.authorityPreamble(),
2541
3126
  `Tools available to you: ${native.join(", ")}.`
2542
3127
  ];
@@ -2582,7 +3167,7 @@ ${this.opts.memoryText}`);
2582
3167
  return false;
2583
3168
  }
2584
3169
  /** Run one user turn end-to-end; streams to stdout, updates history. */
2585
- async send(userInput, signal) {
3170
+ async send(userInput, signal, images) {
2586
3171
  let input = userInput;
2587
3172
  if (this.opts.hooks?.has("UserPromptSubmit")) {
2588
3173
  const outcome = await this.opts.hooks.run("UserPromptSubmit", { prompt: userInput });
@@ -2598,11 +3183,22 @@ ${outcome.context.join("\n")}`;
2598
3183
  }
2599
3184
  }
2600
3185
  this.session?.appendUser(userInput);
2601
- this.history.push({ role: "user", content: input });
3186
+ if (images && images.length > 0) {
3187
+ this.history.push({
3188
+ role: "user",
3189
+ content: [
3190
+ { type: "text", text: input },
3191
+ ...images.map((img) => ({ type: "image", image: img.dataUrl }))
3192
+ ]
3193
+ });
3194
+ } else {
3195
+ this.history.push({ role: "user", content: input });
3196
+ }
2602
3197
  await this.maybeCompact();
2603
3198
  const providerOptions = buildProviderOptions(this.modelKey, this.thinking);
3199
+ const turnStart = Date.now();
2604
3200
  const spinner = new Spinner();
2605
- spinner.start("working\u2026 " + c.dim("esc to interrupt"));
3201
+ spinner.start("", { phrases: thinkingWords(this.opts.yolo), hint: "esc to interrupt" });
2606
3202
  let spinning = true;
2607
3203
  const stopSpin = () => {
2608
3204
  if (spinning) {
@@ -2623,11 +3219,21 @@ ${outcome.context.join("\n")}`;
2623
3219
  let aborted = false;
2624
3220
  let textBuf = "";
2625
3221
  let genFirstAt = 0;
3222
+ const toolStart = /* @__PURE__ */ new Map();
2626
3223
  const flushText = () => {
2627
3224
  const t = textBuf.trim();
2628
3225
  textBuf = "";
2629
3226
  phase = "none";
2630
- if (t) responseBox(t);
3227
+ if (!t) return;
3228
+ const inner = boxInnerWidth();
3229
+ const label = " Zeta-G1.0 ";
3230
+ const rem = Math.max(0, inner - label.length);
3231
+ const lft = Math.floor(rem / 2);
3232
+ line();
3233
+ line(" " + c.cyan("\u256D" + "\u2500".repeat(lft) + label + "\u2500".repeat(rem - lft) + "\u256E"));
3234
+ for (const ln of renderMarkdown(t, inner - 2)) line(ln);
3235
+ line(" " + c.cyan("\u2570" + "\u2500".repeat(inner) + "\u256F"));
3236
+ line();
2631
3237
  };
2632
3238
  try {
2633
3239
  for await (const part of result.fullStream) {
@@ -2658,7 +3264,7 @@ ${outcome.context.join("\n")}`;
2658
3264
  }
2659
3265
  case "start-step":
2660
3266
  if (phase !== "none" && !spinning) {
2661
- spinner.start("working\u2026 " + c.dim("esc to interrupt"));
3267
+ spinner.start("", { phrases: thinkingWords(this.opts.yolo), hint: "esc to interrupt" });
2662
3268
  spinning = true;
2663
3269
  }
2664
3270
  break;
@@ -2670,7 +3276,20 @@ ${outcome.context.join("\n")}`;
2670
3276
  line();
2671
3277
  phase = "none";
2672
3278
  }
3279
+ {
3280
+ const id = part.toolCallId;
3281
+ if (id) toolStart.set(id, Date.now());
3282
+ }
3283
+ break;
3284
+ case "tool-result": {
3285
+ const id = part.toolCallId;
3286
+ const nm = String(part.toolName ?? "tool");
3287
+ const started = id ? toolStart.get(id) : void 0;
3288
+ const ms = started ? Date.now() - started : 0;
3289
+ if (ms >= 300) line(c.dim(` \u2713 ${nm} ${(ms / 1e3).toFixed(1)}s`));
3290
+ if (id) toolStart.delete(id);
2673
3291
  break;
3292
+ }
2674
3293
  case "tool-error": {
2675
3294
  stopSpin();
2676
3295
  if (phase === "text") flushText();
@@ -2756,6 +3375,7 @@ ${outcome.context.join("\n")}`;
2756
3375
  } catch {
2757
3376
  }
2758
3377
  if (!aborted) await this.maybeCompact();
3378
+ if (!aborted && Date.now() - turnStart > 8e3) bell();
2759
3379
  return { aborted, usage };
2760
3380
  }
2761
3381
  };
@@ -2773,12 +3393,14 @@ function isPermissionMode(v) {
2773
3393
  }
2774
3394
  var PLAN_REFUSAL = "refused: plan mode is read-only \u2014 present a plan for approval, don't act. The user can switch with /mode default or approve with /approve.";
2775
3395
  var Permissions = class {
2776
- constructor(mode, confirm) {
3396
+ constructor(mode, confirm, persistMode) {
2777
3397
  this.mode = mode;
2778
3398
  this.confirm = confirm;
3399
+ this.persistMode = persistMode;
2779
3400
  }
2780
3401
  mode;
2781
3402
  confirm;
3403
+ persistMode;
2782
3404
  alwaysCommands = /* @__PURE__ */ new Set();
2783
3405
  acceptAllEdits = false;
2784
3406
  isPlan() {
@@ -2801,8 +3423,15 @@ var Permissions = class {
2801
3423
  const ans = await this.confirm(`apply this edit to ${c.bold(path)}?`, [
2802
3424
  { key: "y", label: "yes" },
2803
3425
  { key: "n", label: "no" },
2804
- { key: "a", label: "yes to all edits this session" }
3426
+ { key: "a", label: "yes to all edits this session" },
3427
+ { key: "r", label: "yes to all edits, every session (remember)" }
2805
3428
  ]);
3429
+ if (ans === "r") {
3430
+ this.acceptAllEdits = true;
3431
+ this.mode = "acceptEdits";
3432
+ this.persistMode?.("acceptEdits");
3433
+ return "allow";
3434
+ }
2806
3435
  if (ans === "a") {
2807
3436
  this.acceptAllEdits = true;
2808
3437
  return "allow";
@@ -3023,6 +3652,139 @@ var Session = class _Session {
3023
3652
  import { writeFileSync as writeFileSync2, existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
3024
3653
  import { homedir as homedir4 } from "os";
3025
3654
  import { join as join8 } from "path";
3655
+
3656
+ // src/git.ts
3657
+ import { execFileSync } from "child_process";
3658
+ function git(args, cwd) {
3659
+ return execFileSync("git", args, {
3660
+ cwd,
3661
+ encoding: "utf8",
3662
+ maxBuffer: 32 * 1024 * 1024,
3663
+ stdio: ["ignore", "pipe", "pipe"]
3664
+ }).trim();
3665
+ }
3666
+ function gitSafe(args, cwd) {
3667
+ try {
3668
+ return git(args, cwd);
3669
+ } catch {
3670
+ return null;
3671
+ }
3672
+ }
3673
+ function isRepo(cwd) {
3674
+ return gitSafe(["rev-parse", "--is-inside-work-tree"], cwd) === "true";
3675
+ }
3676
+ var SECRET_HINTS = [
3677
+ { re: /\bAKIA[0-9A-Z]{16}\b/, label: "AWS access key id" },
3678
+ { re: /\bsk-[A-Za-z0-9]{20,}\b/, label: "OpenAI-style key" },
3679
+ { re: /\bsk_live_[A-Za-z0-9]{20,}\b/, label: "Stripe live secret" },
3680
+ { re: /\bghp_[A-Za-z0-9]{30,}\b/, label: "GitHub token" },
3681
+ { re: /\bvbfl_[A-Za-z0-9]{8}_[A-Za-z0-9_-]{20,}\b/, label: "Wholestack PAT" },
3682
+ { re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, label: "private key" }
3683
+ ];
3684
+ function scanSecrets(diff) {
3685
+ const hits = /* @__PURE__ */ new Set();
3686
+ for (const { re, label } of SECRET_HINTS) {
3687
+ if (re.test(diff)) hits.add(label);
3688
+ }
3689
+ return [...hits];
3690
+ }
3691
+ var MAX_DIFF_CHARS = 12e3;
3692
+ var gitCommands = [
3693
+ {
3694
+ name: "diff",
3695
+ summary: "show working changes (/diff [--staged] [path])",
3696
+ source: "builtin",
3697
+ run: (ctx) => {
3698
+ if (!isRepo(ctx.cwd)) {
3699
+ ctx.print(" " + c.yellow("not a git repository."));
3700
+ return { type: "handled" };
3701
+ }
3702
+ const parts = ctx.args.split(/\s+/).filter(Boolean);
3703
+ const staged = parts.includes("--staged") || parts.includes("--cached");
3704
+ const paths = parts.filter((p) => p !== "--staged" && p !== "--cached");
3705
+ const args = ["--no-pager", "diff"];
3706
+ if (staged) args.push("--staged");
3707
+ if (paths.length) args.push("--", ...paths);
3708
+ const out = gitSafe(args, ctx.cwd) ?? "";
3709
+ if (!out) {
3710
+ ctx.print(" " + c.dim(staged ? "no staged changes." : "no unstaged changes."));
3711
+ return { type: "handled" };
3712
+ }
3713
+ const secrets = scanSecrets(out);
3714
+ if (secrets.length) {
3715
+ ctx.print(" " + c.red(`\u26A0 possible secret(s) in diff: ${secrets.join(", ")}`));
3716
+ }
3717
+ const shown = out.length > MAX_DIFF_CHARS ? out.slice(0, MAX_DIFF_CHARS) : out;
3718
+ ctx.print(shown);
3719
+ if (out.length > MAX_DIFF_CHARS) {
3720
+ ctx.print(" " + c.dim(`\u2026 (${out.length - MAX_DIFF_CHARS} more chars \u2014 run \`git diff\` for the rest)`));
3721
+ }
3722
+ return { type: "handled" };
3723
+ }
3724
+ },
3725
+ {
3726
+ name: "commit",
3727
+ summary: "commit changes (/commit [message] \u2014 omit to auto-write one)",
3728
+ source: "builtin",
3729
+ run: (ctx) => {
3730
+ if (!isRepo(ctx.cwd)) {
3731
+ ctx.print(" " + c.yellow("not a git repository. Run `git init` first."));
3732
+ return { type: "handled" };
3733
+ }
3734
+ const full = (gitSafe(["--no-pager", "diff", "HEAD"], ctx.cwd) ?? "") + (gitSafe(["--no-pager", "diff", "--staged"], ctx.cwd) ?? "");
3735
+ const secrets = scanSecrets(full);
3736
+ if (secrets.length) {
3737
+ ctx.print(" " + c.red(`\u2717 refusing to commit \u2014 possible secret(s): ${secrets.join(", ")}`));
3738
+ ctx.print(" " + c.dim("remove them (or add to .gitignore) and try again."));
3739
+ return { type: "handled" };
3740
+ }
3741
+ const msg = ctx.args.trim();
3742
+ if (msg) {
3743
+ try {
3744
+ git(["add", "-A"], ctx.cwd);
3745
+ const out = git(["commit", "-m", msg], ctx.cwd);
3746
+ ctx.print(" " + c.green("\u2713 committed"));
3747
+ ctx.print(" " + c.dim(out.split("\n")[0] ?? ""));
3748
+ } catch (e) {
3749
+ ctx.print(" " + c.red(e.message.split("\n")[0]));
3750
+ }
3751
+ return { type: "handled" };
3752
+ }
3753
+ const branch = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], ctx.cwd) ?? "?";
3754
+ return {
3755
+ type: "prompt",
3756
+ text: `Create a git commit for the current changes. Steps, using run_command:
3757
+ 1. \`git status --porcelain\` and \`git --no-pager diff HEAD\` to see what changed.
3758
+ 2. If nothing is staged, \`git add -A\` (but NEVER stage .env, secrets, or key files).
3759
+ 3. Write ONE Conventional Commit message: \`type(scope): summary\` (\u226472 chars), a body only if the change needs it. Base it strictly on the actual diff \u2014 no invented changes.
3760
+ 4. \`git commit -m "\u2026"\` (current branch: ${branch}). Show me the message you used.
3761
+ Do NOT push. Do NOT commit anything that looks like a credential.`
3762
+ };
3763
+ }
3764
+ },
3765
+ {
3766
+ name: "pr",
3767
+ summary: "push the branch and open a pull request (/pr [title])",
3768
+ source: "builtin",
3769
+ run: (ctx) => {
3770
+ if (!isRepo(ctx.cwd)) {
3771
+ ctx.print(" " + c.yellow("not a git repository."));
3772
+ return { type: "handled" };
3773
+ }
3774
+ const branch = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], ctx.cwd) ?? "?";
3775
+ const title = ctx.args.trim();
3776
+ return {
3777
+ type: "prompt",
3778
+ text: `Open a pull request for the current branch. Steps, using run_command:
3779
+ 1. Confirm the branch (\`${branch}\`) is not the default; if it is, create a feature branch first.
3780
+ 2. \`git push -u origin HEAD\`.
3781
+ 3. \`gh pr create\` with a clear title` + (title ? ` (use: "${title}")` : " summarizing the change") + " and a concise body listing the key changes from the commit log/diff.\nIf `gh` is not installed or not authenticated, stop and tell me the manual PR URL instead."
3782
+ };
3783
+ }
3784
+ }
3785
+ ];
3786
+
3787
+ // src/commands.ts
3026
3788
  var ZETA_MD_TEMPLATE = `# ZETA.md \u2014 project memory for Zeta-G
3027
3789
 
3028
3790
  Tell the agent how this project works. It reads this file into its system prompt.
@@ -3359,6 +4121,39 @@ var BUILTINS = [
3359
4121
  ctx.print(" " + c.dim("run ") + c.cyan("zeta-g login") + c.dim(" in your shell, then restart the session."));
3360
4122
  return { type: "handled" };
3361
4123
  }
4124
+ },
4125
+ ...gitCommands,
4126
+ {
4127
+ name: "export",
4128
+ aliases: ["save"],
4129
+ summary: "write the transcript to markdown (/export [file.md])",
4130
+ source: "builtin",
4131
+ run: (ctx) => ({ type: "export", file: ctx.args.trim() || void 0 })
4132
+ },
4133
+ {
4134
+ name: "tasks",
4135
+ aliases: ["jobs"],
4136
+ summary: "list background tasks (/tasks \xB7 /tasks kill <id>)",
4137
+ source: "builtin",
4138
+ run: (ctx) => {
4139
+ const parts = ctx.args.split(/\s+/).filter(Boolean);
4140
+ if (parts[0] === "kill" && parts[1]) {
4141
+ const ok = tasks.kill(parts[1]);
4142
+ ctx.print(" " + (ok ? c.green(`signaled ${parts[1]}`) : c.red(`no task ${parts[1]}`)));
4143
+ return { type: "handled" };
4144
+ }
4145
+ const list = tasks.list();
4146
+ if (list.length === 0) {
4147
+ ctx.print(" " + c.dim("no background tasks. The agent starts them with run_background."));
4148
+ return { type: "handled" };
4149
+ }
4150
+ for (const t of list) {
4151
+ const dot = t.status === "running" ? c.yellow("\u25CF") : t.status === "exited" ? c.green("\u2713") : c.red("\u2717");
4152
+ const code = t.exitCode == null ? "" : c.dim(` (exit ${t.exitCode})`);
4153
+ ctx.print(` ${dot} ${c.cyan(t.id)} ${c.dim(`${tasks.runtimeSeconds(t)}s`)} ${t.display}${code}`);
4154
+ }
4155
+ return { type: "handled" };
4156
+ }
3362
4157
  }
3363
4158
  ];
3364
4159
  function parseCustom(name, body, source = "custom") {
@@ -3859,8 +4654,16 @@ var InputController = class {
3859
4654
  }
3860
4655
  return [[], line2];
3861
4656
  }
3862
- /** Read one logical line, joining trailing-backslash continuations. */
4657
+ /** Read one logical line. On a TTY this renders a live bordered input box
4658
+ * (the box appears WHILE you type, not as an after-the-fact echo). On a
4659
+ * non-TTY / piped stdin it falls back to plain readline. */
3863
4660
  async readLine(promptStr) {
4661
+ if (stdin.isTTY && !process.env.NO_COLOR) {
4662
+ const text = await this.readBoxed();
4663
+ const trimmed2 = text.trim();
4664
+ if (trimmed2) this.saveHistory(trimmed2);
4665
+ return trimmed2;
4666
+ }
3864
4667
  let acc = "";
3865
4668
  let prompt = promptStr;
3866
4669
  for (; ; ) {
@@ -3877,6 +4680,230 @@ var InputController = class {
3877
4680
  if (trimmed) this.saveHistory(trimmed);
3878
4681
  return trimmed;
3879
4682
  }
4683
+ /**
4684
+ * Live bordered input editor. Redraws a 4-side box on every keystroke with
4685
+ * the caret tracked inside it. Supports: insert/backspace/delete, ←/→,
4686
+ * Home/End (Ctrl-A/E), ↑/↓ history, Tab completion (/commands + @paths),
4687
+ * Shift/Alt-Enter or a trailing "\\" for a newline, Enter to submit,
4688
+ * Ctrl-C to quit, Ctrl-U to clear, Esc to clear the line.
4689
+ */
4690
+ readBoxed() {
4691
+ const PROMPT = "\u203A ";
4692
+ const history = loadHistory();
4693
+ let histIdx = history.length;
4694
+ let draft = "";
4695
+ let buf = "";
4696
+ let cur = 0;
4697
+ let prevRows = 0;
4698
+ let F = Math.max(8, boxInnerWidth() - 2);
4699
+ let W = F + 2;
4700
+ const up = (n) => n > 0 ? `\x1B[${n}A` : "";
4701
+ const right = (n) => n > 0 ? `\x1B[${n}C` : "";
4702
+ const layout = (s) => {
4703
+ const rows = [""];
4704
+ const map = [];
4705
+ let r = 0;
4706
+ let col = 0;
4707
+ for (let i = 0; i < s.length; i++) {
4708
+ map[i] = { r, c: col };
4709
+ const ch = s[i];
4710
+ if (ch === "\n") {
4711
+ r++;
4712
+ col = 0;
4713
+ rows[r] = "";
4714
+ continue;
4715
+ }
4716
+ rows[r] += ch;
4717
+ col++;
4718
+ if (col >= F) {
4719
+ r++;
4720
+ col = 0;
4721
+ rows[r] = "";
4722
+ }
4723
+ }
4724
+ map[s.length] = { r, c: col };
4725
+ return { rows, map };
4726
+ };
4727
+ const render = () => {
4728
+ const S = PROMPT + buf;
4729
+ const { rows, map } = layout(S);
4730
+ const caret = map[PROMPT.length + cur];
4731
+ const contentRows = rows.length;
4732
+ let out = "";
4733
+ if (prevRows > 0) out += up(prevCaretR + 1) + "\r";
4734
+ out += "\x1B[J";
4735
+ out += "\r " + c.dim("\u256D" + "\u2500".repeat(W) + "\u256E") + "\n";
4736
+ rows.forEach((row, i) => {
4737
+ let cell = row;
4738
+ let body;
4739
+ if (i === 0 && cell.startsWith(PROMPT)) {
4740
+ body = c.cyan(PROMPT) + cell.slice(PROMPT.length).padEnd(F - PROMPT.length);
4741
+ } else {
4742
+ body = cell.padEnd(F);
4743
+ }
4744
+ out += "\r " + c.dim("\u2502") + " " + body + " " + c.dim("\u2502") + "\n";
4745
+ });
4746
+ out += "\r " + c.dim("\u2570" + "\u2500".repeat(W) + "\u256F");
4747
+ out += up(contentRows - caret.r) + "\r" + right(4 + caret.c);
4748
+ stdout.write(out);
4749
+ prevRows = contentRows;
4750
+ prevCaretR = caret.r;
4751
+ };
4752
+ let prevCaretR = 0;
4753
+ return new Promise((resolve3) => {
4754
+ emitKeypressEvents(stdin);
4755
+ const watching = this.activeWatch;
4756
+ if (watching) this.stopWatch();
4757
+ this.rl.pause();
4758
+ if (stdin.isTTY) stdin.setRawMode(true);
4759
+ stdin.resume();
4760
+ stdout.write("\x1B[?2004h");
4761
+ const onResize = () => {
4762
+ F = Math.max(8, boxInnerWidth() - 2);
4763
+ W = F + 2;
4764
+ prevRows = 0;
4765
+ render();
4766
+ };
4767
+ process.stdout.on("resize", onResize);
4768
+ const finish = (val) => {
4769
+ stdin.off("keypress", onKey);
4770
+ process.stdout.off("resize", onResize);
4771
+ stdout.write("\x1B[?2004l");
4772
+ if (stdin.isTTY) stdin.setRawMode(false);
4773
+ const down = prevRows - prevCaretR;
4774
+ stdout.write("\x1B[" + Math.max(1, down) + "B\r\n");
4775
+ this.rl.resume();
4776
+ if (watching) this.startWatch(watching);
4777
+ resolve3(val);
4778
+ };
4779
+ const replaceLine = (s) => {
4780
+ buf = s;
4781
+ cur = buf.length;
4782
+ };
4783
+ const onKey = (s, key) => {
4784
+ const name = key?.name;
4785
+ if (key?.ctrl && name === "c") {
4786
+ stdout.write("\n");
4787
+ this.opts.onExit();
4788
+ return;
4789
+ }
4790
+ if (key?.ctrl && name === "u") {
4791
+ buf = "";
4792
+ cur = 0;
4793
+ return render();
4794
+ }
4795
+ if (key?.ctrl && name === "w") {
4796
+ const before = buf.slice(0, cur).replace(/\s*\S+\s*$/, "");
4797
+ const after = buf.slice(cur);
4798
+ buf = before + after;
4799
+ cur = before.length;
4800
+ return render();
4801
+ }
4802
+ if (key?.ctrl && name === "k") {
4803
+ buf = buf.slice(0, cur);
4804
+ return render();
4805
+ }
4806
+ if (key?.meta && name === "left") {
4807
+ const m = buf.slice(0, cur).match(/\S+\s*$/);
4808
+ cur -= m ? m[0].length : cur;
4809
+ return render();
4810
+ }
4811
+ if (key?.meta && name === "right") {
4812
+ const m = buf.slice(cur).match(/^\s*\S+/);
4813
+ cur += m ? m[0].length : buf.length - cur;
4814
+ return render();
4815
+ }
4816
+ if (name === "return" || name === "enter") {
4817
+ if (key?.shift || key?.meta || buf.endsWith("\\")) {
4818
+ if (buf.endsWith("\\")) {
4819
+ buf = buf.slice(0, cur - 1) + "\n" + buf.slice(cur);
4820
+ } else {
4821
+ buf = buf.slice(0, cur) + "\n" + buf.slice(cur);
4822
+ }
4823
+ cur += 1;
4824
+ return render();
4825
+ }
4826
+ return finish(buf);
4827
+ }
4828
+ if (name === "backspace") {
4829
+ if (cur > 0) {
4830
+ buf = buf.slice(0, cur - 1) + buf.slice(cur);
4831
+ cur--;
4832
+ }
4833
+ return render();
4834
+ }
4835
+ if (name === "delete") {
4836
+ if (cur < buf.length) buf = buf.slice(0, cur) + buf.slice(cur + 1);
4837
+ return render();
4838
+ }
4839
+ if (name === "left") {
4840
+ if (cur > 0) cur--;
4841
+ return render();
4842
+ }
4843
+ if (name === "right") {
4844
+ if (cur < buf.length) cur++;
4845
+ return render();
4846
+ }
4847
+ if (name === "home" || key?.ctrl && name === "a") {
4848
+ cur = 0;
4849
+ return render();
4850
+ }
4851
+ if (name === "end" || key?.ctrl && name === "e") {
4852
+ cur = buf.length;
4853
+ return render();
4854
+ }
4855
+ if (name === "up") {
4856
+ if (histIdx === history.length) draft = buf;
4857
+ if (histIdx > 0) {
4858
+ histIdx--;
4859
+ replaceLine(history[histIdx]);
4860
+ }
4861
+ return render();
4862
+ }
4863
+ if (name === "down") {
4864
+ if (histIdx < history.length) {
4865
+ histIdx++;
4866
+ replaceLine(histIdx === history.length ? draft : history[histIdx]);
4867
+ }
4868
+ return render();
4869
+ }
4870
+ if (name === "escape") {
4871
+ buf = "";
4872
+ cur = 0;
4873
+ return render();
4874
+ }
4875
+ if (name === "tab") {
4876
+ const [hits] = this.complete(buf.slice(0, cur));
4877
+ if (hits.length) {
4878
+ const prefix = longestCommonPrefix(hits);
4879
+ const before = buf.slice(0, cur);
4880
+ const ws = Math.max(before.lastIndexOf(" "), before.lastIndexOf("\n"));
4881
+ const tokenStart = before.startsWith("/") && ws < 0 ? 0 : ws + 1;
4882
+ buf = buf.slice(0, tokenStart) + prefix + buf.slice(cur);
4883
+ cur = tokenStart + prefix.length;
4884
+ }
4885
+ return render();
4886
+ }
4887
+ const seq = key?.sequence ?? s ?? "";
4888
+ if (seq.includes("\x1B[200~") || seq.includes("\x1B[201~")) {
4889
+ const pasted = seq.replace(/\x1b\[20[01]~/g, "");
4890
+ buf = buf.slice(0, cur) + pasted + buf.slice(cur);
4891
+ cur += pasted.length;
4892
+ return render();
4893
+ }
4894
+ const ins = s && !key?.ctrl && !key?.meta ? s : "";
4895
+ if (ins && !ins.includes("\x1B")) {
4896
+ buf = buf.slice(0, cur) + ins + buf.slice(cur);
4897
+ cur += ins.length;
4898
+ return render();
4899
+ }
4900
+ };
4901
+ const bar = this.opts.contextBar?.();
4902
+ if (bar) stdout.write(" " + bar + "\n");
4903
+ stdin.on("keypress", onKey);
4904
+ render();
4905
+ });
4906
+ }
3880
4907
  saveHistory(entry) {
3881
4908
  try {
3882
4909
  mkdirSync3(dirname5(HISTORY_FILE), { recursive: true });
@@ -3948,6 +4975,17 @@ var InputController = class {
3948
4975
  this.rl.close();
3949
4976
  }
3950
4977
  };
4978
+ function longestCommonPrefix(items) {
4979
+ if (!items.length) return "";
4980
+ let prefix = items[0];
4981
+ for (const it of items.slice(1)) {
4982
+ let i = 0;
4983
+ while (i < prefix.length && i < it.length && prefix[i] === it[i]) i++;
4984
+ prefix = prefix.slice(0, i);
4985
+ if (!prefix) break;
4986
+ }
4987
+ return prefix;
4988
+ }
3951
4989
 
3952
4990
  export {
3953
4991
  c,
@@ -3961,6 +4999,7 @@ export {
3961
4999
  PROPERTY_KINDS,
3962
5000
  runProver,
3963
5001
  killRunningApps,
5002
+ tasks,
3964
5003
  buildTools,
3965
5004
  HookRunner,
3966
5005
  mergeHookSets,
@@ -3971,6 +5010,7 @@ export {
3971
5010
  resolveModelKey,
3972
5011
  modelLabel,
3973
5012
  modelId,
5013
+ visionCapable,
3974
5014
  modelContextWindow,
3975
5015
  supportsThinking,
3976
5016
  listModels,