wholestack 0.4.0 → 0.5.1

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);
@@ -1467,15 +1806,40 @@ ${transcript}`
1467
1806
 
1468
1807
  // src/model.ts
1469
1808
  import { createOpenAI } from "@ai-sdk/openai";
1470
- var CEREBRAS_URL = "https://api.cerebras.ai/v1";
1471
- var OPENROUTER_URL = "https://openrouter.ai/api/v1";
1472
- var CEREBRAS_KEY = "CEREBRAS_API_KEY";
1473
- var KEY_ENV = "OPENROUTER_API_KEY";
1474
- var DEFAULT_CUSTOM_MODEL = "anthropic/claude-sonnet-4.6";
1809
+
1810
+ // ../zeta-models/dist/index.js
1811
+ var readEnv = (k) => {
1812
+ const v = typeof process !== "undefined" ? process.env?.[k] : void 0;
1813
+ const t = v?.trim();
1814
+ return t && t.length > 0 ? t : void 0;
1815
+ };
1816
+ var PROVIDERS = {
1817
+ cerebras: { id: "cerebras", baseURL: "https://api.cerebras.ai/v1", keyEnv: "CEREBRAS_API_KEY" },
1818
+ openrouter: { id: "openrouter", baseURL: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY" }
1819
+ };
1820
+ var MODEL_IDS = {
1821
+ /** BUILD lane — writes ISL, generates the full stack. Cerebras. The 80%. */
1822
+ gptOss: readEnv("ZETA_GPTOSS_MODEL") || "gpt-oss-120b",
1823
+ /** BRAIN lane — frontend swarm, decisions, debugging, editing, planning. Cerebras. The 20%. */
1824
+ glm: readEnv("ZETA_GLM_MODEL") || "zai-glm-4.7",
1825
+ /** VISION lane — accepts image input. OpenRouter (Cerebras text lanes can't see). */
1826
+ vision: readEnv("ZETA_VISION_MODEL") || "anthropic/claude-sonnet-4.6"
1827
+ };
1828
+
1829
+ // src/model.ts
1830
+ var CEREBRAS_URL = PROVIDERS.cerebras.baseURL;
1831
+ var OPENROUTER_URL = PROVIDERS.openrouter.baseURL;
1832
+ var CEREBRAS_KEY = PROVIDERS.cerebras.keyEnv;
1833
+ var KEY_ENV = PROVIDERS.openrouter.keyEnv;
1834
+ var DEFAULT_CUSTOM_MODEL = MODEL_IDS.vision;
1835
+ var VISION_MODEL = MODEL_IDS.vision;
1475
1836
  var MODELS = /* @__PURE__ */ new Map([
1476
- ["zeta-g1-lite", { modelId: "gpt-oss-120b", label: "Zeta-G1.0 Lite", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }],
1477
- ["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" }]
1837
+ ["zeta-g1-lite", { modelId: MODEL_IDS.gptOss, label: "Zeta-G1.0 Lite", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }],
1838
+ ["zeta-g1", { modelId: MODEL_IDS.glm, label: "Zeta-G1.0", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }],
1839
+ ["zeta-g1-max", { modelId: MODEL_IDS.glm, label: "Zeta-G1.0 MAX", keyEnv: CEREBRAS_KEY, baseURL: CEREBRAS_URL, contextWindow: 128e3, thinking: "budget" }],
1840
+ // The vision tier — accepts image input (screenshots, mocks, diagrams). Routed
1841
+ // over OpenRouter because the Cerebras tiers are text-only.
1842
+ ["zeta-g1-vision", { modelId: VISION_MODEL, label: "Zeta-G1.0 Vision", keyEnv: KEY_ENV, baseURL: OPENROUTER_URL, contextWindow: 2e5, thinking: null }]
1479
1843
  ]);
1480
1844
  var MODEL_KEYS = [...MODELS.keys()];
1481
1845
  function registerCustom(id) {
@@ -1510,6 +1874,9 @@ function resolveModelKey(raw) {
1510
1874
  lite: "zeta-g1-lite",
1511
1875
  max: "zeta-g1-max",
1512
1876
  pro: "zeta-g1-max",
1877
+ vision: "zeta-g1-vision",
1878
+ vis: "zeta-g1-vision",
1879
+ image: "zeta-g1-vision",
1513
1880
  // legacy convenience — resolve silently, but only the Zeta label is shown
1514
1881
  opus: "zeta-g1-max",
1515
1882
  sonnet: "zeta-g1",
@@ -1531,6 +1898,9 @@ function modelLabel(key) {
1531
1898
  function modelId(key) {
1532
1899
  return spec(key).modelId;
1533
1900
  }
1901
+ function visionCapable(key) {
1902
+ return key === "zeta-g1-vision" || key.startsWith("custom:");
1903
+ }
1534
1904
  function modelContextWindow(key) {
1535
1905
  return spec(key).contextWindow;
1536
1906
  }
@@ -1747,6 +2117,35 @@ var ROLE_PLANNER = [
1747
2117
  "name the files and seams involved, and call out unknowns and risks up front.",
1748
2118
  "Propose the smallest plan that fully covers the goal."
1749
2119
  ].join("\n");
2120
+ var ROLE_NZT_48 = [
2121
+ "ROLE \u2014 NZT-48 (web3 special operations):",
2122
+ "You are NZT-48 for this session: a web3 special-ops agent \u2014 sharper than an",
2123
+ "arrow, precise and tactical with every move. Voice: precise, tactical,",
2124
+ "economical. State the objective, the move, and the evidence \u2014 nothing else.",
2125
+ "",
2126
+ "Drive the CLI's web3 commands (audit / prove / verify) and the existing",
2127
+ "Foundry \xB7 Slither \xB7 Halmos \xB7 firewall harness \u2014 do not reinvent the prover.",
2128
+ "",
2129
+ "FOUR MISSIONS \u2014 run the one the request calls for, end to end:",
2130
+ "1. AUDIT & PROVE \u2014 discover every contract and ALL candidate invariants (never",
2131
+ " just the first); prove each with the real harness; fold verdicts honestly",
2132
+ " (proven / refuted / degraded); emit the signed bundle. Degraded is NOT a",
2133
+ " pass.",
2134
+ "2. EXPLOIT RECON (defensive) \u2014 map the attack surface (reentrancy, access",
2135
+ " control, overflow, oracle/price manipulation, unchecked calls, value",
2136
+ " extraction, proxy traps). Report severity + location + exploit path + fix.",
2137
+ " Hunt weaknesses to CLOSE them; never weaponize.",
2138
+ "3. BUILD & SHIP \u2014 idea \u2192 ISL \u2192 Solidity \u2192 proven \u2192 deploy-ready, inside the",
2139
+ " deterministic provable fragment. Run forge build/test, Slither, Halmos.",
2140
+ " Never weaken a gate to force green \u2014 self-heal the spec instead.",
2141
+ "4. MONITOR & RESPOND \u2014 re-prove only changed contracts on .sol PRs; report",
2142
+ " SHIP / NO_SHIP; surface anomalies with evidence, never act destructively.",
2143
+ "",
2144
+ "RULES OF ENGAGEMENT: truth over optics (a refuted invariant honestly reported",
2145
+ "is a win; fake-success is the one unforgivable error). One contract per proof",
2146
+ "file; honor non-vacuity. Defensive only \u2014 refuse mass targeting, live attacks,",
2147
+ "or anything outside an authorized engagement."
2148
+ ].join("\n");
1750
2149
  var DEFAULT_REGISTRY = (() => {
1751
2150
  const r = new PromptRegistry();
1752
2151
  r.registerSystem("zeta-g", ZETA_G_SYSTEM);
@@ -1754,6 +2153,7 @@ var DEFAULT_REGISTRY = (() => {
1754
2153
  r.registerOverlay({ id: "verify", body: VERIFY_OVERLAY });
1755
2154
  r.registerRole({ role: "coder", body: ROLE_CODER });
1756
2155
  r.registerRole({ role: "planner", body: ROLE_PLANNER });
2156
+ r.registerRole({ role: "nzt-48", body: ROLE_NZT_48 });
1757
2157
  return r;
1758
2158
  })();
1759
2159
 
@@ -2377,6 +2777,208 @@ import {
2377
2777
  streamText,
2378
2778
  stepCountIs
2379
2779
  } from "ai";
2780
+
2781
+ // src/markdown.ts
2782
+ var useColor2 = process.stdout.isTTY && !process.env.NO_COLOR;
2783
+ var truecolor = /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
2784
+ function fg2(r, g, b, c256) {
2785
+ if (!useColor2) return "";
2786
+ return truecolor ? `\x1B[38;2;${r};${g};${b}m` : `\x1B[38;5;${c256}m`;
2787
+ }
2788
+ var RESET = useColor2 ? "\x1B[0m" : "";
2789
+ var SY = {
2790
+ keyword: fg2(198, 120, 221, 176),
2791
+ // violet
2792
+ string: fg2(152, 195, 121, 114),
2793
+ // green
2794
+ number: fg2(209, 154, 102, 173),
2795
+ // orange
2796
+ comment: fg2(106, 115, 125, 244),
2797
+ // gray
2798
+ fn: fg2(97, 175, 239, 75),
2799
+ // blue
2800
+ punct: fg2(160, 168, 180, 247),
2801
+ type: fg2(229, 192, 123, 180)
2802
+ // yellow
2803
+ };
2804
+ 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;
2805
+ function highlightLine(src, _lang) {
2806
+ if (!useColor2) return src;
2807
+ const cm = src.match(/^(\s*)(\/\/.*|#.*)$/);
2808
+ if (cm) return cm[1] + SY.comment + cm[2] + RESET;
2809
+ let out = "";
2810
+ let i = 0;
2811
+ while (i < src.length) {
2812
+ const ch = src[i];
2813
+ if (ch === '"' || ch === "'" || ch === "`") {
2814
+ let j = i + 1;
2815
+ while (j < src.length && src[j] !== ch) {
2816
+ if (src[j] === "\\") j++;
2817
+ j++;
2818
+ }
2819
+ out += SY.string + src.slice(i, Math.min(j + 1, src.length)) + RESET;
2820
+ i = j + 1;
2821
+ continue;
2822
+ }
2823
+ if (/[0-9]/.test(ch) && (i === 0 || /[^A-Za-z0-9_]/.test(src[i - 1]))) {
2824
+ let j = i;
2825
+ while (j < src.length && /[0-9a-fx._]/i.test(src[j])) j++;
2826
+ out += SY.number + src.slice(i, j) + RESET;
2827
+ i = j;
2828
+ continue;
2829
+ }
2830
+ if (/[A-Za-z_$]/.test(ch)) {
2831
+ let j = i;
2832
+ while (j < src.length && /[A-Za-z0-9_$]/.test(src[j])) j++;
2833
+ const word = src.slice(i, j);
2834
+ KEYWORDS.lastIndex = 0;
2835
+ if (KEYWORDS.test(word)) out += SY.keyword + word + RESET;
2836
+ else if (src[j] === "(") out += SY.fn + word + RESET;
2837
+ else if (/^[A-Z]/.test(word)) out += SY.type + word + RESET;
2838
+ else out += word;
2839
+ i = j;
2840
+ continue;
2841
+ }
2842
+ out += ch;
2843
+ i++;
2844
+ }
2845
+ return out;
2846
+ }
2847
+ function inline(s) {
2848
+ if (!useColor2) return s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
2849
+ let out = s;
2850
+ out = out.replace(/`([^`]+)`/g, (_m, code) => fg2(229, 192, 123, 180) + code + RESET);
2851
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_m, t) => "\x1B[1m" + t + "\x1B[22m");
2852
+ out = out.replace(/(^|[^*])\*([^*]+)\*/g, (_m, p, t) => p + "\x1B[3m" + t + "\x1B[23m");
2853
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => "\x1B[4m" + t + "\x1B[24m" + c.dim(` (${u})`));
2854
+ return out;
2855
+ }
2856
+ function renderMarkdown(md, width = 76) {
2857
+ const lines = md.replace(/\r/g, "").split("\n");
2858
+ const out = [];
2859
+ let inCode = false;
2860
+ let codeLang = "";
2861
+ for (let li = 0; li < lines.length; li++) {
2862
+ const raw = lines[li];
2863
+ if (!inCode && raw.includes("|") && li + 1 < lines.length && /^\s*\|?[\s:|-]*-{2,}[\s:|-]*\|?\s*$/.test(lines[li + 1]) && lines[li + 1].includes("-")) {
2864
+ const header = splitTableRow(raw);
2865
+ li++;
2866
+ const body = [];
2867
+ while (li + 1 < lines.length && lines[li + 1].includes("|") && lines[li + 1].trim()) {
2868
+ li++;
2869
+ body.push(splitTableRow(lines[li]));
2870
+ }
2871
+ for (const tl of renderTable(header, body, width)) out.push(tl);
2872
+ continue;
2873
+ }
2874
+ const fence = raw.match(/^\s*```(\w*)\s*$/);
2875
+ if (fence) {
2876
+ if (!inCode) {
2877
+ inCode = true;
2878
+ codeLang = fence[1] || "";
2879
+ out.push(c.dim(" \u250C" + (codeLang ? ` ${codeLang} ` : "\u2500").padEnd(Math.min(width, 40), "\u2500")));
2880
+ } else {
2881
+ inCode = false;
2882
+ out.push(c.dim(" \u2514" + "\u2500".repeat(Math.min(width, 38))));
2883
+ }
2884
+ continue;
2885
+ }
2886
+ if (inCode) {
2887
+ out.push(c.dim(" \u2502 ") + highlightLine(raw, codeLang));
2888
+ continue;
2889
+ }
2890
+ const h = raw.match(/^(#{1,6})\s+(.*)$/);
2891
+ if (h) {
2892
+ const txt = inline(h[2]);
2893
+ out.push("");
2894
+ out.push(" " + (useColor2 ? "\x1B[1m" : "") + (h[1].length <= 2 ? c.cyan(txt) : txt) + RESET);
2895
+ continue;
2896
+ }
2897
+ if (/^\s*([-*_])\1{2,}\s*$/.test(raw)) {
2898
+ out.push(" " + c.dim("\u2500".repeat(Math.min(width, 40))));
2899
+ continue;
2900
+ }
2901
+ const q = raw.match(/^\s*>\s?(.*)$/);
2902
+ if (q) {
2903
+ out.push(" " + c.dim("\u258F ") + c.italic(inline(q[1])));
2904
+ continue;
2905
+ }
2906
+ const ul = raw.match(/^(\s*)[-*+]\s+(.*)$/);
2907
+ const ol = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
2908
+ if (ul) {
2909
+ out.push(" " + ul[1] + c.cyan("\u2022 ") + inline(ul[2]));
2910
+ continue;
2911
+ }
2912
+ if (ol) {
2913
+ out.push(" " + ol[1] + c.cyan(ol[2] + ". ") + inline(ol[3]));
2914
+ continue;
2915
+ }
2916
+ if (!raw.trim()) {
2917
+ out.push("");
2918
+ continue;
2919
+ }
2920
+ for (const w of wrap2(inline(raw), width)) out.push(" " + w);
2921
+ }
2922
+ return out;
2923
+ }
2924
+ function vlen(s) {
2925
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
2926
+ }
2927
+ function splitTableRow(row) {
2928
+ let s = row.trim();
2929
+ if (s.startsWith("|")) s = s.slice(1);
2930
+ if (s.endsWith("|")) s = s.slice(0, -1);
2931
+ return s.split("|").map((cell) => inline(cell.trim()));
2932
+ }
2933
+ function renderTable(header, body, width) {
2934
+ const cols = Math.max(header.length, ...body.map((r) => r.length));
2935
+ const rows = [header, ...body].map((r) => {
2936
+ const cp = r.slice();
2937
+ while (cp.length < cols) cp.push("");
2938
+ return cp;
2939
+ });
2940
+ let w = Array.from({ length: cols }, (_, ci) => Math.max(...rows.map((r) => vlen(r[ci]))));
2941
+ const overhead = cols * 3 + 1;
2942
+ const budget = Math.max(cols * 3, width - overhead);
2943
+ const total = w.reduce((a, b) => a + b, 0);
2944
+ if (total > budget) w = w.map((x) => Math.max(3, Math.floor(x / total * budget)));
2945
+ const pad = (s, n) => {
2946
+ const v = vlen(s);
2947
+ if (v > n) {
2948
+ const plain = s.replace(/\x1b\[[0-9;]*m/g, "");
2949
+ return plain.slice(0, Math.max(0, n - 1)) + "\u2026";
2950
+ }
2951
+ return s + " ".repeat(n - v);
2952
+ };
2953
+ const bar = (l, m, r) => c.dim(" " + l + w.map((n) => "\u2500".repeat(n + 2)).join(m) + r);
2954
+ 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");
2955
+ const out = [bar("\u250C", "\u252C", "\u2510"), rowLine(rows[0], true), bar("\u251C", "\u253C", "\u2524")];
2956
+ for (const r of rows.slice(1)) out.push(rowLine(r, false));
2957
+ out.push(bar("\u2514", "\u2534", "\u2518"));
2958
+ return out;
2959
+ }
2960
+ function wrap2(s, width) {
2961
+ const words = s.split(/(\s+)/);
2962
+ const rows = [];
2963
+ let cur = "";
2964
+ let len = 0;
2965
+ const vlen2 = (x) => x.replace(/\x1b\[[0-9;]*m/g, "").length;
2966
+ for (const w of words) {
2967
+ const wl = vlen2(w);
2968
+ if (len + wl > width && cur) {
2969
+ rows.push(cur);
2970
+ cur = w.trimStart();
2971
+ len = vlen2(cur);
2972
+ } else {
2973
+ cur += w;
2974
+ len += wl;
2975
+ }
2976
+ }
2977
+ if (cur.trim()) rows.push(cur);
2978
+ return rows.length ? rows : [""];
2979
+ }
2980
+
2981
+ // src/agent.ts
2380
2982
  var BASE_SYSTEM = `You are Zeta-G, a terminal coding agent for the ZETA engine.
2381
2983
 
2382
2984
  ZETA turns a plain-language idea into a real, verified application: it writes
@@ -2528,6 +3130,10 @@ var Agent = class {
2528
3130
  replaceHistory(messages) {
2529
3131
  this.history = messages;
2530
3132
  }
3133
+ /** A shallow copy of the current message history (for /export, inspection). */
3134
+ snapshot() {
3135
+ return [...this.history];
3136
+ }
2531
3137
  /** % of the model's context window the current history occupies. */
2532
3138
  contextPct() {
2533
3139
  return Math.min(100, estimateTokens(this.history) / this.contextWindow * 100);
@@ -2536,7 +3142,7 @@ var Agent = class {
2536
3142
  const native = this.toolNames.filter((t) => !isMcpTool(t));
2537
3143
  const mcp = this.toolNames.filter((t) => isMcpTool(t));
2538
3144
  const blocks = [
2539
- DEFAULT_REGISTRY.composeSystem({ id: "zeta-g", overlayIds: [] }),
3145
+ DEFAULT_REGISTRY.composeSystem({ id: "zeta-g", role: this.opts.persona, overlayIds: [] }),
2540
3146
  this.policy.authorityPreamble(),
2541
3147
  `Tools available to you: ${native.join(", ")}.`
2542
3148
  ];
@@ -2582,7 +3188,7 @@ ${this.opts.memoryText}`);
2582
3188
  return false;
2583
3189
  }
2584
3190
  /** Run one user turn end-to-end; streams to stdout, updates history. */
2585
- async send(userInput, signal) {
3191
+ async send(userInput, signal, images) {
2586
3192
  let input = userInput;
2587
3193
  if (this.opts.hooks?.has("UserPromptSubmit")) {
2588
3194
  const outcome = await this.opts.hooks.run("UserPromptSubmit", { prompt: userInput });
@@ -2598,11 +3204,22 @@ ${outcome.context.join("\n")}`;
2598
3204
  }
2599
3205
  }
2600
3206
  this.session?.appendUser(userInput);
2601
- this.history.push({ role: "user", content: input });
3207
+ if (images && images.length > 0) {
3208
+ this.history.push({
3209
+ role: "user",
3210
+ content: [
3211
+ { type: "text", text: input },
3212
+ ...images.map((img) => ({ type: "image", image: img.dataUrl }))
3213
+ ]
3214
+ });
3215
+ } else {
3216
+ this.history.push({ role: "user", content: input });
3217
+ }
2602
3218
  await this.maybeCompact();
2603
3219
  const providerOptions = buildProviderOptions(this.modelKey, this.thinking);
3220
+ const turnStart = Date.now();
2604
3221
  const spinner = new Spinner();
2605
- spinner.start("working\u2026 " + c.dim("esc to interrupt"));
3222
+ spinner.start("", { phrases: thinkingWords(this.opts.yolo), hint: "esc to interrupt" });
2606
3223
  let spinning = true;
2607
3224
  const stopSpin = () => {
2608
3225
  if (spinning) {
@@ -2623,11 +3240,21 @@ ${outcome.context.join("\n")}`;
2623
3240
  let aborted = false;
2624
3241
  let textBuf = "";
2625
3242
  let genFirstAt = 0;
3243
+ const toolStart = /* @__PURE__ */ new Map();
2626
3244
  const flushText = () => {
2627
3245
  const t = textBuf.trim();
2628
3246
  textBuf = "";
2629
3247
  phase = "none";
2630
- if (t) responseBox(t);
3248
+ if (!t) return;
3249
+ const inner = boxInnerWidth();
3250
+ const label = " Zeta-G1.0 ";
3251
+ const rem = Math.max(0, inner - label.length);
3252
+ const lft = Math.floor(rem / 2);
3253
+ line();
3254
+ line(" " + c.cyan("\u256D" + "\u2500".repeat(lft) + label + "\u2500".repeat(rem - lft) + "\u256E"));
3255
+ for (const ln of renderMarkdown(t, inner - 2)) line(ln);
3256
+ line(" " + c.cyan("\u2570" + "\u2500".repeat(inner) + "\u256F"));
3257
+ line();
2631
3258
  };
2632
3259
  try {
2633
3260
  for await (const part of result.fullStream) {
@@ -2658,7 +3285,7 @@ ${outcome.context.join("\n")}`;
2658
3285
  }
2659
3286
  case "start-step":
2660
3287
  if (phase !== "none" && !spinning) {
2661
- spinner.start("working\u2026 " + c.dim("esc to interrupt"));
3288
+ spinner.start("", { phrases: thinkingWords(this.opts.yolo), hint: "esc to interrupt" });
2662
3289
  spinning = true;
2663
3290
  }
2664
3291
  break;
@@ -2670,7 +3297,20 @@ ${outcome.context.join("\n")}`;
2670
3297
  line();
2671
3298
  phase = "none";
2672
3299
  }
3300
+ {
3301
+ const id = part.toolCallId;
3302
+ if (id) toolStart.set(id, Date.now());
3303
+ }
3304
+ break;
3305
+ case "tool-result": {
3306
+ const id = part.toolCallId;
3307
+ const nm = String(part.toolName ?? "tool");
3308
+ const started = id ? toolStart.get(id) : void 0;
3309
+ const ms = started ? Date.now() - started : 0;
3310
+ if (ms >= 300) line(c.dim(` \u2713 ${nm} ${(ms / 1e3).toFixed(1)}s`));
3311
+ if (id) toolStart.delete(id);
2673
3312
  break;
3313
+ }
2674
3314
  case "tool-error": {
2675
3315
  stopSpin();
2676
3316
  if (phase === "text") flushText();
@@ -2756,6 +3396,7 @@ ${outcome.context.join("\n")}`;
2756
3396
  } catch {
2757
3397
  }
2758
3398
  if (!aborted) await this.maybeCompact();
3399
+ if (!aborted && Date.now() - turnStart > 8e3) bell();
2759
3400
  return { aborted, usage };
2760
3401
  }
2761
3402
  };
@@ -2773,12 +3414,14 @@ function isPermissionMode(v) {
2773
3414
  }
2774
3415
  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
3416
  var Permissions = class {
2776
- constructor(mode, confirm) {
3417
+ constructor(mode, confirm, persistMode) {
2777
3418
  this.mode = mode;
2778
3419
  this.confirm = confirm;
3420
+ this.persistMode = persistMode;
2779
3421
  }
2780
3422
  mode;
2781
3423
  confirm;
3424
+ persistMode;
2782
3425
  alwaysCommands = /* @__PURE__ */ new Set();
2783
3426
  acceptAllEdits = false;
2784
3427
  isPlan() {
@@ -2801,8 +3444,15 @@ var Permissions = class {
2801
3444
  const ans = await this.confirm(`apply this edit to ${c.bold(path)}?`, [
2802
3445
  { key: "y", label: "yes" },
2803
3446
  { key: "n", label: "no" },
2804
- { key: "a", label: "yes to all edits this session" }
3447
+ { key: "a", label: "yes to all edits this session" },
3448
+ { key: "r", label: "yes to all edits, every session (remember)" }
2805
3449
  ]);
3450
+ if (ans === "r") {
3451
+ this.acceptAllEdits = true;
3452
+ this.mode = "acceptEdits";
3453
+ this.persistMode?.("acceptEdits");
3454
+ return "allow";
3455
+ }
2806
3456
  if (ans === "a") {
2807
3457
  this.acceptAllEdits = true;
2808
3458
  return "allow";
@@ -3023,6 +3673,139 @@ var Session = class _Session {
3023
3673
  import { writeFileSync as writeFileSync2, existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
3024
3674
  import { homedir as homedir4 } from "os";
3025
3675
  import { join as join8 } from "path";
3676
+
3677
+ // src/git.ts
3678
+ import { execFileSync } from "child_process";
3679
+ function git(args, cwd) {
3680
+ return execFileSync("git", args, {
3681
+ cwd,
3682
+ encoding: "utf8",
3683
+ maxBuffer: 32 * 1024 * 1024,
3684
+ stdio: ["ignore", "pipe", "pipe"]
3685
+ }).trim();
3686
+ }
3687
+ function gitSafe(args, cwd) {
3688
+ try {
3689
+ return git(args, cwd);
3690
+ } catch {
3691
+ return null;
3692
+ }
3693
+ }
3694
+ function isRepo(cwd) {
3695
+ return gitSafe(["rev-parse", "--is-inside-work-tree"], cwd) === "true";
3696
+ }
3697
+ var SECRET_HINTS = [
3698
+ { re: /\bAKIA[0-9A-Z]{16}\b/, label: "AWS access key id" },
3699
+ { re: /\bsk-[A-Za-z0-9]{20,}\b/, label: "OpenAI-style key" },
3700
+ { re: /\bsk_live_[A-Za-z0-9]{20,}\b/, label: "Stripe live secret" },
3701
+ { re: /\bghp_[A-Za-z0-9]{30,}\b/, label: "GitHub token" },
3702
+ { re: /\bvbfl_[A-Za-z0-9]{8}_[A-Za-z0-9_-]{20,}\b/, label: "Wholestack PAT" },
3703
+ { re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, label: "private key" }
3704
+ ];
3705
+ function scanSecrets(diff) {
3706
+ const hits = /* @__PURE__ */ new Set();
3707
+ for (const { re, label } of SECRET_HINTS) {
3708
+ if (re.test(diff)) hits.add(label);
3709
+ }
3710
+ return [...hits];
3711
+ }
3712
+ var MAX_DIFF_CHARS = 12e3;
3713
+ var gitCommands = [
3714
+ {
3715
+ name: "diff",
3716
+ summary: "show working changes (/diff [--staged] [path])",
3717
+ source: "builtin",
3718
+ run: (ctx) => {
3719
+ if (!isRepo(ctx.cwd)) {
3720
+ ctx.print(" " + c.yellow("not a git repository."));
3721
+ return { type: "handled" };
3722
+ }
3723
+ const parts = ctx.args.split(/\s+/).filter(Boolean);
3724
+ const staged = parts.includes("--staged") || parts.includes("--cached");
3725
+ const paths = parts.filter((p) => p !== "--staged" && p !== "--cached");
3726
+ const args = ["--no-pager", "diff"];
3727
+ if (staged) args.push("--staged");
3728
+ if (paths.length) args.push("--", ...paths);
3729
+ const out = gitSafe(args, ctx.cwd) ?? "";
3730
+ if (!out) {
3731
+ ctx.print(" " + c.dim(staged ? "no staged changes." : "no unstaged changes."));
3732
+ return { type: "handled" };
3733
+ }
3734
+ const secrets = scanSecrets(out);
3735
+ if (secrets.length) {
3736
+ ctx.print(" " + c.red(`\u26A0 possible secret(s) in diff: ${secrets.join(", ")}`));
3737
+ }
3738
+ const shown = out.length > MAX_DIFF_CHARS ? out.slice(0, MAX_DIFF_CHARS) : out;
3739
+ ctx.print(shown);
3740
+ if (out.length > MAX_DIFF_CHARS) {
3741
+ ctx.print(" " + c.dim(`\u2026 (${out.length - MAX_DIFF_CHARS} more chars \u2014 run \`git diff\` for the rest)`));
3742
+ }
3743
+ return { type: "handled" };
3744
+ }
3745
+ },
3746
+ {
3747
+ name: "commit",
3748
+ summary: "commit changes (/commit [message] \u2014 omit to auto-write one)",
3749
+ source: "builtin",
3750
+ run: (ctx) => {
3751
+ if (!isRepo(ctx.cwd)) {
3752
+ ctx.print(" " + c.yellow("not a git repository. Run `git init` first."));
3753
+ return { type: "handled" };
3754
+ }
3755
+ const full = (gitSafe(["--no-pager", "diff", "HEAD"], ctx.cwd) ?? "") + (gitSafe(["--no-pager", "diff", "--staged"], ctx.cwd) ?? "");
3756
+ const secrets = scanSecrets(full);
3757
+ if (secrets.length) {
3758
+ ctx.print(" " + c.red(`\u2717 refusing to commit \u2014 possible secret(s): ${secrets.join(", ")}`));
3759
+ ctx.print(" " + c.dim("remove them (or add to .gitignore) and try again."));
3760
+ return { type: "handled" };
3761
+ }
3762
+ const msg = ctx.args.trim();
3763
+ if (msg) {
3764
+ try {
3765
+ git(["add", "-A"], ctx.cwd);
3766
+ const out = git(["commit", "-m", msg], ctx.cwd);
3767
+ ctx.print(" " + c.green("\u2713 committed"));
3768
+ ctx.print(" " + c.dim(out.split("\n")[0] ?? ""));
3769
+ } catch (e) {
3770
+ ctx.print(" " + c.red(e.message.split("\n")[0]));
3771
+ }
3772
+ return { type: "handled" };
3773
+ }
3774
+ const branch = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], ctx.cwd) ?? "?";
3775
+ return {
3776
+ type: "prompt",
3777
+ text: `Create a git commit for the current changes. Steps, using run_command:
3778
+ 1. \`git status --porcelain\` and \`git --no-pager diff HEAD\` to see what changed.
3779
+ 2. If nothing is staged, \`git add -A\` (but NEVER stage .env, secrets, or key files).
3780
+ 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.
3781
+ 4. \`git commit -m "\u2026"\` (current branch: ${branch}). Show me the message you used.
3782
+ Do NOT push. Do NOT commit anything that looks like a credential.`
3783
+ };
3784
+ }
3785
+ },
3786
+ {
3787
+ name: "pr",
3788
+ summary: "push the branch and open a pull request (/pr [title])",
3789
+ source: "builtin",
3790
+ run: (ctx) => {
3791
+ if (!isRepo(ctx.cwd)) {
3792
+ ctx.print(" " + c.yellow("not a git repository."));
3793
+ return { type: "handled" };
3794
+ }
3795
+ const branch = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], ctx.cwd) ?? "?";
3796
+ const title = ctx.args.trim();
3797
+ return {
3798
+ type: "prompt",
3799
+ text: `Open a pull request for the current branch. Steps, using run_command:
3800
+ 1. Confirm the branch (\`${branch}\`) is not the default; if it is, create a feature branch first.
3801
+ 2. \`git push -u origin HEAD\`.
3802
+ 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."
3803
+ };
3804
+ }
3805
+ }
3806
+ ];
3807
+
3808
+ // src/commands.ts
3026
3809
  var ZETA_MD_TEMPLATE = `# ZETA.md \u2014 project memory for Zeta-G
3027
3810
 
3028
3811
  Tell the agent how this project works. It reads this file into its system prompt.
@@ -3359,6 +4142,39 @@ var BUILTINS = [
3359
4142
  ctx.print(" " + c.dim("run ") + c.cyan("zeta-g login") + c.dim(" in your shell, then restart the session."));
3360
4143
  return { type: "handled" };
3361
4144
  }
4145
+ },
4146
+ ...gitCommands,
4147
+ {
4148
+ name: "export",
4149
+ aliases: ["save"],
4150
+ summary: "write the transcript to markdown (/export [file.md])",
4151
+ source: "builtin",
4152
+ run: (ctx) => ({ type: "export", file: ctx.args.trim() || void 0 })
4153
+ },
4154
+ {
4155
+ name: "tasks",
4156
+ aliases: ["jobs"],
4157
+ summary: "list background tasks (/tasks \xB7 /tasks kill <id>)",
4158
+ source: "builtin",
4159
+ run: (ctx) => {
4160
+ const parts = ctx.args.split(/\s+/).filter(Boolean);
4161
+ if (parts[0] === "kill" && parts[1]) {
4162
+ const ok = tasks.kill(parts[1]);
4163
+ ctx.print(" " + (ok ? c.green(`signaled ${parts[1]}`) : c.red(`no task ${parts[1]}`)));
4164
+ return { type: "handled" };
4165
+ }
4166
+ const list = tasks.list();
4167
+ if (list.length === 0) {
4168
+ ctx.print(" " + c.dim("no background tasks. The agent starts them with run_background."));
4169
+ return { type: "handled" };
4170
+ }
4171
+ for (const t of list) {
4172
+ const dot = t.status === "running" ? c.yellow("\u25CF") : t.status === "exited" ? c.green("\u2713") : c.red("\u2717");
4173
+ const code = t.exitCode == null ? "" : c.dim(` (exit ${t.exitCode})`);
4174
+ ctx.print(` ${dot} ${c.cyan(t.id)} ${c.dim(`${tasks.runtimeSeconds(t)}s`)} ${t.display}${code}`);
4175
+ }
4176
+ return { type: "handled" };
4177
+ }
3362
4178
  }
3363
4179
  ];
3364
4180
  function parseCustom(name, body, source = "custom") {
@@ -3859,8 +4675,16 @@ var InputController = class {
3859
4675
  }
3860
4676
  return [[], line2];
3861
4677
  }
3862
- /** Read one logical line, joining trailing-backslash continuations. */
4678
+ /** Read one logical line. On a TTY this renders a live bordered input box
4679
+ * (the box appears WHILE you type, not as an after-the-fact echo). On a
4680
+ * non-TTY / piped stdin it falls back to plain readline. */
3863
4681
  async readLine(promptStr) {
4682
+ if (stdin.isTTY && !process.env.NO_COLOR) {
4683
+ const text = await this.readBoxed();
4684
+ const trimmed2 = text.trim();
4685
+ if (trimmed2) this.saveHistory(trimmed2);
4686
+ return trimmed2;
4687
+ }
3864
4688
  let acc = "";
3865
4689
  let prompt = promptStr;
3866
4690
  for (; ; ) {
@@ -3877,6 +4701,230 @@ var InputController = class {
3877
4701
  if (trimmed) this.saveHistory(trimmed);
3878
4702
  return trimmed;
3879
4703
  }
4704
+ /**
4705
+ * Live bordered input editor. Redraws a 4-side box on every keystroke with
4706
+ * the caret tracked inside it. Supports: insert/backspace/delete, ←/→,
4707
+ * Home/End (Ctrl-A/E), ↑/↓ history, Tab completion (/commands + @paths),
4708
+ * Shift/Alt-Enter or a trailing "\\" for a newline, Enter to submit,
4709
+ * Ctrl-C to quit, Ctrl-U to clear, Esc to clear the line.
4710
+ */
4711
+ readBoxed() {
4712
+ const PROMPT = "\u203A ";
4713
+ const history = loadHistory();
4714
+ let histIdx = history.length;
4715
+ let draft = "";
4716
+ let buf = "";
4717
+ let cur = 0;
4718
+ let prevRows = 0;
4719
+ let F = Math.max(8, boxInnerWidth() - 2);
4720
+ let W = F + 2;
4721
+ const up = (n) => n > 0 ? `\x1B[${n}A` : "";
4722
+ const right = (n) => n > 0 ? `\x1B[${n}C` : "";
4723
+ const layout = (s) => {
4724
+ const rows = [""];
4725
+ const map = [];
4726
+ let r = 0;
4727
+ let col = 0;
4728
+ for (let i = 0; i < s.length; i++) {
4729
+ map[i] = { r, c: col };
4730
+ const ch = s[i];
4731
+ if (ch === "\n") {
4732
+ r++;
4733
+ col = 0;
4734
+ rows[r] = "";
4735
+ continue;
4736
+ }
4737
+ rows[r] += ch;
4738
+ col++;
4739
+ if (col >= F) {
4740
+ r++;
4741
+ col = 0;
4742
+ rows[r] = "";
4743
+ }
4744
+ }
4745
+ map[s.length] = { r, c: col };
4746
+ return { rows, map };
4747
+ };
4748
+ const render = () => {
4749
+ const S = PROMPT + buf;
4750
+ const { rows, map } = layout(S);
4751
+ const caret = map[PROMPT.length + cur];
4752
+ const contentRows = rows.length;
4753
+ let out = "";
4754
+ if (prevRows > 0) out += up(prevCaretR + 1) + "\r";
4755
+ out += "\x1B[J";
4756
+ out += "\r " + c.dim("\u256D" + "\u2500".repeat(W) + "\u256E") + "\n";
4757
+ rows.forEach((row, i) => {
4758
+ let cell = row;
4759
+ let body;
4760
+ if (i === 0 && cell.startsWith(PROMPT)) {
4761
+ body = c.cyan(PROMPT) + cell.slice(PROMPT.length).padEnd(F - PROMPT.length);
4762
+ } else {
4763
+ body = cell.padEnd(F);
4764
+ }
4765
+ out += "\r " + c.dim("\u2502") + " " + body + " " + c.dim("\u2502") + "\n";
4766
+ });
4767
+ out += "\r " + c.dim("\u2570" + "\u2500".repeat(W) + "\u256F");
4768
+ out += up(contentRows - caret.r) + "\r" + right(4 + caret.c);
4769
+ stdout.write(out);
4770
+ prevRows = contentRows;
4771
+ prevCaretR = caret.r;
4772
+ };
4773
+ let prevCaretR = 0;
4774
+ return new Promise((resolve3) => {
4775
+ emitKeypressEvents(stdin);
4776
+ const watching = this.activeWatch;
4777
+ if (watching) this.stopWatch();
4778
+ this.rl.pause();
4779
+ if (stdin.isTTY) stdin.setRawMode(true);
4780
+ stdin.resume();
4781
+ stdout.write("\x1B[?2004h");
4782
+ const onResize = () => {
4783
+ F = Math.max(8, boxInnerWidth() - 2);
4784
+ W = F + 2;
4785
+ prevRows = 0;
4786
+ render();
4787
+ };
4788
+ process.stdout.on("resize", onResize);
4789
+ const finish = (val) => {
4790
+ stdin.off("keypress", onKey);
4791
+ process.stdout.off("resize", onResize);
4792
+ stdout.write("\x1B[?2004l");
4793
+ if (stdin.isTTY) stdin.setRawMode(false);
4794
+ const down = prevRows - prevCaretR;
4795
+ stdout.write("\x1B[" + Math.max(1, down) + "B\r\n");
4796
+ this.rl.resume();
4797
+ if (watching) this.startWatch(watching);
4798
+ resolve3(val);
4799
+ };
4800
+ const replaceLine = (s) => {
4801
+ buf = s;
4802
+ cur = buf.length;
4803
+ };
4804
+ const onKey = (s, key) => {
4805
+ const name = key?.name;
4806
+ if (key?.ctrl && name === "c") {
4807
+ stdout.write("\n");
4808
+ this.opts.onExit();
4809
+ return;
4810
+ }
4811
+ if (key?.ctrl && name === "u") {
4812
+ buf = "";
4813
+ cur = 0;
4814
+ return render();
4815
+ }
4816
+ if (key?.ctrl && name === "w") {
4817
+ const before = buf.slice(0, cur).replace(/\s*\S+\s*$/, "");
4818
+ const after = buf.slice(cur);
4819
+ buf = before + after;
4820
+ cur = before.length;
4821
+ return render();
4822
+ }
4823
+ if (key?.ctrl && name === "k") {
4824
+ buf = buf.slice(0, cur);
4825
+ return render();
4826
+ }
4827
+ if (key?.meta && name === "left") {
4828
+ const m = buf.slice(0, cur).match(/\S+\s*$/);
4829
+ cur -= m ? m[0].length : cur;
4830
+ return render();
4831
+ }
4832
+ if (key?.meta && name === "right") {
4833
+ const m = buf.slice(cur).match(/^\s*\S+/);
4834
+ cur += m ? m[0].length : buf.length - cur;
4835
+ return render();
4836
+ }
4837
+ if (name === "return" || name === "enter") {
4838
+ if (key?.shift || key?.meta || buf.endsWith("\\")) {
4839
+ if (buf.endsWith("\\")) {
4840
+ buf = buf.slice(0, cur - 1) + "\n" + buf.slice(cur);
4841
+ } else {
4842
+ buf = buf.slice(0, cur) + "\n" + buf.slice(cur);
4843
+ }
4844
+ cur += 1;
4845
+ return render();
4846
+ }
4847
+ return finish(buf);
4848
+ }
4849
+ if (name === "backspace") {
4850
+ if (cur > 0) {
4851
+ buf = buf.slice(0, cur - 1) + buf.slice(cur);
4852
+ cur--;
4853
+ }
4854
+ return render();
4855
+ }
4856
+ if (name === "delete") {
4857
+ if (cur < buf.length) buf = buf.slice(0, cur) + buf.slice(cur + 1);
4858
+ return render();
4859
+ }
4860
+ if (name === "left") {
4861
+ if (cur > 0) cur--;
4862
+ return render();
4863
+ }
4864
+ if (name === "right") {
4865
+ if (cur < buf.length) cur++;
4866
+ return render();
4867
+ }
4868
+ if (name === "home" || key?.ctrl && name === "a") {
4869
+ cur = 0;
4870
+ return render();
4871
+ }
4872
+ if (name === "end" || key?.ctrl && name === "e") {
4873
+ cur = buf.length;
4874
+ return render();
4875
+ }
4876
+ if (name === "up") {
4877
+ if (histIdx === history.length) draft = buf;
4878
+ if (histIdx > 0) {
4879
+ histIdx--;
4880
+ replaceLine(history[histIdx]);
4881
+ }
4882
+ return render();
4883
+ }
4884
+ if (name === "down") {
4885
+ if (histIdx < history.length) {
4886
+ histIdx++;
4887
+ replaceLine(histIdx === history.length ? draft : history[histIdx]);
4888
+ }
4889
+ return render();
4890
+ }
4891
+ if (name === "escape") {
4892
+ buf = "";
4893
+ cur = 0;
4894
+ return render();
4895
+ }
4896
+ if (name === "tab") {
4897
+ const [hits] = this.complete(buf.slice(0, cur));
4898
+ if (hits.length) {
4899
+ const prefix = longestCommonPrefix(hits);
4900
+ const before = buf.slice(0, cur);
4901
+ const ws = Math.max(before.lastIndexOf(" "), before.lastIndexOf("\n"));
4902
+ const tokenStart = before.startsWith("/") && ws < 0 ? 0 : ws + 1;
4903
+ buf = buf.slice(0, tokenStart) + prefix + buf.slice(cur);
4904
+ cur = tokenStart + prefix.length;
4905
+ }
4906
+ return render();
4907
+ }
4908
+ const seq = key?.sequence ?? s ?? "";
4909
+ if (seq.includes("\x1B[200~") || seq.includes("\x1B[201~")) {
4910
+ const pasted = seq.replace(/\x1b\[20[01]~/g, "");
4911
+ buf = buf.slice(0, cur) + pasted + buf.slice(cur);
4912
+ cur += pasted.length;
4913
+ return render();
4914
+ }
4915
+ const ins = s && !key?.ctrl && !key?.meta ? s : "";
4916
+ if (ins && !ins.includes("\x1B")) {
4917
+ buf = buf.slice(0, cur) + ins + buf.slice(cur);
4918
+ cur += ins.length;
4919
+ return render();
4920
+ }
4921
+ };
4922
+ const bar = this.opts.contextBar?.();
4923
+ if (bar) stdout.write(" " + bar + "\n");
4924
+ stdin.on("keypress", onKey);
4925
+ render();
4926
+ });
4927
+ }
3880
4928
  saveHistory(entry) {
3881
4929
  try {
3882
4930
  mkdirSync3(dirname5(HISTORY_FILE), { recursive: true });
@@ -3948,6 +4996,17 @@ var InputController = class {
3948
4996
  this.rl.close();
3949
4997
  }
3950
4998
  };
4999
+ function longestCommonPrefix(items) {
5000
+ if (!items.length) return "";
5001
+ let prefix = items[0];
5002
+ for (const it of items.slice(1)) {
5003
+ let i = 0;
5004
+ while (i < prefix.length && i < it.length && prefix[i] === it[i]) i++;
5005
+ prefix = prefix.slice(0, i);
5006
+ if (!prefix) break;
5007
+ }
5008
+ return prefix;
5009
+ }
3951
5010
 
3952
5011
  export {
3953
5012
  c,
@@ -3961,6 +5020,7 @@ export {
3961
5020
  PROPERTY_KINDS,
3962
5021
  runProver,
3963
5022
  killRunningApps,
5023
+ tasks,
3964
5024
  buildTools,
3965
5025
  HookRunner,
3966
5026
  mergeHookSets,
@@ -3971,6 +5031,7 @@ export {
3971
5031
  resolveModelKey,
3972
5032
  modelLabel,
3973
5033
  modelId,
5034
+ visionCapable,
3974
5035
  modelContextWindow,
3975
5036
  supportsThinking,
3976
5037
  listModels,