wholestack 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,673 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Agent,
4
+ CommandRegistry,
5
+ HookRunner,
6
+ InputController,
7
+ PROPERTY_KINDS,
8
+ Permissions,
9
+ Session,
10
+ announcePlanMode,
11
+ banner,
12
+ buildWebTools,
13
+ c,
14
+ isPermissionMode,
15
+ killRunningApps,
16
+ line,
17
+ loadHookFiles,
18
+ loadMcpTools,
19
+ loadPlugins,
20
+ loadProjectMemory,
21
+ mergeHookSets,
22
+ modelContextWindow,
23
+ modelLabel,
24
+ resolveModel,
25
+ resolveModelKey,
26
+ runProver,
27
+ statusLine,
28
+ supportsThinking,
29
+ userBox
30
+ } from "./chunk-7DJJXUV4.js";
31
+
32
+ // src/cli.ts
33
+ import { createInterface } from "readline/promises";
34
+ import { stdin, stdout, argv, exit, cwd } from "process";
35
+
36
+ // src/config.ts
37
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
38
+ import { homedir } from "os";
39
+ import { join } from "path";
40
+ var CONFIG_DIR = join(homedir(), ".zeta-g");
41
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
42
+ function parseDotenv(text) {
43
+ const out = {};
44
+ for (const raw of text.split("\n")) {
45
+ const l = raw.trim();
46
+ if (!l || l.startsWith("#")) continue;
47
+ const eq = l.indexOf("=");
48
+ if (eq < 0) continue;
49
+ const k = l.slice(0, eq).trim();
50
+ let v = l.slice(eq + 1).trim();
51
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
52
+ v = v.slice(1, -1);
53
+ }
54
+ if (k) out[k] = v;
55
+ }
56
+ return out;
57
+ }
58
+ function fill(src) {
59
+ for (const [k, v] of Object.entries(src)) {
60
+ if (v && process.env[k] === void 0) process.env[k] = v;
61
+ }
62
+ }
63
+ function loadStoredEnv() {
64
+ const localEnv = join(process.cwd(), ".env");
65
+ if (existsSync(localEnv)) {
66
+ try {
67
+ fill(parseDotenv(readFileSync(localEnv, "utf8")));
68
+ } catch {
69
+ }
70
+ }
71
+ if (existsSync(CONFIG_PATH)) {
72
+ try {
73
+ const json = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
74
+ const flat = {};
75
+ for (const [k, v] of Object.entries(json)) if (typeof v === "string") flat[k] = v;
76
+ fill(flat);
77
+ } catch {
78
+ }
79
+ }
80
+ }
81
+ function saveKey(name, value) {
82
+ mkdirSync(CONFIG_DIR, { recursive: true });
83
+ let current = {};
84
+ if (existsSync(CONFIG_PATH)) {
85
+ try {
86
+ current = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
87
+ } catch {
88
+ current = {};
89
+ }
90
+ }
91
+ current[name] = value;
92
+ writeFileSync(CONFIG_PATH, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
93
+ chmodSync(CONFIG_PATH, 384);
94
+ return CONFIG_PATH;
95
+ }
96
+
97
+ // src/cli-login.ts
98
+ import { createServer } from "http";
99
+ import { spawn } from "child_process";
100
+ import { randomBytes } from "crypto";
101
+ import { hostname, platform } from "os";
102
+ var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>Wholestack CLI</title>
103
+ <body style="margin:0;background:#06070b;color:#e8eef2;font:15px/1.5 system-ui,sans-serif;display:grid;place-items:center;height:100vh">
104
+ <div style="text-align:center"><div style="font-size:42px">\u2713</div>
105
+ <h1 style="font-size:18px;margin:.4rem 0">CLI authorized</h1>
106
+ <p style="opacity:.6">You can close this tab and return to your terminal.</p></div></body>`;
107
+ function openBrowser(url) {
108
+ const p = platform();
109
+ const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
110
+ const args = p === "win32" ? ["/c", "start", "", url] : [url];
111
+ try {
112
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
113
+ child.on("error", () => {
114
+ });
115
+ child.unref();
116
+ } catch {
117
+ }
118
+ }
119
+ async function loginBrowser(opts) {
120
+ const state = randomBytes(16).toString("hex");
121
+ const device = hostname().slice(0, 60);
122
+ const timeoutMs = (opts.timeoutSec ?? 240) * 1e3;
123
+ return new Promise((resolve) => {
124
+ let settled = false;
125
+ const finish = (token) => {
126
+ if (settled) return;
127
+ settled = true;
128
+ clearTimeout(timer);
129
+ try {
130
+ server.close();
131
+ } catch {
132
+ }
133
+ resolve(token);
134
+ };
135
+ const server = createServer((req, res) => {
136
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
137
+ if (url.pathname !== "/callback") {
138
+ res.writeHead(404).end();
139
+ return;
140
+ }
141
+ if (url.searchParams.get("state") !== state) {
142
+ res.writeHead(400).end("state mismatch");
143
+ return;
144
+ }
145
+ const token = url.searchParams.get("token")?.trim();
146
+ res.writeHead(token ? 200 : 400, { "content-type": "text/html; charset=utf-8" });
147
+ res.end(token ? SUCCESS_HTML : "missing token");
148
+ if (token) {
149
+ const path = saveKey("ZETA_API_KEY", token);
150
+ line(c.green(" \u2713 logged in") + c.dim(` \u2192 ${path} (chmod 600)`));
151
+ finish(token);
152
+ }
153
+ });
154
+ server.on("error", (e) => {
155
+ line(c.red(` could not start loopback listener: ${e.message}`));
156
+ finish(null);
157
+ });
158
+ server.listen(0, "127.0.0.1", () => {
159
+ const addr = server.address();
160
+ const port = typeof addr === "object" && addr ? addr.port : 0;
161
+ if (!port) {
162
+ finish(null);
163
+ return;
164
+ }
165
+ const authUrl = `${opts.webBase.replace(/\/$/, "")}/cli/authorize?port=${port}&state=${state}&device=${encodeURIComponent(device)}`;
166
+ line();
167
+ line(c.cyan(" \u259F Wholestack login"));
168
+ line(c.dim(" Opening your browser to approve this device\u2026"));
169
+ line(c.dim(" If it doesn't open, visit:"));
170
+ line(" " + c.underline(authUrl));
171
+ line();
172
+ if (!opts.noBrowser) openBrowser(authUrl);
173
+ });
174
+ const timer = setTimeout(() => {
175
+ line(c.red(" login timed out \u2014 run `zeta login` again."));
176
+ finish(null);
177
+ }, timeoutMs);
178
+ });
179
+ }
180
+ function loginWithToken(token) {
181
+ const t = token.trim();
182
+ if (!/^vbfl_[A-Za-z0-9]{8}_[A-Za-z0-9_-]+$/.test(t)) {
183
+ line(c.red(" that doesn't look like a Wholestack token (expected vbfl_\u2026)."));
184
+ return null;
185
+ }
186
+ const path = saveKey("ZETA_API_KEY", t);
187
+ line(c.green(" \u2713 logged in") + c.dim(` \u2192 ${path} (chmod 600)`));
188
+ return t;
189
+ }
190
+
191
+ // src/checkpoints.ts
192
+ import { readFile, writeFile, mkdir, rm } from "fs/promises";
193
+ import { dirname } from "path";
194
+ var CheckpointStore = class {
195
+ undoStack = [];
196
+ redoStack = [];
197
+ seq = 0;
198
+ /** Snaps accumulated for the in-flight tool call (multi_edit groups several). */
199
+ pending = [];
200
+ /** Begin a new group; call before a mutating tool touches any file. */
201
+ begin() {
202
+ this.pending = [];
203
+ }
204
+ /** Record a file's pre-edit state once per path within the current group. */
205
+ capture(path, before) {
206
+ if (!this.pending.some((s) => s.path === path)) this.pending.push({ path, before });
207
+ }
208
+ /** Seal the group into an undoable checkpoint. A redo stack is invalidated by new edits. */
209
+ commit(label) {
210
+ if (!this.pending.length) return;
211
+ this.undoStack.push({ id: ++this.seq, label, at: Date.now(), snaps: this.pending });
212
+ this.pending = [];
213
+ this.redoStack = [];
214
+ }
215
+ get canUndo() {
216
+ return this.undoStack.length > 0;
217
+ }
218
+ get canRedo() {
219
+ return this.redoStack.length > 0;
220
+ }
221
+ list(limit = 20) {
222
+ return this.undoStack.slice(-limit).reverse();
223
+ }
224
+ /** Restore the most recent checkpoint; the displaced state becomes redoable. */
225
+ async undo() {
226
+ const cp = this.undoStack.pop();
227
+ if (!cp) return { ok: false, error: "nothing to undo" };
228
+ const redoSnaps = await this.captureCurrent(cp.snaps);
229
+ const applied = await this.apply(cp.snaps);
230
+ this.redoStack.push({ ...cp, snaps: redoSnaps });
231
+ return { ok: true, label: cp.label, ...applied };
232
+ }
233
+ /** Re-apply the most recently undone checkpoint. */
234
+ async redo() {
235
+ const cp = this.redoStack.pop();
236
+ if (!cp) return { ok: false, error: "nothing to redo" };
237
+ const undoSnaps = await this.captureCurrent(cp.snaps);
238
+ const applied = await this.apply(cp.snaps);
239
+ this.undoStack.push({ ...cp, snaps: undoSnaps });
240
+ return { ok: true, label: cp.label, ...applied };
241
+ }
242
+ /** Read the current on-disk state of each path so it can be re-applied later. */
243
+ async captureCurrent(snaps) {
244
+ return Promise.all(
245
+ snaps.map(async (s) => ({
246
+ path: s.path,
247
+ before: await readFile(s.path, "utf8").catch(() => null)
248
+ }))
249
+ );
250
+ }
251
+ /** Write each snap's `before` back to disk (or delete when it was absent). */
252
+ async apply(snaps) {
253
+ const restored = [];
254
+ const deleted = [];
255
+ for (const s of snaps) {
256
+ if (s.before === null) {
257
+ await rm(s.path, { force: true });
258
+ deleted.push(s.path);
259
+ } else {
260
+ await mkdir(dirname(s.path), { recursive: true });
261
+ await writeFile(s.path, s.before, "utf8");
262
+ restored.push(s.path);
263
+ }
264
+ }
265
+ return { restored, deleted };
266
+ }
267
+ };
268
+
269
+ // src/cli.ts
270
+ function parse(raw) {
271
+ const a = {
272
+ zetaUrl: process.env.ZETA_API_URL ?? "https://wholestack.ai",
273
+ buildMode: "auto",
274
+ yes: false,
275
+ plan: false,
276
+ noMcp: false,
277
+ noPlugins: false,
278
+ print: false,
279
+ cont: false,
280
+ help: false,
281
+ version: false,
282
+ prompt: ""
283
+ };
284
+ const rest = [];
285
+ for (let i = 0; i < raw.length; i++) {
286
+ const t = raw[i];
287
+ if (t === "--model" || t === "-m") a.model = raw[++i];
288
+ else if (t === "--zeta-url") a.zetaUrl = raw[++i];
289
+ else if (t === "--build-mode") a.buildMode = raw[++i];
290
+ else if (t === "--local") a.buildMode = "local";
291
+ else if (t === "--http") a.buildMode = "http";
292
+ else if (t === "--yes" || t === "-y") a.yes = true;
293
+ else if (t === "--mode") {
294
+ const m = raw[++i];
295
+ if (isPermissionMode(m)) a.mode = m;
296
+ } else if (t === "--plan") a.plan = true;
297
+ else if (t === "--think") a.think = true;
298
+ else if (t === "--no-think") a.think = false;
299
+ else if (t === "--mcp") a.mcp = (raw[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
300
+ else if (t === "--no-mcp") a.noMcp = true;
301
+ else if (t === "--no-plugins") a.noPlugins = true;
302
+ else if (t === "--print" || t === "-p") a.print = true;
303
+ else if (t === "--continue") a.cont = true;
304
+ else if (t === "--resume") {
305
+ const next = raw[i + 1];
306
+ if (next && !next.startsWith("-")) a.resume = raw[++i];
307
+ else a.resume = "";
308
+ } else if (t === "--help" || t === "-h") a.help = true;
309
+ else if (t === "--version" || t === "-v") a.version = true;
310
+ else rest.push(t);
311
+ }
312
+ a.prompt = rest.join(" ").trim();
313
+ return a;
314
+ }
315
+ var HELP = `${c.cyan("zeta-g")} \u2014 conversational coding agent for the ZETA engine
316
+
317
+ ${c.bold("Usage")}
318
+ zeta-g start an interactive session
319
+ zeta-g "build me a todo app" run one prompt and exit
320
+ zeta-g -m zeta-g1-max "..." drive it with the most capable brain
321
+
322
+ ${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmos \xB7 signed cert)")}
323
+ zeta-g audit <dir> <Contract> full auto-detect security sweep
324
+ zeta-g prove <dir> <Contract> --property <k> prove one invariant
325
+ zeta-g verify <dir> <Contract> --cert c.json passthrough + signed certificate
326
+ ${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
327
+
328
+ ${c.bold("Session")}
329
+ -m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max
330
+ (lite = light \xB7 g1 = default \xB7 max = most capable)
331
+ --mode <m> default | acceptEdits | plan | yolo (permission mode)
332
+ --plan start in read-only plan mode
333
+ --think/--no-think toggle extended thinking (when a tier supports it)
334
+ --continue resume the most recent session
335
+ --resume [id] resume a session (id, or the latest)
336
+ -p, --print one-shot: print the answer and exit
337
+ -y, --yes skip all confirmations (yolo mode)
338
+
339
+ ${c.bold("Extensibility")}
340
+ --mcp a,b,c only mount these MCP servers (else all from .mcp.json)
341
+ --no-mcp skip MCP servers --no-plugins skip plugins
342
+
343
+ ${c.bold("Build engine")}
344
+ --zeta-url <u> ZETA engine base URL (default http://localhost:3000)
345
+ --local build via the in-repo worker (no server needed)
346
+ --http build via the running ZETA engine only
347
+
348
+ ${c.bold("Auth")} ${c.dim("(set once, then just run zeta-g)")}
349
+ zeta-g login interactive \u2014 pick member/brain/engine, paste key
350
+ zeta-g login --member <key> membership key (keep + deploy generated apps)
351
+ zeta-g login --brain <key> brain key (powers the Zeta-G1.0 tiers)
352
+ zeta-g login --build <key> ZETA build-engine key (for /build)
353
+
354
+ ${c.bold("In a session")} ${c.dim("(type /help for the full list)")}
355
+ /model /cost /tools /undo /redo /checkpoints /compact /resume /memory /mcp /mode /think /init /doctor /clear /exit`;
356
+ async function runSecuritySubcommand(raw) {
357
+ const verb = raw[0];
358
+ if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
359
+ const rest = raw.slice(1);
360
+ const proverArgs = verb === "verify" && rest[0]?.endsWith(".json") ? ["verify", ...rest] : [...rest];
361
+ if (verb === "prove" && !proverArgs.includes("--property")) {
362
+ line(c.red(" prove needs --property <kind>"));
363
+ line(c.dim(` kinds: ${PROPERTY_KINDS.join(", ")}`));
364
+ return 1;
365
+ }
366
+ line();
367
+ line(c.cyan(` \u259F ShipGate ${verb}`) + c.dim(" \xB7 forge \xB7 slither \xB7 firewall \xB7 halmos"));
368
+ line();
369
+ const r = await runProver(proverArgs, { cwd: cwd(), inherit: true });
370
+ if (r.missing) {
371
+ line(c.red(` ${r.out}`));
372
+ return 1;
373
+ }
374
+ line();
375
+ line(r.ok ? c.green(" \u2713 verified") : c.red(" \u2717 NOT verified"));
376
+ return r.code;
377
+ }
378
+ function webBaseUrl() {
379
+ return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
380
+ }
381
+ async function runLogin(raw) {
382
+ if (raw[0] !== "login" && raw[0] !== "logout") return null;
383
+ const rest = raw.slice(1);
384
+ if (raw[0] === "logout") {
385
+ saveKey("ZETA_API_KEY", "");
386
+ line(c.green(" \u2713 logged out") + c.dim(" (token cleared)"));
387
+ return 0;
388
+ }
389
+ const tokenIdx = rest.indexOf("--token");
390
+ if (tokenIdx >= 0) {
391
+ const tok = rest[tokenIdx + 1];
392
+ if (!tok) {
393
+ line(c.red(" usage: zeta login --token <vbfl_\u2026>"));
394
+ return 1;
395
+ }
396
+ return loginWithToken(tok) ? 0 : 1;
397
+ }
398
+ const pick = (flag) => {
399
+ const i = rest.indexOf(flag);
400
+ return i >= 0 ? rest[i + 1] : void 0;
401
+ };
402
+ let name;
403
+ let value;
404
+ const brainFlags = ["--brain", "--smart", "--openrouter"];
405
+ const buildFlags = ["--build", "--fast", "--cerebras"];
406
+ const memberFlags = ["--member", "--key", "--subscribe"];
407
+ const hit = (flags) => flags.find((f) => rest.includes(f));
408
+ if (!hit(memberFlags) && !hit(brainFlags) && !hit(buildFlags)) {
409
+ const noBrowser = rest.includes("--no-browser");
410
+ const token = await loginBrowser({ webBase: webBaseUrl(), noBrowser });
411
+ return token ? 0 : 1;
412
+ }
413
+ if (hit(memberFlags)) {
414
+ name = "ZETA_API_KEY";
415
+ value = pick(hit(memberFlags));
416
+ } else if (hit(brainFlags)) {
417
+ name = "CEREBRAS_API_KEY";
418
+ value = pick(hit(brainFlags));
419
+ } else if (hit(buildFlags)) {
420
+ name = "CEREBRAS_API_KEY";
421
+ value = pick(hit(buildFlags));
422
+ }
423
+ if (!name || !value) {
424
+ if (!stdin.isTTY) {
425
+ line(c.red(" usage: zeta-g login --member <key> | --brain <key> | --build <key>"));
426
+ return 1;
427
+ }
428
+ const rl = createInterface({ input: stdin, output: stdout });
429
+ if (!name) {
430
+ const which = (await rl.question(c.cyan(" key for [member/brain/engine]: "))).trim().toLowerCase();
431
+ name = which.startsWith("m") ? "ZETA_API_KEY" : "CEREBRAS_API_KEY";
432
+ }
433
+ value = (await rl.question(c.cyan(" paste key: "))).trim();
434
+ rl.close();
435
+ }
436
+ if (!value) {
437
+ line(c.red(" no key provided."));
438
+ return 1;
439
+ }
440
+ const what = name === "ZETA_API_KEY" ? "membership (keep + deploy your apps)" : name === "OPENROUTER_API_KEY" ? "brain (Zeta-G1.0 tiers)" : "ZETA build engine";
441
+ const path = saveKey(name, value);
442
+ line(c.green(` \u2713 saved key for the ${what}`) + c.dim(` \u2192 ${path} (chmod 600)`));
443
+ line(c.dim(" now just run `zeta-g`."));
444
+ return 0;
445
+ }
446
+ var EMPTY_PLUGINS = {
447
+ plugins: [],
448
+ systemText: "",
449
+ commands: [],
450
+ mcpServers: {},
451
+ hooks: {},
452
+ failed: []
453
+ };
454
+ function bootSummary(memory, plugins, mcp, mode, thinking) {
455
+ const bits = [`mode ${c.yellow(mode)}`];
456
+ if (thinking) bits.push("thinking on");
457
+ if (memory.sources.length) bits.push(`${memory.sources.length} memory file${memory.sources.length === 1 ? "" : "s"}`);
458
+ if (plugins.plugins.length) bits.push(`${plugins.plugins.length} plugin${plugins.plugins.length === 1 ? "" : "s"}`);
459
+ if (mcp.mounted.length) bits.push(`${mcp.mounted.length} mcp`);
460
+ line(" " + c.dim(bits.join(" \xB7 ")));
461
+ for (const s of memory.sources) line(" " + c.dim(` memory: ${s.path}`));
462
+ for (const p of plugins.plugins) line(" " + c.dim(` plugin: ${p.name} (${p.dir})`));
463
+ for (const f of mcp.failed) line(" " + c.dim(`mcp ${f.key} unavailable: ${f.error}`));
464
+ for (const f of plugins.failed) line(" " + c.dim(`plugin ${f.dir} failed: ${f.error}`));
465
+ line();
466
+ }
467
+ async function main() {
468
+ loadStoredEnv();
469
+ const rawArgs = argv.slice(2);
470
+ const loggedIn = await runLogin(rawArgs);
471
+ if (loggedIn !== null) exit(loggedIn);
472
+ const sub = await runSecuritySubcommand(rawArgs);
473
+ if (sub !== null) exit(sub);
474
+ const args = parse(rawArgs);
475
+ if (args.version) {
476
+ line("wholestack 0.4.0");
477
+ return;
478
+ }
479
+ if (args.help) {
480
+ line(HELP);
481
+ return;
482
+ }
483
+ let modelKey;
484
+ try {
485
+ modelKey = resolveModelKey(args.model);
486
+ } catch (e) {
487
+ line(c.red(e.message));
488
+ exit(1);
489
+ }
490
+ let model;
491
+ try {
492
+ model = resolveModel(modelKey);
493
+ } catch (e) {
494
+ line(c.red(e.message));
495
+ exit(1);
496
+ }
497
+ const isTty = !!stdin.isTTY;
498
+ const oneShot = args.prompt.length > 0;
499
+ const workdir = cwd();
500
+ const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : isTty ? "default" : "acceptEdits";
501
+ const memory = await loadProjectMemory(workdir);
502
+ const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
503
+ const memoryText = [memory.text, plugins.systemText].filter(Boolean).join("\n\n") || void 0;
504
+ const mcp = await loadMcpTools({
505
+ cwd: workdir,
506
+ only: args.mcp,
507
+ disabled: args.noMcp,
508
+ extraServers: plugins.mcpServers
509
+ });
510
+ const extraTools = { ...buildWebTools(), ...mcp.tools };
511
+ const hooks = new HookRunner(mergeHookSets(loadHookFiles(workdir), plugins.hooks), workdir);
512
+ const registry = new CommandRegistry();
513
+ registry.loadCustom(workdir);
514
+ for (const cmd of plugins.commands) registry.register(cmd);
515
+ let ic = null;
516
+ let shuttingDown = false;
517
+ const shutdown = async (code = 0) => {
518
+ if (shuttingDown) return;
519
+ shuttingDown = true;
520
+ killRunningApps();
521
+ await mcp.close().catch(() => {
522
+ });
523
+ ic?.close();
524
+ exit(code);
525
+ };
526
+ if (isTty) {
527
+ ic = new InputController({
528
+ commandNames: () => registry.names(),
529
+ onExit: () => {
530
+ line(c.dim(" bye."));
531
+ void shutdown(0);
532
+ }
533
+ });
534
+ }
535
+ const confirm = isTty && ic ? ic.confirm.bind(ic) : void 0;
536
+ const permissions = new Permissions(mode, confirm);
537
+ const checkpoints = new CheckpointStore();
538
+ let session;
539
+ let resumedMessages = null;
540
+ if (args.resume !== void 0 || args.cont) {
541
+ const id = args.resume || Session.latestId(workdir);
542
+ const r = id ? Session.resume(id) : null;
543
+ if (r) {
544
+ session = r.session;
545
+ resumedMessages = r.messages;
546
+ } else {
547
+ line(c.dim(" no session to resume \u2014 starting fresh."));
548
+ session = Session.create(workdir, modelLabel(modelKey));
549
+ }
550
+ } else {
551
+ session = Session.create(workdir, modelLabel(modelKey));
552
+ }
553
+ const thinking = args.think ?? false;
554
+ const agent = new Agent({
555
+ model,
556
+ modelKey,
557
+ ctx: { cwd: workdir, zetaApiUrl: args.zetaUrl, buildMode: args.buildMode, permissions, checkpoints },
558
+ extraTools,
559
+ hooks,
560
+ memoryText,
561
+ thinking,
562
+ maxSteps: 16,
563
+ contextWindow: modelContextWindow(modelKey),
564
+ session
565
+ });
566
+ if (resumedMessages) agent.replaceHistory(resumedMessages);
567
+ if (oneShot) {
568
+ if (ic) await ic.runInterruptible((sig) => agent.send(args.prompt, sig));
569
+ else await agent.send(args.prompt);
570
+ await shutdown(0);
571
+ return;
572
+ }
573
+ if (!ic) {
574
+ line(c.red(" no prompt given and stdin is not a TTY \u2014 nothing to do."));
575
+ await shutdown(1);
576
+ return;
577
+ }
578
+ banner();
579
+ bootSummary(memory, plugins, mcp, mode, thinking);
580
+ if (mode === "plan") announcePlanMode();
581
+ if (resumedMessages) line(" " + c.dim(`resumed ${session.meta.id} \xB7 ${resumedMessages.length} messages`) + "\n");
582
+ const baseCtx = () => ({
583
+ cwd: workdir,
584
+ modelKey,
585
+ modelLabel: modelLabel(modelKey),
586
+ thinkingOn: agent.thinkingOn,
587
+ permissions,
588
+ usage: agent.usage,
589
+ memory,
590
+ mcp,
591
+ toolNames: agent.toolNames,
592
+ capabilities: agent.capabilities(),
593
+ checkpoints,
594
+ print: (s = "") => line(s)
595
+ });
596
+ const runTurn = async (text) => {
597
+ const t0 = Date.now();
598
+ await ic.runInterruptible((sig) => agent.send(text, sig));
599
+ statusLine({
600
+ model: modelLabel(modelKey),
601
+ tokens: agent.usage.totalTokens,
602
+ tps: agent.lastTps,
603
+ elapsedMs: Date.now() - t0,
604
+ cost: agent.usage.totalCost,
605
+ mode: permissions.mode,
606
+ contextPct: agent.contextPct()
607
+ });
608
+ };
609
+ for (; ; ) {
610
+ const input = await ic.readLine(c.cyan(" \u203A "));
611
+ if (!input) continue;
612
+ if (input.startsWith("/")) {
613
+ const res = await registry.dispatch(input, baseCtx());
614
+ if (!res || res.type === "handled") continue;
615
+ if (res.type === "exit") break;
616
+ if (res.type === "clear") {
617
+ agent.reset();
618
+ session = Session.create(workdir, modelLabel(modelKey));
619
+ agent.setSession(session);
620
+ line(c.dim(" context cleared \u2014 new session.\n"));
621
+ } else if (res.type === "compact") {
622
+ await agent.runCompaction(true);
623
+ } else if (res.type === "prompt") {
624
+ await runTurn(res.text);
625
+ } else if (res.type === "switchModel") {
626
+ try {
627
+ const m = resolveModel(res.modelKey);
628
+ modelKey = res.modelKey;
629
+ agent.setModel(m, modelKey, modelContextWindow(modelKey));
630
+ agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
631
+ line(
632
+ " " + c.green(`\u2192 ${modelLabel(modelKey)}`) + (agent.thinkingOn ? c.dim(" \xB7 thinking on") : "") + "\n"
633
+ );
634
+ } catch (e) {
635
+ line(" " + c.red(e.message));
636
+ }
637
+ } else if (res.type === "setMode") {
638
+ permissions.setMode(res.mode);
639
+ line(" " + c.dim(`mode: ${res.mode}`) + "\n");
640
+ if (res.mode === "plan") announcePlanMode();
641
+ } else if (res.type === "toggleThinking") {
642
+ if (supportsThinking(modelKey)) {
643
+ agent.setThinking(res.on);
644
+ line(" " + c.dim(`thinking ${res.on ? "on" : "off"}`) + "\n");
645
+ } else {
646
+ line(" " + c.dim(`${modelLabel(modelKey)} has no thinking mode`) + "\n");
647
+ }
648
+ } else if (res.type === "resume") {
649
+ const r = Session.resume(res.sessionId);
650
+ if (r) {
651
+ agent.replaceHistory(r.messages);
652
+ agent.setSession(r.session);
653
+ session = r.session;
654
+ line(" " + c.green(`resumed ${res.sessionId} \xB7 ${r.messages.length} messages`) + "\n");
655
+ } else {
656
+ line(" " + c.red(`no session ${res.sessionId}`) + "\n");
657
+ }
658
+ }
659
+ continue;
660
+ }
661
+ userBox(input);
662
+ await runTurn(input);
663
+ }
664
+ killRunningApps();
665
+ await mcp.close().catch(() => {
666
+ });
667
+ ic.close();
668
+ line(c.dim(" bye."));
669
+ }
670
+ main().catch((e) => {
671
+ line(c.red(String(e?.stack ?? e)));
672
+ exit(1);
673
+ });