wholestack 0.5.5 → 0.5.6

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/README.md CHANGED
@@ -43,6 +43,23 @@ on wholestack.ai.
43
43
  | `-p, --print` | non-interactive, print result and exit |
44
44
  | `--zeta-url <url>` | override the engine origin (defaults to https://wholestack.ai) |
45
45
 
46
+ ## Projects — one spine with the web IDE
47
+
48
+ Everything you build (terminal, wholestack.ai/build, /demo) lands in the same
49
+ project list. The terminal can read and write the exact working tree the web
50
+ IDE edits:
51
+
52
+ ```bash
53
+ wholestack projects # list your platform projects
54
+ wholestack pull <id> [dir] # download a project's working tree
55
+ wholestack push [dir] # sync local edits back to the web IDE
56
+ wholestack open [buildId] # open a generated build in the web IDE
57
+ ```
58
+
59
+ `pull` links the directory via `.wholestack/project.json`, so a later `push`
60
+ (and `open` inside a delivered app) needs no arguments. Pushes skip
61
+ `node_modules`, build output, `.env*` and binaries.
62
+
46
63
  ## Security verification
47
64
 
48
65
  Run ShipGate's gates on a generated contract:
@@ -455,16 +455,149 @@ function renderNewFileDiff(path, content, opts = {}) {
455
455
  line();
456
456
  }
457
457
 
458
+ // src/platform.ts
459
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
460
+ import { join, resolve, sep, dirname, normalize as pathNormalize } from "path";
461
+ import fg from "fast-glob";
462
+ function authHeaders() {
463
+ const key = process.env.ZETA_API_KEY?.trim();
464
+ if (!key) return null;
465
+ return { authorization: `Bearer ${key}`, "content-type": "application/json" };
466
+ }
467
+ var NO_KEY = "set ZETA_API_KEY (run `wholestack login`) first.";
468
+ async function apiJson(url, init) {
469
+ const headers = authHeaders();
470
+ if (!headers) return { ok: false, error: NO_KEY };
471
+ let resp;
472
+ try {
473
+ resp = await fetch(url, { ...init, headers: { ...headers, ...init?.headers ?? {} } });
474
+ } catch (e) {
475
+ return { ok: false, error: `cannot reach the platform: ${e.message}` };
476
+ }
477
+ const data = await resp.json().catch(() => null);
478
+ if (!resp.ok) {
479
+ return {
480
+ ok: false,
481
+ status: resp.status,
482
+ error: data?.error ?? (resp.status === 401 ? "token rejected \u2014 run `wholestack login` again." : `platform returned ${resp.status}`)
483
+ };
484
+ }
485
+ return { ok: true, data };
486
+ }
487
+ async function listProjects(apiUrl) {
488
+ const r = await apiJson(`${apiUrl}/api/projects`);
489
+ if (!r.ok) return { ok: false, error: r.error };
490
+ return { ok: true, projects: r.data.projects ?? [] };
491
+ }
492
+ async function pullWorkingTree(apiUrl, projectId) {
493
+ const r = await apiJson(
494
+ `${apiUrl}/api/projects/${encodeURIComponent(projectId)}/working-tree`
495
+ );
496
+ if (!r.ok) return { ok: false, error: r.error };
497
+ return { ok: true, name: r.data.project?.name, files: r.data.files ?? [] };
498
+ }
499
+ async function pushWorkingTree(apiUrl, projectId, files) {
500
+ const r = await apiJson(
501
+ `${apiUrl}/api/projects/${encodeURIComponent(projectId)}/working-tree`,
502
+ { method: "PATCH", body: JSON.stringify({ files }) }
503
+ );
504
+ if (!r.ok) return { ok: false, error: r.error };
505
+ return { ok: true };
506
+ }
507
+ async function openBuildInEditor(apiUrl, buildId) {
508
+ const r = await apiJson(
509
+ `${apiUrl}/api/zeta/build/${encodeURIComponent(buildId)}/open-in-editor`,
510
+ { method: "POST" }
511
+ );
512
+ if (!r.ok) return { ok: false, error: r.error };
513
+ return { ok: true, projectId: r.data.projectId, editorUrl: r.data.editorUrl };
514
+ }
515
+ var LINK_DIR = ".wholestack";
516
+ var LINK_FILE = "project.json";
517
+ function readProjectLink(dir) {
518
+ try {
519
+ const raw = readFileSync(join(dir, LINK_DIR, LINK_FILE), "utf8");
520
+ const json = JSON.parse(raw);
521
+ return json && typeof json === "object" ? json : null;
522
+ } catch {
523
+ return null;
524
+ }
525
+ }
526
+ function writeProjectLink(dir, link) {
527
+ try {
528
+ const prev = readProjectLink(dir) ?? {};
529
+ mkdirSync(join(dir, LINK_DIR), { recursive: true });
530
+ writeFileSync(
531
+ join(dir, LINK_DIR, LINK_FILE),
532
+ JSON.stringify({ ...prev, ...link }, null, 2) + "\n"
533
+ );
534
+ } catch {
535
+ }
536
+ }
537
+ function writeWorkingTree(destDir, files) {
538
+ const root = resolve(destDir);
539
+ let written = 0;
540
+ for (const f of files) {
541
+ if (!f?.path || typeof f.content !== "string") continue;
542
+ const rel = pathNormalize(f.path).replace(/^(\.\.(\/|\\|$))+/, "").replace(/^\/+/, "");
543
+ const full = resolve(root, rel);
544
+ if (full !== root && !full.startsWith(root + sep)) continue;
545
+ mkdirSync(dirname(full), { recursive: true });
546
+ writeFileSync(full, f.content);
547
+ written += 1;
548
+ }
549
+ return written;
550
+ }
551
+ var PUSH_IGNORE = [
552
+ "**/node_modules/**",
553
+ "**/.next/**",
554
+ "**/.git/**",
555
+ "**/dist/**",
556
+ "**/.turbo/**",
557
+ "**/.wholestack/**",
558
+ "**/.zeta-g/**",
559
+ "**/*.lock",
560
+ "**/pnpm-lock.yaml",
561
+ "**/.env*"
562
+ ];
563
+ var MAX_PUSH_FILES = 2e3;
564
+ var MAX_FILE_BYTES = 512 * 1024;
565
+ async function collectLocalFiles(dir) {
566
+ const paths = await fg("**/*", {
567
+ cwd: dir,
568
+ dot: true,
569
+ onlyFiles: true,
570
+ ignore: PUSH_IGNORE,
571
+ followSymbolicLinks: false
572
+ });
573
+ const files = [];
574
+ let skipped = 0;
575
+ for (const rel of paths.slice(0, MAX_PUSH_FILES)) {
576
+ try {
577
+ const buf = readFileSync(join(dir, rel));
578
+ if (buf.length > MAX_FILE_BYTES || buf.includes(0)) {
579
+ skipped += 1;
580
+ continue;
581
+ }
582
+ files.push({ path: rel.split(sep).join("/"), content: buf.toString("utf8") });
583
+ } catch {
584
+ skipped += 1;
585
+ }
586
+ }
587
+ skipped += Math.max(0, paths.length - MAX_PUSH_FILES);
588
+ return { files, skipped };
589
+ }
590
+
458
591
  // src/prover.ts
459
592
  import { spawn as spawn2 } from "child_process";
460
- import { join as join2 } from "path";
461
- import { existsSync as existsSync2 } from "fs";
593
+ import { join as join3 } from "path";
594
+ import { existsSync as existsSync3 } from "fs";
462
595
 
463
596
  // src/zeta-engine.ts
464
597
  import { spawn } from "child_process";
465
598
  import { tmpdir } from "os";
466
- import { join, dirname, normalize as pathNormalize, resolve, sep } from "path";
467
- import { existsSync, mkdirSync, writeFileSync } from "fs";
599
+ import { join as join2, dirname as dirname2, normalize as pathNormalize2, resolve as resolve2, sep as sep2 } from "path";
600
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
468
601
  import { fileURLToPath } from "url";
469
602
  function normalize(result) {
470
603
  const files = Array.isArray(result.files) ? result.files : [];
@@ -651,25 +784,26 @@ async function deliverBuild(zetaApiUrl, buildId, destDir, onPhase) {
651
784
  }
652
785
  const data = await resp.json().catch(() => ({}));
653
786
  const files = Array.isArray(data.files) ? data.files : [];
654
- const root = resolve(destDir);
787
+ const root = resolve2(destDir);
655
788
  let written = 0;
656
789
  for (const f of files) {
657
790
  if (!f?.path || typeof f.content !== "string") continue;
658
- const rel = pathNormalize(f.path).replace(/^(\.\.(\/|\\|$))+/, "");
659
- const full = resolve(root, rel);
660
- if (full !== root && !full.startsWith(root + sep)) continue;
661
- mkdirSync(dirname(full), { recursive: true });
662
- writeFileSync(full, f.content);
791
+ const rel = pathNormalize2(f.path).replace(/^(\.\.(\/|\\|$))+/, "");
792
+ const full = resolve2(root, rel);
793
+ if (full !== root && !full.startsWith(root + sep2)) continue;
794
+ mkdirSync2(dirname2(full), { recursive: true });
795
+ writeFileSync2(full, f.content);
663
796
  written += 1;
664
797
  onPhase?.(`wrote ${rel}`);
665
798
  }
799
+ writeProjectLink(root, { buildId });
666
800
  return { ok: true, written, dir: root };
667
801
  }
668
802
  function findRepoRoot(start) {
669
- let dir = start ?? dirname(fileURLToPath(import.meta.url));
803
+ let dir = start ?? dirname2(fileURLToPath(import.meta.url));
670
804
  for (let i = 0; i < 12; i++) {
671
- if (existsSync(join(dir, "scripts", "zeta-build-worker.mts"))) return dir;
672
- const up = dirname(dir);
805
+ if (existsSync2(join2(dir, "scripts", "zeta-build-worker.mts"))) return dir;
806
+ const up = dirname2(dir);
673
807
  if (up === dir) break;
674
808
  dir = up;
675
809
  }
@@ -689,9 +823,9 @@ async function localBuild(body, onPhase, repoRoot) {
689
823
  error: "local build needs the ZETA monorepo (scripts/zeta-build-worker.mts not found). Run from inside the repo, or use the http engine (--zeta-url)."
690
824
  };
691
825
  }
692
- const worker = join(root, "scripts", "zeta-build-worker.mts");
826
+ const worker = join2(root, "scripts", "zeta-build-worker.mts");
693
827
  const buildId = `zeta-build-${Date.now().toString(36)}-${Math.floor(performance.now())}`;
694
- const projectDir = join(tmpdir(), buildId);
828
+ const projectDir = join2(tmpdir(), buildId);
695
829
  const ideaB64 = Buffer.from(body.idea, "utf8").toString("base64");
696
830
  const args = [
697
831
  "--import",
@@ -753,11 +887,11 @@ var PROPERTY_KINDS = [
753
887
  function resolveProver(repoRoot) {
754
888
  const root = repoRoot ?? findRepoRoot();
755
889
  if (!root) return null;
756
- const base = join2(root, "packages", "shipgate-contract-prover");
757
- const dist = join2(base, "dist", "cli.js");
758
- if (existsSync2(dist)) return { nodeArgs: [dist] };
759
- const src = join2(base, "src", "cli.ts");
760
- if (existsSync2(src)) return { nodeArgs: ["--import", "tsx", src] };
890
+ const base = join3(root, "packages", "shipgate-contract-prover");
891
+ const dist = join3(base, "dist", "cli.js");
892
+ if (existsSync3(dist)) return { nodeArgs: [dist] };
893
+ const src = join3(base, "src", "cli.ts");
894
+ if (existsSync3(src)) return { nodeArgs: ["--import", "tsx", src] };
761
895
  return null;
762
896
  }
763
897
  function runProver(proverArgs, opts) {
@@ -802,8 +936,8 @@ ${e.message}` });
802
936
  // src/app-runner.ts
803
937
  import { spawn as spawn3, spawnSync } from "child_process";
804
938
  import { readFile } from "fs/promises";
805
- import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
806
- import { join as join3, dirname as dirname2 } from "path";
939
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
940
+ import { join as join4, dirname as dirname3 } from "path";
807
941
  var running = [];
808
942
  function killRunningApps() {
809
943
  for (const a of running.splice(0)) {
@@ -816,8 +950,8 @@ function killRunningApps() {
816
950
  var URL_RE = /(https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?[^\s)]*)/i;
817
951
  var FATAL_RE = /(Error:|error TS\d+|Cannot find module|EADDRINUSE|Failed to compile|SyntaxError|Module not found|exited with)/i;
818
952
  async function detectRun(dir) {
819
- const pkgPath = join3(dir, "package.json");
820
- if (!existsSync3(pkgPath)) {
953
+ const pkgPath = join4(dir, "package.json");
954
+ if (!existsSync4(pkgPath)) {
821
955
  return { error: `no package.json in ${dir} \u2014 not a runnable app directory` };
822
956
  }
823
957
  let scripts = {};
@@ -827,7 +961,7 @@ async function detectRun(dir) {
827
961
  } catch (e) {
828
962
  return { error: `unreadable package.json: ${e.message}` };
829
963
  }
830
- const pm = existsSync3(join3(dir, "pnpm-lock.yaml")) ? "pnpm" : existsSync3(join3(dir, "yarn.lock")) ? "yarn" : "npm";
964
+ const pm = existsSync4(join4(dir, "pnpm-lock.yaml")) ? "pnpm" : existsSync4(join4(dir, "yarn.lock")) ? "yarn" : "npm";
831
965
  for (const s of ["dev", "start", "serve"]) {
832
966
  if (scripts[s]) return { command: pm, args: ["run", s], reason: `${pm} run ${s}` };
833
967
  }
@@ -844,44 +978,44 @@ function pnpmAvailable() {
844
978
  return _pnpmOnPath;
845
979
  }
846
980
  function installPmFor(dir) {
847
- if (existsSync3(join3(dir, "yarn.lock"))) return "yarn";
848
- if (existsSync3(join3(dir, "pnpm-lock.yaml"))) return "pnpm";
981
+ if (existsSync4(join4(dir, "yarn.lock"))) return "yarn";
982
+ if (existsSync4(join4(dir, "pnpm-lock.yaml"))) return "pnpm";
849
983
  return pnpmAvailable() ? "pnpm" : "npm";
850
984
  }
851
985
  function ensureNpmrc(dir, lines) {
852
986
  try {
853
- const npmrc = join3(dir, ".npmrc");
854
- const prev = existsSync3(npmrc) ? readFileSync(npmrc, "utf8") : "";
987
+ const npmrc = join4(dir, ".npmrc");
988
+ const prev = existsSync4(npmrc) ? readFileSync2(npmrc, "utf8") : "";
855
989
  const missing = lines.filter((l) => !prev.includes(l.split("=")[0]));
856
990
  if (missing.length === 0) return;
857
- const sep3 = prev && !prev.endsWith("\n") ? "\n" : "";
858
- writeFileSync2(npmrc, prev + sep3 + missing.join("\n") + "\n");
991
+ const sep4 = prev && !prev.endsWith("\n") ? "\n" : "";
992
+ writeFileSync3(npmrc, prev + sep4 + missing.join("\n") + "\n");
859
993
  } catch {
860
994
  }
861
995
  }
862
996
  function isolateFromWorkspace(dir) {
863
- if (existsSync3(join3(dir, "pnpm-workspace.yaml"))) return;
864
- let cur = dirname2(dir);
997
+ if (existsSync4(join4(dir, "pnpm-workspace.yaml"))) return;
998
+ let cur = dirname3(dir);
865
999
  let nested = false;
866
1000
  for (let i = 0; i < 12; i++) {
867
- if (existsSync3(join3(cur, "pnpm-workspace.yaml"))) {
1001
+ if (existsSync4(join4(cur, "pnpm-workspace.yaml"))) {
868
1002
  nested = true;
869
1003
  break;
870
1004
  }
871
- const up = dirname2(cur);
1005
+ const up = dirname3(cur);
872
1006
  if (up === cur) break;
873
1007
  cur = up;
874
1008
  }
875
1009
  if (!nested) return;
876
1010
  try {
877
- writeFileSync2(join3(dir, "pnpm-workspace.yaml"), "packages:\n - .\n");
1011
+ writeFileSync3(join4(dir, "pnpm-workspace.yaml"), "packages:\n - .\n");
878
1012
  ensureNpmrc(dir, ["ignore-workspace-root-check=true"]);
879
1013
  } catch {
880
1014
  }
881
1015
  }
882
1016
  function installDeps(dir, onLog, signal) {
883
1017
  const pm = installPmFor(dir);
884
- if (pm === "pnpm" && !existsSync3(join3(dir, "pnpm-lock.yaml"))) {
1018
+ if (pm === "pnpm" && !existsSync4(join4(dir, "pnpm-lock.yaml"))) {
885
1019
  ensureNpmrc(dir, ["shamefully-hoist=true", "prefer-offline=true"]);
886
1020
  }
887
1021
  const args = pm === "pnpm" ? (
@@ -891,7 +1025,7 @@ function installDeps(dir, onLog, signal) {
891
1025
  ["install", "--ignore-workspace", "--no-frozen-lockfile", "--prefer-offline", "--config.confirmModulesPurge=false"]
892
1026
  ) : pm === "yarn" ? ["install"] : ["install", "--legacy-peer-deps", "--no-audit", "--no-fund", "--prefer-offline"];
893
1027
  onLog?.(`installing dependencies (${pm})\u2026`);
894
- return new Promise((resolve3) => {
1028
+ return new Promise((resolve4) => {
895
1029
  const child = spawn3(pm, args, { cwd: dir, shell: false, env: process.env });
896
1030
  let log = "";
897
1031
  const timer = setTimeout(() => child.kill("SIGKILL"), 8 * 6e4);
@@ -904,18 +1038,18 @@ function installDeps(dir, onLog, signal) {
904
1038
  child.stderr.on("data", sink);
905
1039
  child.on("error", (e) => {
906
1040
  clearTimeout(timer);
907
- resolve3({ ok: false, log: log + `
1041
+ resolve4({ ok: false, log: log + `
908
1042
  ${e.message}` });
909
1043
  });
910
1044
  child.on("close", (code) => {
911
1045
  clearTimeout(timer);
912
1046
  signal?.removeEventListener("abort", onAbort);
913
- resolve3({ ok: code === 0, log: log.slice(-4e3) });
1047
+ resolve4({ ok: code === 0, log: log.slice(-4e3) });
914
1048
  });
915
1049
  });
916
1050
  }
917
1051
  async function runApp(dir, opts = {}) {
918
- if (!existsSync3(join3(dir, "node_modules"))) {
1052
+ if (!existsSync4(join4(dir, "node_modules"))) {
919
1053
  isolateFromWorkspace(dir);
920
1054
  const inst = await installDeps(dir, void 0, opts.signal);
921
1055
  if (!inst.ok) {
@@ -929,7 +1063,7 @@ async function runApp(dir, opts = {}) {
929
1063
  let command;
930
1064
  let cmdArgs;
931
1065
  if (opts.script) {
932
- const pm = existsSync3(join3(dir, "pnpm-lock.yaml")) ? "pnpm" : "npm";
1066
+ const pm = existsSync4(join4(dir, "pnpm-lock.yaml")) ? "pnpm" : "npm";
933
1067
  command = pm;
934
1068
  cmdArgs = ["run", opts.script];
935
1069
  } else {
@@ -940,7 +1074,7 @@ async function runApp(dir, opts = {}) {
940
1074
  }
941
1075
  const timeoutMs = opts.timeoutMs ?? 9e4;
942
1076
  const child = spawn3(command, cmdArgs, { cwd: dir, shell: false, env: process.env });
943
- return new Promise((resolve3) => {
1077
+ return new Promise((resolve4) => {
944
1078
  let log = "";
945
1079
  let settled = false;
946
1080
  const finish = (r) => {
@@ -948,7 +1082,7 @@ async function runApp(dir, opts = {}) {
948
1082
  settled = true;
949
1083
  clearTimeout(timer);
950
1084
  opts.signal?.removeEventListener("abort", onAbort);
951
- resolve3(r);
1085
+ resolve4(r);
952
1086
  };
953
1087
  const timer = setTimeout(() => {
954
1088
  child.kill("SIGTERM");
@@ -1091,13 +1225,13 @@ import { tool } from "ai";
1091
1225
  import { z } from "zod";
1092
1226
  import { spawn as spawn5 } from "child_process";
1093
1227
  import { readFile as readFile2, writeFile, mkdir, readdir, stat } from "fs/promises";
1094
- import { resolve as resolve2, dirname as dirname3, relative, join as join4, sep as sep2 } from "path";
1095
- import fg from "fast-glob";
1228
+ import { resolve as resolve3, dirname as dirname4, relative, join as join5, sep as sep3 } from "path";
1229
+ import fg2 from "fast-glob";
1096
1230
  var IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.next/**", "**/build/**", "**/.turbo/**"];
1097
1231
  function inside(ctx, p) {
1098
- const abs = resolve2(ctx.cwd, p);
1232
+ const abs = resolve3(ctx.cwd, p);
1099
1233
  const rel = relative(ctx.cwd, abs);
1100
- if (rel === ".." || rel.startsWith(".." + sep2)) {
1234
+ if (rel === ".." || rel.startsWith(".." + sep3)) {
1101
1235
  throw new Error(`path "${p}" escapes the workspace root`);
1102
1236
  }
1103
1237
  return abs;
@@ -1141,9 +1275,9 @@ ${CWD_MARK}:`);
1141
1275
  const p = (nl >= 0 ? rest.slice(0, nl) : rest).trim();
1142
1276
  out = out.slice(0, idx);
1143
1277
  if (p) {
1144
- const cand = resolve2(p);
1278
+ const cand = resolve3(p);
1145
1279
  const rel = relative(opts.root, cand);
1146
- nextCwd = rel === "" || rel !== ".." && !rel.startsWith(".." + sep2) ? cand : opts.root;
1280
+ nextCwd = rel === "" || rel !== ".." && !rel.startsWith(".." + sep3) ? cand : opts.root;
1147
1281
  }
1148
1282
  }
1149
1283
  res({ code: code ?? 1, out, cwd: nextCwd });
@@ -1180,7 +1314,7 @@ async function search(ctx, pattern, searchPath, globPat, ignoreCase, maxResults,
1180
1314
  });
1181
1315
  if (viaRg) return viaRg;
1182
1316
  const re = new RegExp(pattern, ignoreCase ? "i" : "");
1183
- const files = await fg(fgPat, {
1317
+ const files = await fg2(fgPat, {
1184
1318
  cwd: abs,
1185
1319
  ignore: IGNORE,
1186
1320
  onlyFiles: true,
@@ -1298,7 +1432,7 @@ function buildTools(ctx) {
1298
1432
  }
1299
1433
  }
1300
1434
  if (keep !== false && viaHosted && result.ok && result.buildId && !result.paywalled) {
1301
- const dest = join4(ctx.cwd, `zeta-${String(result.buildId).slice(-8)}`);
1435
+ const dest = join5(ctx.cwd, `zeta-${String(result.buildId).slice(-8)}`);
1302
1436
  line(c.dim(" \u21B3 saving your code\u2026"));
1303
1437
  const delivered = await deliverBuild(
1304
1438
  ctx.zetaApiUrl,
@@ -1414,7 +1548,7 @@ function buildTools(ctx) {
1414
1548
  ctx.checkpoints?.begin();
1415
1549
  ctx.checkpoints?.capture(abs, before);
1416
1550
  ctx.checkpoints?.commit(before == null ? `create ${path}` : `write ${path}`);
1417
- await mkdir(dirname3(abs), { recursive: true });
1551
+ await mkdir(dirname4(abs), { recursive: true });
1418
1552
  await writeFile(abs, content, "utf8");
1419
1553
  toolLine("write_file", c.dim(path));
1420
1554
  return { ok: true, path, bytes: Buffer.byteLength(content) };
@@ -1546,7 +1680,7 @@ function buildTools(ctx) {
1546
1680
  const names = await readdir(abs);
1547
1681
  const entries = await Promise.all(
1548
1682
  names.map(async (n) => {
1549
- const s = await stat(join4(abs, n)).catch(() => null);
1683
+ const s = await stat(join5(abs, n)).catch(() => null);
1550
1684
  return { name: n, dir: s?.isDirectory() ?? false };
1551
1685
  })
1552
1686
  );
@@ -1565,7 +1699,7 @@ function buildTools(ctx) {
1565
1699
  execute: async ({ pattern, path }) => {
1566
1700
  try {
1567
1701
  const abs = inside(ctx, path);
1568
- const files = await fg(pattern, {
1702
+ const files = await fg2(pattern, {
1569
1703
  cwd: abs,
1570
1704
  ignore: IGNORE,
1571
1705
  onlyFiles: true,
@@ -1721,23 +1855,23 @@ function buildTools(ctx) {
1721
1855
  }
1722
1856
 
1723
1857
  // src/config.ts
1724
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync4, chmodSync, renameSync } from "fs";
1858
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync5, chmodSync, renameSync } from "fs";
1725
1859
  import { homedir } from "os";
1726
- import { join as join5 } from "path";
1727
- var LEGACY_DIR = join5(homedir(), ".zeta-g");
1728
- var STATE_DIR = join5(homedir(), ".wholestack");
1860
+ import { join as join6 } from "path";
1861
+ var LEGACY_DIR = join6(homedir(), ".zeta-g");
1862
+ var STATE_DIR = join6(homedir(), ".wholestack");
1729
1863
  try {
1730
- if (existsSync4(LEGACY_DIR) && !existsSync4(STATE_DIR)) renameSync(LEGACY_DIR, STATE_DIR);
1864
+ if (existsSync5(LEGACY_DIR) && !existsSync5(STATE_DIR)) renameSync(LEGACY_DIR, STATE_DIR);
1731
1865
  } catch {
1732
1866
  }
1733
1867
  function stateDir() {
1734
1868
  return STATE_DIR;
1735
1869
  }
1736
1870
  function projectDirs(cwd) {
1737
- return [join5(cwd, ".wholestack"), join5(cwd, ".zeta-g")];
1871
+ return [join6(cwd, ".wholestack"), join6(cwd, ".zeta-g")];
1738
1872
  }
1739
1873
  var CONFIG_DIR = STATE_DIR;
1740
- var CONFIG_PATH = join5(CONFIG_DIR, "config.json");
1874
+ var CONFIG_PATH = join6(CONFIG_DIR, "config.json");
1741
1875
  function parseDotenv(text) {
1742
1876
  const out = {};
1743
1877
  for (const raw of text.split("\n")) {
@@ -1760,16 +1894,16 @@ function fill(src) {
1760
1894
  }
1761
1895
  }
1762
1896
  function loadStoredEnv() {
1763
- const localEnv = join5(process.cwd(), ".env");
1764
- if (existsSync4(localEnv)) {
1897
+ const localEnv = join6(process.cwd(), ".env");
1898
+ if (existsSync5(localEnv)) {
1765
1899
  try {
1766
- fill(parseDotenv(readFileSync2(localEnv, "utf8")));
1900
+ fill(parseDotenv(readFileSync3(localEnv, "utf8")));
1767
1901
  } catch {
1768
1902
  }
1769
1903
  }
1770
- if (existsSync4(CONFIG_PATH)) {
1904
+ if (existsSync5(CONFIG_PATH)) {
1771
1905
  try {
1772
- const json = JSON.parse(readFileSync2(CONFIG_PATH, "utf8"));
1906
+ const json = JSON.parse(readFileSync3(CONFIG_PATH, "utf8"));
1773
1907
  const flat = {};
1774
1908
  for (const [k, v] of Object.entries(json)) if (typeof v === "string") flat[k] = v;
1775
1909
  fill(flat);
@@ -1778,25 +1912,25 @@ function loadStoredEnv() {
1778
1912
  }
1779
1913
  }
1780
1914
  function saveKey(name, value) {
1781
- mkdirSync2(CONFIG_DIR, { recursive: true });
1915
+ mkdirSync3(CONFIG_DIR, { recursive: true });
1782
1916
  let current = {};
1783
- if (existsSync4(CONFIG_PATH)) {
1917
+ if (existsSync5(CONFIG_PATH)) {
1784
1918
  try {
1785
- current = JSON.parse(readFileSync2(CONFIG_PATH, "utf8"));
1919
+ current = JSON.parse(readFileSync3(CONFIG_PATH, "utf8"));
1786
1920
  } catch {
1787
1921
  current = {};
1788
1922
  }
1789
1923
  }
1790
1924
  current[name] = value;
1791
- writeFileSync3(CONFIG_PATH, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
1925
+ writeFileSync4(CONFIG_PATH, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
1792
1926
  chmodSync(CONFIG_PATH, 384);
1793
1927
  return CONFIG_PATH;
1794
1928
  }
1795
1929
 
1796
1930
  // src/hooks.ts
1797
1931
  import { spawn as spawn6 } from "child_process";
1798
- import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
1799
- import { join as join6 } from "path";
1932
+ import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
1933
+ import { join as join7 } from "path";
1800
1934
  var HOOK_TIMEOUT_MS = 15e3;
1801
1935
  function runOne(def, event, payload, cwd) {
1802
1936
  return new Promise((res) => {
@@ -1882,12 +2016,12 @@ function mergeHookSets(...sets) {
1882
2016
  return out;
1883
2017
  }
1884
2018
  function loadHookFiles(cwd) {
1885
- const files = [join6(stateDir(), "hooks.json"), ...projectDirs(cwd).map((d) => join6(d, "hooks.json"))];
2019
+ const files = [join7(stateDir(), "hooks.json"), ...projectDirs(cwd).map((d) => join7(d, "hooks.json"))];
1886
2020
  const sets = [];
1887
2021
  for (const f of files) {
1888
- if (!existsSync5(f)) continue;
2022
+ if (!existsSync6(f)) continue;
1889
2023
  try {
1890
- sets.push(JSON.parse(readFileSync3(f, "utf8")));
2024
+ sets.push(JSON.parse(readFileSync4(f, "utf8")));
1891
2025
  } catch {
1892
2026
  }
1893
2027
  }
@@ -3019,24 +3153,24 @@ import {
3019
3153
  // src/markdown.ts
3020
3154
  var useColor2 = process.stdout.isTTY && !process.env.NO_COLOR;
3021
3155
  var truecolor = /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
3022
- function fg2(r, g, b, c256) {
3156
+ function fg3(r, g, b, c256) {
3023
3157
  if (!useColor2) return "";
3024
3158
  return truecolor ? `\x1B[38;2;${r};${g};${b}m` : `\x1B[38;5;${c256}m`;
3025
3159
  }
3026
3160
  var RESET = useColor2 ? "\x1B[0m" : "";
3027
3161
  var SY = {
3028
- keyword: fg2(198, 120, 221, 176),
3162
+ keyword: fg3(198, 120, 221, 176),
3029
3163
  // violet
3030
- string: fg2(152, 195, 121, 114),
3164
+ string: fg3(152, 195, 121, 114),
3031
3165
  // green
3032
- number: fg2(209, 154, 102, 173),
3166
+ number: fg3(209, 154, 102, 173),
3033
3167
  // orange
3034
- comment: fg2(106, 115, 125, 244),
3168
+ comment: fg3(106, 115, 125, 244),
3035
3169
  // gray
3036
- fn: fg2(97, 175, 239, 75),
3170
+ fn: fg3(97, 175, 239, 75),
3037
3171
  // blue
3038
- punct: fg2(160, 168, 180, 247),
3039
- type: fg2(229, 192, 123, 180)
3172
+ punct: fg3(160, 168, 180, 247),
3173
+ type: fg3(229, 192, 123, 180)
3040
3174
  // yellow
3041
3175
  };
3042
3176
  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;
@@ -3085,7 +3219,7 @@ function highlightLine(src, _lang) {
3085
3219
  function inline(s) {
3086
3220
  if (!useColor2) return s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
3087
3221
  let out = s;
3088
- out = out.replace(/`([^`]+)`/g, (_m, code) => fg2(229, 192, 123, 180) + code + RESET);
3222
+ out = out.replace(/`([^`]+)`/g, (_m, code) => fg3(229, 192, 123, 180) + code + RESET);
3089
3223
  out = out.replace(/\*\*([^*]+)\*\*/g, (_m, t) => "\x1B[1m" + t + "\x1B[22m");
3090
3224
  out = out.replace(/(^|[^*])\*([^*]+)\*/g, (_m, p, t) => p + "\x1B[3m" + t + "\x1B[23m");
3091
3225
  out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => "\x1B[4m" + t + "\x1B[24m" + c.dim(` (${u})`));
@@ -3880,16 +4014,16 @@ function announcePlanMode() {
3880
4014
 
3881
4015
  // src/memory.ts
3882
4016
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
3883
- import { existsSync as existsSync6 } from "fs";
3884
- import { join as join7, dirname as dirname4 } from "path";
4017
+ import { existsSync as existsSync7 } from "fs";
4018
+ import { join as join8, dirname as dirname5 } from "path";
3885
4019
  var FILE_NAMES = ["ZETA.md", "AGENTS.md", "CLAUDE.md"];
3886
4020
  var MAX_TOTAL = 14e3;
3887
4021
  var MAX_PER_FILE = 8e3;
3888
4022
  function repoRootFrom(start) {
3889
4023
  let dir = start;
3890
4024
  for (let i = 0; i < 24; i++) {
3891
- if (existsSync6(join7(dir, ".git"))) return dir;
3892
- const up = dirname4(dir);
4025
+ if (existsSync7(join8(dir, ".git"))) return dir;
4026
+ const up = dirname5(dir);
3893
4027
  if (up === dir) break;
3894
4028
  dir = up;
3895
4029
  }
@@ -3906,22 +4040,22 @@ async function readBounded(path, limit) {
3906
4040
  }
3907
4041
  async function loadProjectMemory(cwd) {
3908
4042
  const candidates = [];
3909
- const globalFile = join7(stateDir(), "ZETA.md");
3910
- if (existsSync6(globalFile)) candidates.push(globalFile);
4043
+ const globalFile = join8(stateDir(), "ZETA.md");
4044
+ if (existsSync7(globalFile)) candidates.push(globalFile);
3911
4045
  const root = repoRootFrom(cwd);
3912
4046
  const chain = [];
3913
4047
  let dir = cwd;
3914
4048
  for (let i = 0; i < 24; i++) {
3915
4049
  chain.unshift(dir);
3916
4050
  if (dir === root) break;
3917
- const up = dirname4(dir);
4051
+ const up = dirname5(dir);
3918
4052
  if (up === dir) break;
3919
4053
  dir = up;
3920
4054
  }
3921
4055
  for (const d of chain) {
3922
4056
  for (const name of FILE_NAMES) {
3923
- const p = join7(d, name);
3924
- if (existsSync6(p) && !candidates.includes(p)) {
4057
+ const p = join8(d, name);
4058
+ if (existsSync7(p) && !candidates.includes(p)) {
3925
4059
  candidates.push(p);
3926
4060
  break;
3927
4061
  }
@@ -3948,20 +4082,20 @@ ${r.text.trim()}
3948
4082
  // src/session.ts
3949
4083
  import {
3950
4084
  appendFileSync,
3951
- mkdirSync as mkdirSync3,
3952
- readFileSync as readFileSync4,
4085
+ mkdirSync as mkdirSync4,
4086
+ readFileSync as readFileSync5,
3953
4087
  readdirSync,
3954
- existsSync as existsSync7,
4088
+ existsSync as existsSync8,
3955
4089
  statSync
3956
4090
  } from "fs";
3957
- import { join as join8 } from "path";
4091
+ import { join as join9 } from "path";
3958
4092
  import { randomUUID } from "crypto";
3959
- var SESSIONS_DIR = join8(stateDir(), "sessions");
4093
+ var SESSIONS_DIR = join9(stateDir(), "sessions");
3960
4094
  function ensureDir() {
3961
- mkdirSync3(SESSIONS_DIR, { recursive: true });
4095
+ mkdirSync4(SESSIONS_DIR, { recursive: true });
3962
4096
  }
3963
4097
  function fileFor(id) {
3964
- return join8(SESSIONS_DIR, `${id}.jsonl`);
4098
+ return join9(SESSIONS_DIR, `${id}.jsonl`);
3965
4099
  }
3966
4100
  function previewOf(message) {
3967
4101
  if (message.role !== "user") return "";
@@ -3992,8 +4126,8 @@ var Session = class _Session {
3992
4126
  /** Reopen an existing session and replay its messages. */
3993
4127
  static resume(id) {
3994
4128
  const path = fileFor(id);
3995
- if (!existsSync7(path)) return null;
3996
- const lines = readFileSync4(path, "utf8").split("\n").filter(Boolean);
4129
+ if (!existsSync8(path)) return null;
4130
+ const lines = readFileSync5(path, "utf8").split("\n").filter(Boolean);
3997
4131
  let meta = null;
3998
4132
  const messages = [];
3999
4133
  for (const l of lines) {
@@ -4016,16 +4150,16 @@ var Session = class _Session {
4016
4150
  }
4017
4151
  /** List sessions newest-first, with a preview of the first user message. */
4018
4152
  static list(limit = 30, cwd) {
4019
- if (!existsSync7(SESSIONS_DIR)) return [];
4153
+ if (!existsSync8(SESSIONS_DIR)) return [];
4020
4154
  const out = [];
4021
4155
  for (const name of readdirSync(SESSIONS_DIR)) {
4022
4156
  if (!name.endsWith(".jsonl")) continue;
4023
- const path = join8(SESSIONS_DIR, name);
4157
+ const path = join9(SESSIONS_DIR, name);
4024
4158
  let meta = null;
4025
4159
  let preview = "";
4026
4160
  let turns = 0;
4027
4161
  try {
4028
- const lines = readFileSync4(path, "utf8").split("\n").filter(Boolean);
4162
+ const lines = readFileSync5(path, "utf8").split("\n").filter(Boolean);
4029
4163
  for (const l of lines) {
4030
4164
  const rec = JSON.parse(l);
4031
4165
  if (rec.kind === "meta") meta = { id: rec.id, cwd: rec.cwd, model: rec.model, startedAt: rec.startedAt };
@@ -4058,8 +4192,8 @@ var Session = class _Session {
4058
4192
  };
4059
4193
 
4060
4194
  // src/commands.ts
4061
- import { writeFileSync as writeFileSync4, existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
4062
- import { join as join9 } from "path";
4195
+ import { writeFileSync as writeFileSync5, existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
4196
+ import { join as join10 } from "path";
4063
4197
 
4064
4198
  // src/git.ts
4065
4199
  import { execFileSync } from "child_process";
@@ -4385,13 +4519,13 @@ var BUILTINS = [
4385
4519
  summary: "create a ZETA.md memory scaffold here",
4386
4520
  source: "builtin",
4387
4521
  run: (ctx) => {
4388
- const path = join9(ctx.cwd, "ZETA.md");
4389
- if (existsSync8(path)) {
4522
+ const path = join10(ctx.cwd, "ZETA.md");
4523
+ if (existsSync9(path)) {
4390
4524
  ctx.print(" " + c.yellow(`ZETA.md already exists at ${path}`));
4391
4525
  return { type: "handled" };
4392
4526
  }
4393
4527
  try {
4394
- writeFileSync4(path, ZETA_MD_TEMPLATE, "utf8");
4528
+ writeFileSync5(path, ZETA_MD_TEMPLATE, "utf8");
4395
4529
  ctx.print(" " + c.green(`\u2713 wrote ${path}`) + c.dim(" \xB7 edit it, then /clear to reload"));
4396
4530
  } catch (e) {
4397
4531
  ctx.print(" " + c.red(e.message));
@@ -4588,12 +4722,12 @@ function parseCustom(name, body, source = "custom") {
4588
4722
  };
4589
4723
  }
4590
4724
  function loadMdCommands(dir, source) {
4591
- if (!existsSync8(dir)) return [];
4725
+ if (!existsSync9(dir)) return [];
4592
4726
  const cmds = [];
4593
4727
  for (const file of readdirSync2(dir)) {
4594
4728
  if (!file.endsWith(".md")) continue;
4595
4729
  try {
4596
- const body = readFileSync5(join9(dir, file), "utf8");
4730
+ const body = readFileSync6(join10(dir, file), "utf8");
4597
4731
  cmds.push(parseCustom(file.replace(/\.md$/, ""), body, source));
4598
4732
  } catch {
4599
4733
  }
@@ -4629,7 +4763,7 @@ var CommandRegistry = class {
4629
4763
  }
4630
4764
  /** Load *.md commands from ~/.wholestack/commands and <cwd>/.wholestack/commands (or legacy .zeta-g/). */
4631
4765
  loadCustom(cwd) {
4632
- const dirs = [join9(stateDir(), "commands"), ...projectDirs(cwd).map((d) => join9(d, "commands"))];
4766
+ const dirs = [join10(stateDir(), "commands"), ...projectDirs(cwd).map((d) => join10(d, "commands"))];
4633
4767
  for (const dir of dirs) {
4634
4768
  for (const cmd of loadMdCommands(dir, "custom")) this.register(cmd);
4635
4769
  }
@@ -4649,16 +4783,16 @@ var CommandRegistry = class {
4649
4783
  };
4650
4784
 
4651
4785
  // src/plugins.ts
4652
- import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
4653
- import { join as join10 } from "path";
4786
+ import { readFileSync as readFileSync7, existsSync as existsSync10, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
4787
+ import { join as join11 } from "path";
4654
4788
  function pluginRoots(cwd) {
4655
- return [join10(stateDir(), "plugins"), ...projectDirs(cwd).map((d) => join10(d, "plugins"))];
4789
+ return [join11(stateDir(), "plugins"), ...projectDirs(cwd).map((d) => join11(d, "plugins"))];
4656
4790
  }
4657
4791
  function resolveSystemPrompt(dir, value) {
4658
- const asFile = join10(dir, value);
4659
- if (value.length < 200 && existsSync9(asFile)) {
4792
+ const asFile = join11(dir, value);
4793
+ if (value.length < 200 && existsSync10(asFile)) {
4660
4794
  try {
4661
- return readFileSync6(asFile, "utf8").trim();
4795
+ return readFileSync7(asFile, "utf8").trim();
4662
4796
  } catch {
4663
4797
  return "";
4664
4798
  }
@@ -4677,9 +4811,9 @@ function loadPlugins(cwd) {
4677
4811
  const systemBlocks = [];
4678
4812
  const hookSets = [];
4679
4813
  for (const root of pluginRoots(cwd)) {
4680
- if (!existsSync9(root)) continue;
4814
+ if (!existsSync10(root)) continue;
4681
4815
  for (const entry of readdirSync3(root)) {
4682
- const dir = join10(root, entry);
4816
+ const dir = join11(root, entry);
4683
4817
  let info = null;
4684
4818
  try {
4685
4819
  info = statSync2(dir);
@@ -4687,10 +4821,10 @@ function loadPlugins(cwd) {
4687
4821
  continue;
4688
4822
  }
4689
4823
  if (!info.isDirectory()) continue;
4690
- const manifestPath = join10(dir, "zeta-plugin.json");
4691
- if (!existsSync9(manifestPath)) continue;
4824
+ const manifestPath = join11(dir, "zeta-plugin.json");
4825
+ if (!existsSync10(manifestPath)) continue;
4692
4826
  try {
4693
- const manifest = JSON.parse(readFileSync6(manifestPath, "utf8"));
4827
+ const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
4694
4828
  const name = manifest.name ?? entry;
4695
4829
  if (manifest.systemPrompt) {
4696
4830
  const text = resolveSystemPrompt(dir, manifest.systemPrompt);
@@ -4698,7 +4832,7 @@ function loadPlugins(cwd) {
4698
4832
  ${text}
4699
4833
  </plugin>`);
4700
4834
  }
4701
- const cmdDir = join10(dir, manifest.commandsDir ?? "commands");
4835
+ const cmdDir = join11(dir, manifest.commandsDir ?? "commands");
4702
4836
  result.commands.push(...loadMdCommands(cmdDir, name));
4703
4837
  if (manifest.mcpServers) Object.assign(result.mcpServers, manifest.mcpServers);
4704
4838
  if (manifest.hooks) hookSets.push(tagHooks(manifest.hooks, name));
@@ -4726,14 +4860,14 @@ import { dynamicTool, jsonSchema } from "ai";
4726
4860
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4727
4861
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4728
4862
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4729
- import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
4730
- import { join as join11, dirname as dirname5 } from "path";
4863
+ import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
4864
+ import { join as join12, dirname as dirname6 } from "path";
4731
4865
  function isHttp(c2) {
4732
4866
  return typeof c2.url === "string";
4733
4867
  }
4734
4868
  function readConfigFile(path) {
4735
4869
  try {
4736
- const json = JSON.parse(readFileSync7(path, "utf8"));
4870
+ const json = JSON.parse(readFileSync8(path, "utf8"));
4737
4871
  return json.mcpServers ?? {};
4738
4872
  } catch {
4739
4873
  return {};
@@ -4741,28 +4875,28 @@ function readConfigFile(path) {
4741
4875
  }
4742
4876
  function discoverConfigs(cwd) {
4743
4877
  const merged = {};
4744
- const global = join11(stateDir(), "mcp.json");
4745
- if (existsSync10(global)) Object.assign(merged, readConfigFile(global));
4878
+ const global = join12(stateDir(), "mcp.json");
4879
+ if (existsSync11(global)) Object.assign(merged, readConfigFile(global));
4746
4880
  let dir = cwd;
4747
4881
  for (let i = 0; i < 24; i++) {
4748
- const p = join11(dir, ".mcp.json");
4749
- if (existsSync10(p)) {
4882
+ const p = join12(dir, ".mcp.json");
4883
+ if (existsSync11(p)) {
4750
4884
  Object.assign(merged, readConfigFile(p));
4751
4885
  break;
4752
4886
  }
4753
- const up = dirname5(dir);
4887
+ const up = dirname6(dir);
4754
4888
  if (up === dir) break;
4755
4889
  dir = up;
4756
4890
  }
4757
4891
  return merged;
4758
4892
  }
4759
4893
  function withTimeout(p, ms, label) {
4760
- return new Promise((resolve3, reject) => {
4894
+ return new Promise((resolve4, reject) => {
4761
4895
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
4762
4896
  p.then(
4763
4897
  (v) => {
4764
4898
  clearTimeout(t);
4765
- resolve3(v);
4899
+ resolve4(v);
4766
4900
  },
4767
4901
  (e) => {
4768
4902
  clearTimeout(t);
@@ -4793,7 +4927,7 @@ async function loadMcpTools(opts) {
4793
4927
  await Promise.all(
4794
4928
  keys.map(async (key) => {
4795
4929
  const cfg = configs[key];
4796
- const client = new Client({ name: "wholestack", version: "0.5.5" }, { capabilities: {} });
4930
+ const client = new Client({ name: "wholestack", version: "0.5.6" }, { capabilities: {} });
4797
4931
  try {
4798
4932
  const transport = isHttp(cfg) ? new StreamableHTTPClientTransport(new URL(cfg.url), {
4799
4933
  requestInit: cfg.headers ? { headers: cfg.headers } : void 0
@@ -5003,13 +5137,13 @@ function buildWebTools() {
5003
5137
  import { createInterface } from "readline/promises";
5004
5138
  import { emitKeypressEvents } from "readline";
5005
5139
  import { stdin, stdout } from "process";
5006
- import { readFileSync as readFileSync8, appendFileSync as appendFileSync2, mkdirSync as mkdirSync4, existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
5007
- import { join as join12, dirname as dirname6, basename } from "path";
5008
- var HISTORY_FILE = join12(stateDir(), "history");
5140
+ import { readFileSync as readFileSync9, appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync12, readdirSync as readdirSync4 } from "fs";
5141
+ import { join as join13, dirname as dirname7, basename } from "path";
5142
+ var HISTORY_FILE = join13(stateDir(), "history");
5009
5143
  var HISTORY_MAX = 1e3;
5010
5144
  function loadHistory() {
5011
5145
  try {
5012
- return readFileSync8(HISTORY_FILE, "utf8").split("\n").filter(Boolean).slice(-HISTORY_MAX);
5146
+ return readFileSync9(HISTORY_FILE, "utf8").split("\n").filter(Boolean).slice(-HISTORY_MAX);
5013
5147
  } catch {
5014
5148
  return [];
5015
5149
  }
@@ -5045,12 +5179,12 @@ var InputController = class {
5045
5179
  const at = line2.lastIndexOf("@");
5046
5180
  if (at >= 0 && at >= line2.lastIndexOf(" ")) {
5047
5181
  const partial = line2.slice(at + 1);
5048
- const dir = partial.includes("/") ? dirname6(partial) : ".";
5182
+ const dir = partial.includes("/") ? dirname7(partial) : ".";
5049
5183
  const base = basename(partial);
5050
5184
  try {
5051
5185
  const entries = readdirSync4(dir, { withFileTypes: true });
5052
5186
  const hits = entries.filter((e) => e.name.startsWith(base)).map((e) => {
5053
- const full = dir === "." ? e.name : join12(dir, e.name);
5187
+ const full = dir === "." ? e.name : join13(dir, e.name);
5054
5188
  return line2.slice(0, at + 1) + full + (e.isDirectory() ? "/" : "");
5055
5189
  });
5056
5190
  if (hits.length) return [hits, line2];
@@ -5155,7 +5289,7 @@ var InputController = class {
5155
5289
  prevCaretR = caret.r;
5156
5290
  };
5157
5291
  let prevCaretR = 0;
5158
- return new Promise((resolve3) => {
5292
+ return new Promise((resolve4) => {
5159
5293
  emitKeypressEvents(stdin);
5160
5294
  const watching = this.activeWatch;
5161
5295
  if (watching) this.stopWatch();
@@ -5179,7 +5313,7 @@ var InputController = class {
5179
5313
  stdout.write("\x1B[" + Math.max(1, down) + "B\r\n");
5180
5314
  this.rl.resume();
5181
5315
  if (watching) this.startWatch(watching);
5182
- resolve3(val);
5316
+ resolve4(val);
5183
5317
  };
5184
5318
  const replaceLine = (s) => {
5185
5319
  buf = s;
@@ -5311,8 +5445,8 @@ var InputController = class {
5311
5445
  }
5312
5446
  saveHistory(entry) {
5313
5447
  try {
5314
- mkdirSync4(dirname6(HISTORY_FILE), { recursive: true });
5315
- if (!existsSync11(HISTORY_FILE) || entry !== loadHistory().slice(-1)[0]) {
5448
+ mkdirSync5(dirname7(HISTORY_FILE), { recursive: true });
5449
+ if (!existsSync12(HISTORY_FILE) || entry !== loadHistory().slice(-1)[0]) {
5316
5450
  appendFileSync2(HISTORY_FILE, entry.replace(/\n/g, " ") + "\n", "utf8");
5317
5451
  }
5318
5452
  } catch {
@@ -5401,6 +5535,14 @@ export {
5401
5535
  diffStat,
5402
5536
  renderEditDiff,
5403
5537
  renderNewFileDiff,
5538
+ listProjects,
5539
+ pullWorkingTree,
5540
+ pushWorkingTree,
5541
+ openBuildInEditor,
5542
+ readProjectLink,
5543
+ writeProjectLink,
5544
+ writeWorkingTree,
5545
+ collectLocalFiles,
5404
5546
  PROPERTY_KINDS,
5405
5547
  runProver,
5406
5548
  killRunningApps,
package/dist/cli.js CHANGED
@@ -12,9 +12,11 @@ import {
12
12
  buildTools,
13
13
  buildWebTools,
14
14
  c,
15
+ collectLocalFiles,
15
16
  isPermissionMode,
16
17
  killRunningApps,
17
18
  line,
19
+ listProjects,
18
20
  loadHookFiles,
19
21
  loadMcpTools,
20
22
  loadPlugins,
@@ -23,6 +25,10 @@ import {
23
25
  mergeHookSets,
24
26
  modelContextWindow,
25
27
  modelLabel,
28
+ openBuildInEditor,
29
+ pullWorkingTree,
30
+ pushWorkingTree,
31
+ readProjectLink,
26
32
  resolveModel,
27
33
  resolveModelKey,
28
34
  runProver,
@@ -32,8 +38,10 @@ import {
32
38
  supportsThinking,
33
39
  tasks,
34
40
  userBox,
35
- visionCapable
36
- } from "./chunk-7NZ77Q7Q.js";
41
+ visionCapable,
42
+ writeProjectLink,
43
+ writeWorkingTree
44
+ } from "./chunk-K6MSDQJT.js";
37
45
 
38
46
  // src/cli.ts
39
47
  import { createInterface } from "readline/promises";
@@ -871,6 +879,12 @@ ${c.bold("Usage")}
871
879
  wholestack "build me a todo app" run one prompt and exit
872
880
  wholestack -m zeta-g1-max "..." drive it with the most capable brain
873
881
 
882
+ ${c.bold("Projects")} ${c.dim("(one spine across terminal, dashboard and the web IDE)")}
883
+ wholestack projects list your platform projects
884
+ wholestack pull <id> [dir] download a project's working tree
885
+ wholestack push [dir] sync local edits back to the web IDE
886
+ wholestack open [buildId] open a generated build in the web IDE
887
+
874
888
  ${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmos \xB7 signed cert)")}
875
889
  wholestack audit <dir> <Contract> full auto-detect security sweep
876
890
  wholestack prove <dir> <Contract> --property <k> prove one invariant
@@ -1023,6 +1037,87 @@ async function runLogin(raw) {
1023
1037
  line(c.dim(" now just run `wholestack`."));
1024
1038
  return 0;
1025
1039
  }
1040
+ async function runProjectSubcommand(raw) {
1041
+ const verb = raw[0];
1042
+ if (verb !== "projects" && verb !== "pull" && verb !== "push" && verb !== "open") return null;
1043
+ const base = webBaseUrl();
1044
+ if (verb === "projects") {
1045
+ const r2 = await listProjects(base);
1046
+ if (!r2.ok) {
1047
+ line(c.red(` ${r2.error}`));
1048
+ return 1;
1049
+ }
1050
+ if (!r2.projects?.length) {
1051
+ line(c.dim(' no projects yet \u2014 build something (`wholestack "an idea"`) or create one in the web IDE.'));
1052
+ return 0;
1053
+ }
1054
+ line();
1055
+ for (const p of r2.projects) {
1056
+ const when = p.updatedAt ? new Date(p.updatedAt).toISOString().slice(0, 10) : "";
1057
+ line(" " + c.cyan(p.id) + " " + c.bold(p.name) + (when ? c.dim(` \xB7 ${when}`) : ""));
1058
+ if (p.description) line(" " + c.dim(` ${p.description}`));
1059
+ }
1060
+ line();
1061
+ line(c.dim(" pull one with ") + c.cyan("wholestack pull <id>") + c.dim(" \xB7 open in the IDE at ") + c.cyan(`${base}/ide/studio?projectId=<id>`));
1062
+ return 0;
1063
+ }
1064
+ if (verb === "pull") {
1065
+ const projectId = raw[1];
1066
+ if (!projectId) {
1067
+ line(c.red(" usage: wholestack pull <projectId> [dir]"));
1068
+ return 1;
1069
+ }
1070
+ const r2 = await pullWorkingTree(base, projectId);
1071
+ if (!r2.ok) {
1072
+ line(c.red(` ${r2.error}`));
1073
+ return 1;
1074
+ }
1075
+ const dest = raw[2] ?? (r2.name ? r2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") : projectId);
1076
+ const written = writeWorkingTree(dest, r2.files ?? []);
1077
+ writeProjectLink(dest, { projectId });
1078
+ line(c.green(` \u2713 pulled ${written} files`) + c.dim(` \u2192 ${dest}/`));
1079
+ line(c.dim(" edit locally, then ") + c.cyan(`wholestack push ${dest === "." ? "" : dest}`).trimEnd() + c.dim(" to sync back to the web IDE."));
1080
+ return 0;
1081
+ }
1082
+ if (verb === "push") {
1083
+ const dir = raw[1] ?? ".";
1084
+ const link = readProjectLink(dir);
1085
+ const flagIdx = raw.indexOf("--project");
1086
+ const projectId = (flagIdx >= 0 ? raw[flagIdx + 1] : void 0) ?? link?.projectId;
1087
+ if (!projectId) {
1088
+ line(c.red(" no linked project \u2014 pull first, or pass --project <id>."));
1089
+ return 1;
1090
+ }
1091
+ const { files, skipped } = await collectLocalFiles(dir);
1092
+ if (!files.length) {
1093
+ line(c.red(" nothing to push (no readable text files found)."));
1094
+ return 1;
1095
+ }
1096
+ const r2 = await pushWorkingTree(base, projectId, files);
1097
+ if (!r2.ok) {
1098
+ line(c.red(` ${r2.error}`));
1099
+ return 1;
1100
+ }
1101
+ writeProjectLink(dir, { projectId });
1102
+ line(c.green(` \u2713 pushed ${files.length} files`) + c.dim(skipped ? ` (${skipped} skipped: binary/oversize)` : ""));
1103
+ line(c.dim(" open it: ") + c.cyan(`${base}/ide/studio?projectId=${projectId}`));
1104
+ return 0;
1105
+ }
1106
+ const buildId = raw[1] ?? readProjectLink(".")?.buildId;
1107
+ if (!buildId) {
1108
+ line(c.red(" usage: wholestack open <buildId> (or run it inside a delivered app dir)"));
1109
+ return 1;
1110
+ }
1111
+ const r = await openBuildInEditor(base, buildId);
1112
+ if (!r.ok) {
1113
+ line(c.red(` ${r.error}`));
1114
+ return 1;
1115
+ }
1116
+ if (r.projectId) writeProjectLink(".", { buildId, projectId: r.projectId });
1117
+ line(c.green(" \u2713 project ready in the web IDE"));
1118
+ line(c.dim(" ") + c.cyan(`${base}${r.editorUrl ?? `/ide/studio?projectId=${r.projectId}`}`));
1119
+ return 0;
1120
+ }
1026
1121
  var EMPTY_PLUGINS = {
1027
1122
  plugins: [],
1028
1123
  systemText: "",
@@ -1051,6 +1146,8 @@ async function main() {
1051
1146
  if (loggedIn !== null) exit(loggedIn);
1052
1147
  const sub = await runSecuritySubcommand(rawArgs);
1053
1148
  if (sub !== null) exit(sub);
1149
+ const proj = await runProjectSubcommand(rawArgs);
1150
+ if (proj !== null) exit(proj);
1054
1151
  const args = parse(rawArgs);
1055
1152
  if (args.version) {
1056
1153
  line(`wholestack ${VERSION}`);
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  resolveModelKey,
50
50
  severityRank,
51
51
  supportsThinking
52
- } from "./chunk-7NZ77Q7Q.js";
52
+ } from "./chunk-K6MSDQJT.js";
53
53
 
54
54
  // src/prompts/template.ts
55
55
  var PLACEHOLDER = /\{\{([A-Za-z0-9_.]+)\}\}/g;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wholestack",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Wholestack — a pro-grade conversational terminal agent for the Wholestack codegen engine. Talk to it in plain language: it writes ISL, generates full-stack or Solidity apps, and proves them with ShipGate. Browser login, membership-gated builds, slash commands, sessions, plan mode, diffs, MCP, plugins.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",