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.
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  Session,
10
10
  announcePlanMode,
11
11
  banner,
12
+ buildTools,
12
13
  buildWebTools,
13
14
  c,
14
15
  isPermissionMode,
@@ -26,8 +27,10 @@ import {
26
27
  runProver,
27
28
  statusLine,
28
29
  supportsThinking,
29
- userBox
30
- } from "./chunk-7DJJXUV4.js";
30
+ tasks,
31
+ userBox,
32
+ visionCapable
33
+ } from "./chunk-PGKDYDAR.js";
31
34
 
32
35
  // src/cli.ts
33
36
  import { createInterface } from "readline/promises";
@@ -120,7 +123,7 @@ async function loginBrowser(opts) {
120
123
  const state = randomBytes(16).toString("hex");
121
124
  const device = hostname().slice(0, 60);
122
125
  const timeoutMs = (opts.timeoutSec ?? 240) * 1e3;
123
- return new Promise((resolve) => {
126
+ return new Promise((resolve4) => {
124
127
  let settled = false;
125
128
  const finish = (token) => {
126
129
  if (settled) return;
@@ -130,7 +133,7 @@ async function loginBrowser(opts) {
130
133
  server.close();
131
134
  } catch {
132
135
  }
133
- resolve(token);
136
+ resolve4(token);
134
137
  };
135
138
  const server = createServer((req, res) => {
136
139
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -266,7 +269,615 @@ var CheckpointStore = class {
266
269
  }
267
270
  };
268
271
 
272
+ // src/mentions.ts
273
+ import { readFile as readFile2, stat, readdir } from "fs/promises";
274
+ import { resolve, relative, join as join2, isAbsolute } from "path";
275
+ import fg from "fast-glob";
276
+ var IGNORE = [
277
+ "**/node_modules/**",
278
+ "**/.git/**",
279
+ "**/dist/**",
280
+ "**/.next/**",
281
+ "**/build/**",
282
+ "**/.turbo/**"
283
+ ];
284
+ var CHARS_PER_TOKEN = 4;
285
+ var DEFAULT_TOKEN_BUDGET = 25e3;
286
+ var PER_FILE_TOKEN_CAP = 8e3;
287
+ var MAX_FILES_PER_MENTION = 60;
288
+ var MAX_TREE_ENTRIES = 200;
289
+ function extractMentions(line2) {
290
+ const out = [];
291
+ const re = /(^|\s)@([~A-Za-z0-9._\-/*]+)/g;
292
+ for (let m = re.exec(line2); m; m = re.exec(line2)) {
293
+ let p = m[2];
294
+ p = p.replace(/[.,;:)]+$/g, (tail) => tail.length ? "" : tail);
295
+ if (p) out.push(p);
296
+ }
297
+ return [...new Set(out)];
298
+ }
299
+ function estTokens(s) {
300
+ return Math.ceil(s.length / CHARS_PER_TOKEN);
301
+ }
302
+ function looksBinary(buf) {
303
+ if (buf.includes("\0")) return true;
304
+ let ctrl = 0;
305
+ const n = Math.min(buf.length, 1e3);
306
+ for (let i = 0; i < n; i++) {
307
+ const code = buf.charCodeAt(i);
308
+ if (code < 9 || code > 13 && code < 32) ctrl++;
309
+ }
310
+ return ctrl / Math.max(1, n) > 0.3;
311
+ }
312
+ async function renderTree(absDir, relDir) {
313
+ const lines = [];
314
+ let count = 0;
315
+ async function walk(dir, prefix, depth) {
316
+ if (count >= MAX_TREE_ENTRIES || depth > 3) return;
317
+ let entries;
318
+ try {
319
+ entries = await readdir(dir, { withFileTypes: true });
320
+ } catch {
321
+ return;
322
+ }
323
+ const filtered = entries.filter((e) => !["node_modules", ".git", "dist", ".next", "build", ".turbo"].includes(e.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
324
+ for (const e of filtered) {
325
+ if (count >= MAX_TREE_ENTRIES) {
326
+ lines.push(`${prefix}\u2026 (truncated)`);
327
+ return;
328
+ }
329
+ count++;
330
+ lines.push(`${prefix}${e.name}${e.isDirectory() ? "/" : ""}`);
331
+ if (e.isDirectory()) await walk(join2(dir, e.name), prefix + " ", depth + 1);
332
+ }
333
+ }
334
+ await walk(absDir, "", 0);
335
+ return `${relDir || "."}/ (directory tree)
336
+ ${lines.join("\n")}`;
337
+ }
338
+ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET) {
339
+ const mentions = extractMentions(message);
340
+ if (mentions.length === 0) return { context: "", notice: null };
341
+ const blocks = [];
342
+ const attached = [];
343
+ const seen = /* @__PURE__ */ new Set();
344
+ let spent = 0;
345
+ for (const mention of mentions) {
346
+ if (spent >= tokenBudget) break;
347
+ const raw = mention.startsWith("~/") ? join2(process.env.HOME ?? "", mention.slice(2)) : mention;
348
+ const direct = isAbsolute(raw) ? raw : resolve(cwd2, raw);
349
+ let isDir = false;
350
+ try {
351
+ isDir = (await stat(direct)).isDirectory();
352
+ } catch {
353
+ isDir = false;
354
+ }
355
+ if (isDir) {
356
+ const rel = relative(cwd2, direct);
357
+ const tree = await renderTree(direct, rel);
358
+ const cost = estTokens(tree);
359
+ if (spent + cost <= tokenBudget) {
360
+ blocks.push(tree);
361
+ spent += cost;
362
+ attached.push(`${rel || "."}/ (tree)`);
363
+ }
364
+ continue;
365
+ }
366
+ let files = [];
367
+ try {
368
+ files = await fg(raw, {
369
+ cwd: cwd2,
370
+ ignore: IGNORE,
371
+ onlyFiles: true,
372
+ dot: true,
373
+ suppressErrors: true,
374
+ absolute: false
375
+ });
376
+ } catch {
377
+ files = [];
378
+ }
379
+ if (files.length === 0) {
380
+ try {
381
+ if ((await stat(direct)).isFile()) files = [relative(cwd2, direct)];
382
+ } catch {
383
+ }
384
+ }
385
+ files = files.sort().slice(0, MAX_FILES_PER_MENTION);
386
+ for (const rel of files) {
387
+ if (spent >= tokenBudget) break;
388
+ if (seen.has(rel)) continue;
389
+ seen.add(rel);
390
+ let buf;
391
+ try {
392
+ buf = await readFile2(resolve(cwd2, rel), "utf8");
393
+ } catch {
394
+ continue;
395
+ }
396
+ if (looksBinary(buf)) continue;
397
+ const cap = Math.min(PER_FILE_TOKEN_CAP, tokenBudget - spent);
398
+ let body = buf;
399
+ let truncated = false;
400
+ if (estTokens(body) > cap) {
401
+ body = body.slice(0, cap * CHARS_PER_TOKEN);
402
+ truncated = true;
403
+ }
404
+ const fence = body.includes("```") ? "````" : "```";
405
+ const header = truncated ? `${rel} (truncated to ~${cap} tokens of ${estTokens(buf)})` : rel;
406
+ blocks.push(`${header}
407
+ ${fence}
408
+ ${body}
409
+ ${fence}`);
410
+ spent += estTokens(body);
411
+ attached.push(truncated ? `${rel} (truncated)` : rel);
412
+ }
413
+ }
414
+ if (blocks.length === 0) return { context: "", notice: null };
415
+ const context = "The user attached these files with @-mentions. Use them as primary context for this turn:\n\n" + blocks.join("\n\n");
416
+ const notice = `attached ${attached.length} item(s): ${attached.slice(0, 8).join(", ")}${attached.length > 8 ? `, +${attached.length - 8} more` : ""} (~${spent} tokens)`;
417
+ return { context, notice };
418
+ }
419
+
420
+ // src/subagent.ts
421
+ import { generateText, stepCountIs, tool } from "ai";
422
+ import { z } from "zod";
423
+ var READONLY_TOOLS = ["read_file", "glob", "grep", "list_dir"];
424
+ function readonlyToolset(ctx) {
425
+ const all = buildTools(ctx);
426
+ const out = {};
427
+ for (const k of READONLY_TOOLS) if (all[k]) out[k] = all[k];
428
+ return out;
429
+ }
430
+ var SUBAGENT_SYSTEM = "You are a focused sub-agent spawned by a lead engineer. You have READ-ONLY tools (read_file, glob, grep, list_dir). Investigate ONLY the task you are given, then return a tight, factual answer: what you found, with file:line evidence where it applies. Do not pad, do not speculate beyond the evidence, do not attempt edits.";
431
+ function buildSubagentTool(deps) {
432
+ const maxSteps = deps.maxSteps ?? 12;
433
+ const maxAgents = deps.maxAgents ?? 6;
434
+ const subtools = readonlyToolset(deps.ctx);
435
+ return {
436
+ spawn_agents: tool({
437
+ description: "Run several READ-ONLY sub-agents IN PARALLEL, each on its own focused task, and get their answers back together. Use for breadth \u2014 searching/reviewing/investigating multiple areas at once is far faster than doing them one by one yourself. Each sub-agent can read, glob, and grep but CANNOT edit. Returns one result per task.",
438
+ inputSchema: z.object({
439
+ tasks: z.array(
440
+ z.object({
441
+ label: z.string().describe("Short label for this slice, e.g. 'auth' or 'billing routes'."),
442
+ task: z.string().describe("The self-contained instruction for this sub-agent.")
443
+ })
444
+ ).min(1).max(maxAgents).describe(`1\u2013${maxAgents} independent tasks to run concurrently.`)
445
+ }),
446
+ execute: async ({ tasks: tasks2 }) => {
447
+ line(c.dim(` \u21C9 spawning ${tasks2.length} sub-agent(s) in parallel`));
448
+ const results = await Promise.all(
449
+ tasks2.map(async ({ label, task }) => {
450
+ try {
451
+ const r = await generateText({
452
+ model: deps.model,
453
+ system: SUBAGENT_SYSTEM,
454
+ prompt: task,
455
+ tools: subtools,
456
+ stopWhen: stepCountIs(maxSteps)
457
+ });
458
+ line(c.dim(` \u21B3 ${c.cyan(label)} done`));
459
+ return { label, ok: true, result: r.text.trim() };
460
+ } catch (e) {
461
+ line(c.dim(` \u21B3 ${c.cyan(label)} ${c.red("failed")}`));
462
+ return { label, ok: false, error: e.message };
463
+ }
464
+ })
465
+ );
466
+ return { ok: true, count: results.length, results };
467
+ }
468
+ })
469
+ };
470
+ }
471
+
472
+ // src/images.ts
473
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
474
+ import { resolve as resolve2, extname, relative as relative2, isAbsolute as isAbsolute2, join as join3 } from "path";
475
+ import { execFile } from "child_process";
476
+ import { tmpdir, platform as platform2 } from "os";
477
+ var EXT_TO_MEDIA = {
478
+ ".png": "image/png",
479
+ ".jpg": "image/jpeg",
480
+ ".jpeg": "image/jpeg",
481
+ ".gif": "image/gif",
482
+ ".webp": "image/webp"
483
+ };
484
+ var PER_IMAGE_BYTES = 6 * 1024 * 1024;
485
+ var TOTAL_BYTES = 15 * 1024 * 1024;
486
+ function wantsClipboard(message) {
487
+ return /(^|\s)@(clip|clipboard|paste)\b/i.test(message);
488
+ }
489
+ function run(cmd, args) {
490
+ return new Promise((res) => {
491
+ execFile(cmd, args, (err) => res(!err));
492
+ });
493
+ }
494
+ async function grabClipboardImage() {
495
+ const out = join3(tmpdir(), `zeta-clip-${process.pid}-${process.hrtime.bigint()}.png`);
496
+ const os = platform2();
497
+ if (os === "darwin") {
498
+ const script = `set thePath to "${out}"
499
+ try
500
+ set pngData to (the clipboard as \xABclass PNGf\xBB)
501
+ on error
502
+ return "NOIMAGE"
503
+ end try
504
+ set fp to open for access (POSIX file thePath) with write permission
505
+ write pngData to fp
506
+ close access fp
507
+ return "OK"`;
508
+ const ok = await run("osascript", ["-e", script]);
509
+ if (!ok) return null;
510
+ } else if (os === "linux") {
511
+ const ok = await run("bash", ["-c", `xclip -selection clipboard -t image/png -o > "${out}"`]);
512
+ if (!ok) return null;
513
+ } else {
514
+ return null;
515
+ }
516
+ try {
517
+ const st = await stat2(out);
518
+ return st.isFile() && st.size > 0 ? out : null;
519
+ } catch {
520
+ return null;
521
+ }
522
+ }
523
+ function imageTokens(message) {
524
+ const out = /* @__PURE__ */ new Set();
525
+ const re = /@?([~A-Za-z0-9._\-/]+\.(?:png|jpe?g|gif|webp))\b/gi;
526
+ for (let m = re.exec(message); m; m = re.exec(message)) out.add(m[1]);
527
+ return [...out];
528
+ }
529
+ async function scanImages(message, cwd2) {
530
+ const tokens = imageTokens(message);
531
+ const images = [];
532
+ const skipped = [];
533
+ let total = 0;
534
+ if (wantsClipboard(message)) {
535
+ const clip = await grabClipboardImage();
536
+ if (clip) {
537
+ try {
538
+ const buf = await readFile3(clip);
539
+ if (buf.length <= PER_IMAGE_BYTES) {
540
+ images.push({
541
+ path: "clipboard",
542
+ dataUrl: `data:image/png;base64,${buf.toString("base64")}`,
543
+ mediaType: "image/png",
544
+ bytes: buf.length
545
+ });
546
+ total += buf.length;
547
+ } else {
548
+ skipped.push("clipboard (over the size cap)");
549
+ }
550
+ } catch {
551
+ skipped.push("clipboard (unreadable)");
552
+ }
553
+ } else {
554
+ skipped.push("clipboard (no image found)");
555
+ }
556
+ }
557
+ for (const tok of tokens) {
558
+ const raw = tok.startsWith("~/") ? join3(process.env.HOME ?? "", tok.slice(2)) : tok;
559
+ const abs = isAbsolute2(raw) ? raw : resolve2(cwd2, raw);
560
+ const mediaType = EXT_TO_MEDIA[extname(abs).toLowerCase()];
561
+ if (!mediaType) continue;
562
+ let bytes;
563
+ try {
564
+ const st = await stat2(abs);
565
+ if (!st.isFile()) continue;
566
+ bytes = st.size;
567
+ } catch {
568
+ continue;
569
+ }
570
+ const label = isAbsolute2(raw) ? raw : relative2(cwd2, abs);
571
+ if (bytes > PER_IMAGE_BYTES || total + bytes > TOTAL_BYTES) {
572
+ skipped.push(`${label} (${Math.round(bytes / 1024)}KB \u2014 over the size cap)`);
573
+ continue;
574
+ }
575
+ try {
576
+ const buf = await readFile3(abs);
577
+ const dataUrl = `data:${mediaType};base64,${buf.toString("base64")}`;
578
+ images.push({ path: label, dataUrl, mediaType, bytes });
579
+ total += bytes;
580
+ } catch {
581
+ skipped.push(`${label} (unreadable)`);
582
+ }
583
+ }
584
+ return { images, skipped };
585
+ }
586
+
587
+ // src/weburl.ts
588
+ var CHARS_PER_TOKEN2 = 4;
589
+ var DEFAULT_TOKEN_BUDGET2 = 12e3;
590
+ var PER_URL_TOKEN_CAP = 6e3;
591
+ var FETCH_TIMEOUT_MS = 12e3;
592
+ var MAX_BYTES = 4 * 1024 * 1024;
593
+ function estTokens2(s) {
594
+ return Math.ceil(s.length / CHARS_PER_TOKEN2);
595
+ }
596
+ function extractUrls(message) {
597
+ const out = /* @__PURE__ */ new Set();
598
+ const re = /@?(https?:\/\/[^\s<>")]+)/gi;
599
+ for (let m = re.exec(message); m; m = re.exec(message)) {
600
+ out.add(m[1].replace(/[.,;:)\]]+$/, ""));
601
+ }
602
+ return [...out];
603
+ }
604
+ function htmlToText(html) {
605
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<\/(p|div|li|h[1-6]|tr|br|section|article)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
606
+ }
607
+ async function fetchReadable(url) {
608
+ const ctrl = new AbortController();
609
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
610
+ try {
611
+ const res = await fetch(url, {
612
+ signal: ctrl.signal,
613
+ redirect: "follow",
614
+ headers: { "user-agent": "zeta-g-cli/0.4 (+web-context)" }
615
+ });
616
+ if (!res.ok) return null;
617
+ const ctype = res.headers.get("content-type") ?? "";
618
+ const raw = await res.text();
619
+ const body = raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) : raw;
620
+ const text = /html/i.test(ctype) ? htmlToText(body) : body.trim();
621
+ return { text, finalUrl: res.url || url };
622
+ } catch {
623
+ return null;
624
+ } finally {
625
+ clearTimeout(timer);
626
+ }
627
+ }
628
+ async function expandUrls(message, tokenBudget = DEFAULT_TOKEN_BUDGET2) {
629
+ const urls = extractUrls(message);
630
+ if (urls.length === 0) return { context: "", notice: null };
631
+ const blocks = [];
632
+ const fetched = [];
633
+ const failed = [];
634
+ let spent = 0;
635
+ for (const url of urls) {
636
+ if (spent >= tokenBudget) break;
637
+ const r = await fetchReadable(url);
638
+ if (!r || !r.text) {
639
+ failed.push(url);
640
+ continue;
641
+ }
642
+ const cap = Math.min(PER_URL_TOKEN_CAP, tokenBudget - spent);
643
+ let body = r.text;
644
+ let truncated = false;
645
+ if (estTokens2(body) > cap) {
646
+ body = body.slice(0, cap * CHARS_PER_TOKEN2);
647
+ truncated = true;
648
+ }
649
+ const header = truncated ? `${r.finalUrl} (truncated to ~${cap} tokens)` : r.finalUrl;
650
+ blocks.push(`${header}
651
+ ${body}`);
652
+ spent += estTokens2(body);
653
+ fetched.push(r.finalUrl);
654
+ }
655
+ if (blocks.length === 0) {
656
+ return { context: "", notice: failed.length ? `couldn't fetch: ${failed.join(", ")}` : null };
657
+ }
658
+ const context = "The user linked these pages. Use them as context for this turn:\n\n" + blocks.join("\n\n---\n\n");
659
+ const noticeParts = [`fetched ${fetched.length} URL(s) (~${spent} tokens)`];
660
+ if (failed.length) noticeParts.push(`failed: ${failed.length}`);
661
+ return { context, notice: noticeParts.join(" \xB7 ") };
662
+ }
663
+
664
+ // src/export.ts
665
+ import { writeFileSync as writeFileSync2 } from "fs";
666
+ import { resolve as resolve3 } from "path";
667
+ function renderContent(content) {
668
+ if (typeof content === "string") return content.trim();
669
+ if (!Array.isArray(content)) return "";
670
+ const parts = [];
671
+ for (const p of content) {
672
+ const type = p.type;
673
+ if (type === "text" && typeof p.text === "string") parts.push(p.text.trim());
674
+ else if (type === "image") parts.push("_[image]_");
675
+ else if (type === "tool-call") parts.push(`\`\u2192 ${String(p.toolName ?? "tool")}\``);
676
+ else if (type === "tool-result") parts.push(`\`\u2713 ${String(p.toolName ?? "tool")}\``);
677
+ }
678
+ return parts.filter(Boolean).join("\n\n");
679
+ }
680
+ function renderTranscript(messages, meta) {
681
+ const head = [
682
+ `# zeta-g transcript`,
683
+ ``,
684
+ `- model: ${meta.model}`,
685
+ meta.startedAt ? `- started: ${meta.startedAt}` : "",
686
+ `- messages: ${messages.length}`,
687
+ ``,
688
+ `---`,
689
+ ``
690
+ ].filter((l) => l !== "").join("\n");
691
+ const body = messages.map((m) => {
692
+ const text = renderContent(m.content);
693
+ if (!text) return "";
694
+ const who = m.role === "user" ? "\u{1F9D1} User" : m.role === "assistant" ? "\u{1F916} Zeta-G" : m.role === "tool" ? "\u{1F527} Tool" : m.role;
695
+ return `### ${who}
696
+
697
+ ${text}`;
698
+ }).filter(Boolean).join("\n\n");
699
+ return `${head}
700
+
701
+ ${body}
702
+ `;
703
+ }
704
+ function writeTranscript(messages, meta, file, cwd2) {
705
+ const abs = resolve3(cwd2, file);
706
+ writeFileSync2(abs, renderTranscript(messages, meta), "utf8");
707
+ return abs;
708
+ }
709
+
710
+ // src/cli.ts
711
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
712
+ import { homedir as homedir4 } from "os";
713
+ import { join as join6 } from "path";
714
+
715
+ // src/update-check.ts
716
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
717
+ import { homedir as homedir2 } from "os";
718
+ import { join as join4 } from "path";
719
+ var PKG = "wholestack";
720
+ var CACHE = join4(homedir2(), ".zeta-g", "update-check.json");
721
+ var DAY_MS = 24 * 60 * 60 * 1e3;
722
+ function readCache() {
723
+ try {
724
+ return JSON.parse(readFileSync2(CACHE, "utf8"));
725
+ } catch {
726
+ return { lastCheck: 0, latest: null };
727
+ }
728
+ }
729
+ function writeCache(c2) {
730
+ try {
731
+ mkdirSync2(join4(homedir2(), ".zeta-g"), { recursive: true });
732
+ writeFileSync3(CACHE, JSON.stringify(c2));
733
+ } catch {
734
+ }
735
+ }
736
+ function isNewer(a, b) {
737
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
738
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
739
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
740
+ const x = pa[i] ?? 0;
741
+ const y = pb[i] ?? 0;
742
+ if (x !== y) return x > y;
743
+ }
744
+ return false;
745
+ }
746
+ async function checkForUpdate(current) {
747
+ if (process.env.ZETA_NO_UPDATE_CHECK || !existsSync2) return null;
748
+ const cache = readCache();
749
+ const fresh = Date.now() - cache.lastCheck < DAY_MS;
750
+ let latest = cache.latest;
751
+ if (!fresh) {
752
+ try {
753
+ const ctrl = new AbortController();
754
+ const t = setTimeout(() => ctrl.abort(), 1200);
755
+ const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
756
+ signal: ctrl.signal,
757
+ headers: { accept: "application/json" }
758
+ });
759
+ clearTimeout(t);
760
+ if (res.ok) {
761
+ const json = await res.json();
762
+ latest = json.version ?? null;
763
+ }
764
+ } catch {
765
+ latest = cache.latest;
766
+ }
767
+ writeCache({ lastCheck: Date.now(), latest });
768
+ }
769
+ return latest && isNewer(latest, current) ? latest : null;
770
+ }
771
+
772
+ // src/access.ts
773
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
774
+ import { homedir as homedir3 } from "os";
775
+ import { join as join5 } from "path";
776
+ var CACHE2 = join5(homedir3(), ".zeta-g", "access.json");
777
+ var TTL_MS = 60 * 60 * 1e3;
778
+ function readCache2() {
779
+ try {
780
+ return JSON.parse(readFileSync3(CACHE2, "utf8"));
781
+ } catch {
782
+ return null;
783
+ }
784
+ }
785
+ function writeCache2(c2) {
786
+ try {
787
+ mkdirSync3(join5(homedir3(), ".zeta-g"), { recursive: true });
788
+ writeFileSync4(CACHE2, JSON.stringify(c2));
789
+ } catch {
790
+ }
791
+ }
792
+ async function verifyAccess(webUrl, token) {
793
+ if (!token) return { active: false, tier: null, degraded: false };
794
+ const cached = readCache2();
795
+ if (cached && Date.now() - cached.checkedAt < TTL_MS) {
796
+ return { active: cached.active, tier: cached.tier, degraded: false };
797
+ }
798
+ try {
799
+ const ctrl = new AbortController();
800
+ const t = setTimeout(() => ctrl.abort(), 2500);
801
+ const res = await fetch(`${webUrl.replace(/\/$/, "")}/api/cli/verify`, {
802
+ headers: { authorization: `Bearer ${token}` },
803
+ signal: ctrl.signal
804
+ });
805
+ clearTimeout(t);
806
+ if (res.status === 401) {
807
+ writeCache2({ checkedAt: Date.now(), active: false, tier: null });
808
+ return { active: false, tier: null, degraded: false };
809
+ }
810
+ const json = await res.json();
811
+ const active = Boolean(json.active);
812
+ writeCache2({ checkedAt: Date.now(), active, tier: json.tier ?? null });
813
+ return { active, tier: json.tier ?? null, degraded: false };
814
+ } catch {
815
+ return { active: true, tier: cached?.tier ?? null, degraded: true };
816
+ }
817
+ }
818
+ function showPaywall(loggedIn, webUrl) {
819
+ const w = 52;
820
+ line();
821
+ line(" " + c.cyan("\u256D" + "\u2500".repeat(w) + "\u256E"));
822
+ const row = (s) => line(" " + c.cyan("\u2502") + " " + s.padEnd(w - 2) + " " + c.cyan("\u2502"));
823
+ row(c.bold("zeta needs a subscription"));
824
+ row("");
825
+ if (!loggedIn) {
826
+ row(c.dim("You're not signed in."));
827
+ row("Run " + c.cyan("zeta login") + c.dim(" then subscribe."));
828
+ } else {
829
+ row(c.dim("No active paid plan on this account."));
830
+ row("Subscribe to use zeta in the terminal.");
831
+ }
832
+ row("");
833
+ row(c.dim("Plans: ") + c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
834
+ line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
835
+ line();
836
+ }
837
+
838
+ // src/art.ts
839
+ var ART_PACK = [
840
+ [" \u2588\u2588\u2588\u2588\u2588\u2588\u2588", " \u2588\u2588\u2554\u255D", " \u2588\u2588\u2554\u255D ", " \u2588\u2588\u2554\u255D ", " \u2588\u2588\u2588\u2588\u2588\u2588\u2588 "],
841
+ [" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", " \u2502 \u2571\u2571 \u2502", " \u2502 \u2571\u2571 \u2502", " \u2502\u2571\u2571 \u2502", " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"],
842
+ [" \u259F\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2599", " \u259F\u2588\u259B ", " \u259F\u2588\u259B ", " \u259F\u2588\u259B ", " \u259C\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259B "],
843
+ [" \u250C\u2500\u2510\u250C\u2500\u2510\u250C\u252C\u2510\u250C\u2500\u2510", " \u250C\u2500\u2518\u251C\u2524 \u2502 \u251C\u2500\u2524", " \u2514\u2500\u2518\u2514\u2500\u2518 \u2534 \u2534 \u2534"],
844
+ [" \u2571\u2572 ", " \u2571\u2500\u2500\u2572 ", " \u2571 \u2571\u2571 \u2572 ", "\u2571 \u2571\u2571 \u2572 ", "\u2572\u2571\u2571\u2500\u2500\u2500\u2500\u2500\u2572"]
845
+ ];
846
+ function randomArt(seed) {
847
+ const i = seed != null ? seed % ART_PACK.length : Math.floor(Math.random() * ART_PACK.length);
848
+ return ART_PACK[(i % ART_PACK.length + ART_PACK.length) % ART_PACK.length];
849
+ }
850
+ var vlen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
851
+ function fetchPanel(fields, art = randomArt()) {
852
+ const shown = fields.filter((f) => f.value);
853
+ const labelW = Math.max(0, ...shown.map((f) => f.label.length));
854
+ const artW = Math.max(0, ...art.map(vlen));
855
+ const rows = Math.max(art.length, shown.length);
856
+ const out = [];
857
+ out.push("");
858
+ for (let i = 0; i < rows; i++) {
859
+ const left = (art[i] ?? "").padEnd(artW + 2);
860
+ const f = shown[i];
861
+ const right = f ? c.dim(f.label.padStart(labelW) + " ") + c.bold(f.value) : "";
862
+ out.push(" " + c.cyan(left) + right);
863
+ }
864
+ out.push("");
865
+ for (const l of out) process.stdout.write(l + "\n");
866
+ }
867
+
269
868
  // src/cli.ts
869
+ import { readFileSync as readFileSync4 } from "fs";
870
+ import { basename } from "path";
871
+ var VERSION = "0.5.0";
872
+ function gitBranch(dir) {
873
+ try {
874
+ const head = readFileSync4(join6(dir, ".git", "HEAD"), "utf8").trim();
875
+ const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
876
+ return m ? m[1] : head.slice(0, 7);
877
+ } catch {
878
+ return "";
879
+ }
880
+ }
270
881
  function parse(raw) {
271
882
  const a = {
272
883
  zetaUrl: process.env.ZETA_API_URL ?? "https://wholestack.ai",
@@ -294,6 +905,8 @@ function parse(raw) {
294
905
  const m = raw[++i];
295
906
  if (isPermissionMode(m)) a.mode = m;
296
907
  } else if (t === "--plan") a.plan = true;
908
+ else if (t === "--persona") a.persona = raw[++i];
909
+ else if (t === "--nzt48" || t === "--nzt-48") a.persona = "nzt-48";
297
910
  else if (t === "--think") a.think = true;
298
911
  else if (t === "--no-think") a.think = false;
299
912
  else if (t === "--mcp") a.mcp = (raw[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -324,12 +937,15 @@ ${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmo
324
937
  zeta-g prove <dir> <Contract> --property <k> prove one invariant
325
938
  zeta-g verify <dir> <Contract> --cert c.json passthrough + signed certificate
326
939
  ${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
940
+ zeta-g --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
327
941
 
328
942
  ${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)
943
+ -m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
944
+ (lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
331
945
  --mode <m> default | acceptEdits | plan | yolo (permission mode)
332
946
  --plan start in read-only plan mode
947
+ --nzt48 run as NZT-48, the web3 special-ops persona
948
+ --persona <id> layer a named persona on the base agent
333
949
  --think/--no-think toggle extended thinking (when a tier supports it)
334
950
  --continue resume the most recent session
335
951
  --resume [id] resume a session (id, or the latest)
@@ -352,7 +968,14 @@ ${c.bold("Auth")} ${c.dim("(set once, then just run zeta-g)")}
352
968
  zeta-g login --build <key> ZETA build-engine key (for /build)
353
969
 
354
970
  ${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`;
971
+ /model /cost /tools /undo /redo /checkpoints /compact /resume /memory /mcp /mode /think /init /doctor /clear /exit
972
+ /diff /commit /pr ${c.dim("git flow \u2014 review, commit (auto-written msg), open a PR")}
973
+ /tasks ${c.dim("background jobs \u2014 run_background runs builds/tests while you work")}
974
+ /export [file.md] ${c.dim("write the conversation transcript to markdown")}
975
+ ${c.dim("@path \xB7 @dir/ \xB7 @glob attach files to the turn (token-budgeted, Tab-completes)")}
976
+ ${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
977
+ ${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
978
+ ${c.dim("spawn_agents tool fan a task out to parallel read-only sub-agents")}`;
356
979
  async function runSecuritySubcommand(raw) {
357
980
  const verb = raw[0];
358
981
  if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
@@ -375,6 +998,19 @@ async function runSecuritySubcommand(raw) {
375
998
  line(r.ok ? c.green(" \u2713 verified") : c.red(" \u2717 NOT verified"));
376
999
  return r.code;
377
1000
  }
1001
+ function maybeYoloNotice() {
1002
+ try {
1003
+ const dir = join6(homedir4(), ".zeta-g");
1004
+ const marker = join6(dir, "yolo-notice-seen");
1005
+ if (existsSync3(marker)) return;
1006
+ line(
1007
+ " " + c.yellow("\u26A1 yolo mode") + c.dim(" \u2014 actions run without confirmation. ") + c.cyan("/mode default") + c.dim(" to require approvals \xB7 /undo reverts.") + "\n"
1008
+ );
1009
+ mkdirSync4(dir, { recursive: true });
1010
+ writeFileSync5(marker, (/* @__PURE__ */ new Date()).toISOString());
1011
+ } catch {
1012
+ }
1013
+ }
378
1014
  function webBaseUrl() {
379
1015
  return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
380
1016
  }
@@ -473,13 +1109,21 @@ async function main() {
473
1109
  if (sub !== null) exit(sub);
474
1110
  const args = parse(rawArgs);
475
1111
  if (args.version) {
476
- line("wholestack 0.4.0");
1112
+ line(`wholestack ${VERSION}`);
477
1113
  return;
478
1114
  }
479
1115
  if (args.help) {
480
1116
  line(HELP);
481
1117
  return;
482
1118
  }
1119
+ {
1120
+ const token = process.env.ZETA_API_KEY;
1121
+ const access = await verifyAccess(webBaseUrl(), token);
1122
+ if (!access.active) {
1123
+ showPaywall(Boolean(token), webBaseUrl());
1124
+ exit(1);
1125
+ }
1126
+ }
483
1127
  let modelKey;
484
1128
  try {
485
1129
  modelKey = resolveModelKey(args.model);
@@ -497,7 +1141,11 @@ async function main() {
497
1141
  const isTty = !!stdin.isTTY;
498
1142
  const oneShot = args.prompt.length > 0;
499
1143
  const workdir = cwd();
500
- const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : isTty ? "default" : "acceptEdits";
1144
+ const persistedMode = isPermissionMode(
1145
+ process.env.ZETA_PERMISSION_MODE
1146
+ ) ? process.env.ZETA_PERMISSION_MODE : void 0;
1147
+ const yoloIsDefault = !args.plan && !args.mode && !args.yes && !persistedMode && isTty;
1148
+ const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
501
1149
  const memory = await loadProjectMemory(workdir);
502
1150
  const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
503
1151
  const memoryText = [memory.text, plugins.systemText].filter(Boolean).join("\n\n") || void 0;
@@ -507,7 +1155,6 @@ async function main() {
507
1155
  disabled: args.noMcp,
508
1156
  extraServers: plugins.mcpServers
509
1157
  });
510
- const extraTools = { ...buildWebTools(), ...mcp.tools };
511
1158
  const hooks = new HookRunner(mergeHookSets(loadHookFiles(workdir), plugins.hooks), workdir);
512
1159
  const registry = new CommandRegistry();
513
1160
  registry.loadCustom(workdir);
@@ -518,6 +1165,7 @@ async function main() {
518
1165
  if (shuttingDown) return;
519
1166
  shuttingDown = true;
520
1167
  killRunningApps();
1168
+ tasks.killAll();
521
1169
  await mcp.close().catch(() => {
522
1170
  });
523
1171
  ic?.close();
@@ -529,12 +1177,33 @@ async function main() {
529
1177
  onExit: () => {
530
1178
  line(c.dim(" bye."));
531
1179
  void shutdown(0);
1180
+ },
1181
+ // Starship-style context segments above the prompt box.
1182
+ contextBar: () => {
1183
+ const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename(workdir))];
1184
+ const br = gitBranch(workdir);
1185
+ if (br) seg.push(c.dim("\u2387 " + br));
1186
+ seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
1187
+ return seg.join(c.dim(" \xB7 "));
532
1188
  }
533
1189
  });
534
1190
  }
535
1191
  const confirm = isTty && ic ? ic.confirm.bind(ic) : void 0;
536
- const permissions = new Permissions(mode, confirm);
1192
+ const persistMode = (m) => {
1193
+ try {
1194
+ saveKey("ZETA_PERMISSION_MODE", m);
1195
+ } catch {
1196
+ }
1197
+ };
1198
+ const permissions = new Permissions(mode, confirm, persistMode);
537
1199
  const checkpoints = new CheckpointStore();
1200
+ const toolCtx = { cwd: workdir, zetaApiUrl: args.zetaUrl, buildMode: args.buildMode, permissions, checkpoints };
1201
+ const extraTools = {
1202
+ ...buildWebTools(),
1203
+ ...mcp.tools,
1204
+ // Parallel read-only sub-agents (needs the model handle the base tools lack).
1205
+ ...buildSubagentTool({ model, modelKey, ctx: toolCtx })
1206
+ };
538
1207
  let session;
539
1208
  let resumedMessages = null;
540
1209
  if (args.resume !== void 0 || args.cont) {
@@ -554,19 +1223,27 @@ async function main() {
554
1223
  const agent = new Agent({
555
1224
  model,
556
1225
  modelKey,
557
- ctx: { cwd: workdir, zetaApiUrl: args.zetaUrl, buildMode: args.buildMode, permissions, checkpoints },
1226
+ ctx: toolCtx,
558
1227
  extraTools,
559
1228
  hooks,
560
1229
  memoryText,
1230
+ persona: args.persona,
561
1231
  thinking,
1232
+ yolo: mode === "yolo",
562
1233
  maxSteps: 16,
563
1234
  contextWindow: modelContextWindow(modelKey),
564
1235
  session
565
1236
  });
566
1237
  if (resumedMessages) agent.replaceHistory(resumedMessages);
567
1238
  if (oneShot) {
568
- if (ic) await ic.runInterruptible((sig) => agent.send(args.prompt, sig));
569
- else await agent.send(args.prompt);
1239
+ const m = await expandMentions(args.prompt, workdir);
1240
+ if (m.notice) line(c.dim(` @ ${m.notice}`) + "\n");
1241
+ const oneShotText = m.context ? `${args.prompt}
1242
+
1243
+ ---
1244
+ ${m.context}` : args.prompt;
1245
+ if (ic) await ic.runInterruptible((sig) => agent.send(oneShotText, sig));
1246
+ else await agent.send(oneShotText);
570
1247
  await shutdown(0);
571
1248
  return;
572
1249
  }
@@ -575,10 +1252,28 @@ async function main() {
575
1252
  await shutdown(1);
576
1253
  return;
577
1254
  }
578
- banner();
1255
+ await banner();
1256
+ fetchPanel([
1257
+ { label: "model", value: modelLabel(modelKey) },
1258
+ { label: "mode", value: mode },
1259
+ { label: "dir", value: basename(workdir) },
1260
+ { label: "git", value: gitBranch(workdir) },
1261
+ { label: "node", value: process.version },
1262
+ { label: "memory", value: memory.sources.length ? `${memory.sources.length} file(s)` : "" },
1263
+ { label: "mcp", value: mcp.mounted.length ? `${mcp.mounted.length} mounted` : "" },
1264
+ { label: "zeta", value: VERSION }
1265
+ ]);
579
1266
  bootSummary(memory, plugins, mcp, mode, thinking);
580
1267
  if (mode === "plan") announcePlanMode();
1268
+ else if (yoloIsDefault) maybeYoloNotice();
581
1269
  if (resumedMessages) line(" " + c.dim(`resumed ${session.meta.id} \xB7 ${resumedMessages.length} messages`) + "\n");
1270
+ void checkForUpdate(VERSION).then((latest) => {
1271
+ if (latest) {
1272
+ line(
1273
+ " " + c.yellow(`\u2191 wholestack ${latest} available`) + c.dim(` (you have ${VERSION}) \xB7 `) + c.cyan("npm i -g wholestack") + "\n"
1274
+ );
1275
+ }
1276
+ });
582
1277
  const baseCtx = () => ({
583
1278
  cwd: workdir,
584
1279
  modelKey,
@@ -593,9 +1288,9 @@ async function main() {
593
1288
  checkpoints,
594
1289
  print: (s = "") => line(s)
595
1290
  });
596
- const runTurn = async (text) => {
1291
+ const runTurn = async (text, images) => {
597
1292
  const t0 = Date.now();
598
- await ic.runInterruptible((sig) => agent.send(text, sig));
1293
+ await ic.runInterruptible((sig) => agent.send(text, sig, images));
599
1294
  statusLine({
600
1295
  model: modelLabel(modelKey),
601
1296
  tokens: agent.usage.totalTokens,
@@ -607,6 +1302,11 @@ async function main() {
607
1302
  });
608
1303
  };
609
1304
  for (; ; ) {
1305
+ for (const done of tasks.drainCompleted()) {
1306
+ const mark = done.status === "exited" ? c.green("\u2713") : c.red("\u2717");
1307
+ const code = done.exitCode == null ? "" : ` (exit ${done.exitCode})`;
1308
+ line(` ${mark} ${c.dim("background")} ${c.cyan(done.id)} ${done.display}${c.dim(code)}`);
1309
+ }
610
1310
  const input = await ic.readLine(c.cyan(" \u203A "));
611
1311
  if (!input) continue;
612
1312
  if (input.startsWith("/")) {
@@ -636,7 +1336,12 @@ async function main() {
636
1336
  }
637
1337
  } else if (res.type === "setMode") {
638
1338
  permissions.setMode(res.mode);
639
- line(" " + c.dim(`mode: ${res.mode}`) + "\n");
1339
+ if (res.mode !== "plan") {
1340
+ persistMode(res.mode);
1341
+ line(" " + c.dim(`mode: ${res.mode} \xB7 remembered for next session`) + "\n");
1342
+ } else {
1343
+ line(" " + c.dim(`mode: ${res.mode}`) + "\n");
1344
+ }
640
1345
  if (res.mode === "plan") announcePlanMode();
641
1346
  } else if (res.type === "toggleThinking") {
642
1347
  if (supportsThinking(modelKey)) {
@@ -655,13 +1360,54 @@ async function main() {
655
1360
  } else {
656
1361
  line(" " + c.red(`no session ${res.sessionId}`) + "\n");
657
1362
  }
1363
+ } else if (res.type === "export") {
1364
+ const file = res.file ?? `zeta-transcript-${gitBranch(workdir) || "session"}.md`;
1365
+ try {
1366
+ const abs = writeTranscript(agent.snapshot(), { model: modelLabel(modelKey) }, file, workdir);
1367
+ line(" " + c.green(`\u2713 transcript \u2192 ${abs}`) + "\n");
1368
+ } catch (e) {
1369
+ line(" " + c.red(e.message) + "\n");
1370
+ }
658
1371
  }
659
1372
  continue;
660
1373
  }
661
- userBox(input);
662
- await runTurn(input);
1374
+ if (!process.stdin.isTTY || process.env.NO_COLOR) userBox(input);
1375
+ const mentioned = await expandMentions(input, workdir);
1376
+ if (mentioned.notice) line(c.dim(` @ ${mentioned.notice}`) + "\n");
1377
+ const web = await expandUrls(input);
1378
+ if (web.notice) line(c.dim(` \u2301 ${web.notice}`) + "\n");
1379
+ const extraContext = [mentioned.context, web.context].filter(Boolean).join("\n\n---\n\n");
1380
+ const imgs = await scanImages(input, workdir);
1381
+ let attachments;
1382
+ if (imgs.images.length > 0) {
1383
+ if (!visionCapable(modelKey) && process.env.OPENROUTER_API_KEY) {
1384
+ try {
1385
+ const vkey = resolveModelKey("vision");
1386
+ modelKey = vkey;
1387
+ agent.setModel(resolveModel(vkey), vkey, modelContextWindow(vkey));
1388
+ agent.setThinking(false);
1389
+ line(c.dim(` \u29C9 image attached \u2192 switched to ${modelLabel(vkey)} (/model zeta-g1 to switch back)`) + "\n");
1390
+ } catch (e) {
1391
+ line(c.red(` \u29C9 couldn't switch to the vision lane: ${e.message}`) + "\n");
1392
+ }
1393
+ }
1394
+ if (visionCapable(modelKey)) {
1395
+ attachments = imgs.images.map((i) => ({ dataUrl: i.dataUrl }));
1396
+ line(c.dim(` \u29C9 attached ${imgs.images.length} image(s): ${imgs.images.map((i) => i.path).join(", ")}`) + "\n");
1397
+ } else {
1398
+ line(
1399
+ c.yellow(` \u29C9 ${imgs.images.length} image(s) ignored \u2014 ${modelLabel(modelKey)} is text-only.`) + c.dim(" set OPENROUTER_API_KEY (or /model vision) to enable Zeta-G1.0 Vision") + "\n"
1400
+ );
1401
+ }
1402
+ }
1403
+ if (imgs.skipped.length) line(c.dim(` \u29C9 skipped: ${imgs.skipped.join(", ")}`) + "\n");
1404
+ await runTurn(extraContext ? `${input}
1405
+
1406
+ ---
1407
+ ${extraContext}` : input, attachments);
663
1408
  }
664
1409
  killRunningApps();
1410
+ tasks.killAll();
665
1411
  await mcp.close().catch(() => {
666
1412
  });
667
1413
  ic.close();