wholestack 0.5.5 → 0.5.7
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 +40 -0
- package/dist/chunk-TLYCOIEA.js +9406 -0
- package/dist/cli.js +1207 -53
- package/dist/index.d.ts +594 -1
- package/dist/index.js +1 -1
- package/package.json +15 -11
- package/skills/fix-failing-build.md +38 -0
- package/skills/review-before-ship.md +34 -0
- package/skills/ship-a-saas.md +39 -0
- package/dist/chunk-7NZ77Q7Q.js +0 -5460
package/dist/cli.js
CHANGED
|
@@ -7,15 +7,35 @@ import {
|
|
|
7
7
|
PROPERTY_KINDS,
|
|
8
8
|
Permissions,
|
|
9
9
|
Session,
|
|
10
|
+
SkillRegistry,
|
|
10
11
|
announcePlanMode,
|
|
12
|
+
apiJson,
|
|
11
13
|
banner,
|
|
14
|
+
buildEscalateTool,
|
|
15
|
+
buildEyesTool,
|
|
16
|
+
buildOpsTools,
|
|
17
|
+
buildSkillTools,
|
|
12
18
|
buildTools,
|
|
19
|
+
buildVerifyTools,
|
|
13
20
|
buildWebTools,
|
|
14
21
|
c,
|
|
22
|
+
collectLocalFiles,
|
|
23
|
+
deployBuild,
|
|
24
|
+
drawCanvas,
|
|
25
|
+
escalationBus,
|
|
26
|
+
fetchGithubInstallations,
|
|
27
|
+
fetchGithubRepos,
|
|
28
|
+
generateLegal,
|
|
29
|
+
getBusiness,
|
|
30
|
+
getLaunchKit,
|
|
31
|
+
inspectUrl,
|
|
15
32
|
isPermissionMode,
|
|
16
33
|
killRunningApps,
|
|
17
34
|
line,
|
|
35
|
+
listBusinesses,
|
|
36
|
+
listProjects,
|
|
18
37
|
loadHookFiles,
|
|
38
|
+
loadLearnedMemory,
|
|
19
39
|
loadMcpTools,
|
|
20
40
|
loadPlugins,
|
|
21
41
|
loadProjectMemory,
|
|
@@ -23,21 +43,43 @@ import {
|
|
|
23
43
|
mergeHookSets,
|
|
24
44
|
modelContextWindow,
|
|
25
45
|
modelLabel,
|
|
46
|
+
openBuildInEditor,
|
|
47
|
+
pickBootModel,
|
|
48
|
+
pickGithubRepoInteractive,
|
|
49
|
+
promoteBuild,
|
|
50
|
+
proveLocalRepo,
|
|
51
|
+
pullWorkingTree,
|
|
52
|
+
pushWorkingTree,
|
|
53
|
+
readMetrics,
|
|
54
|
+
readProjectLink,
|
|
55
|
+
readSourceFile,
|
|
56
|
+
rememberLesson,
|
|
57
|
+
renderGtmPlanLines,
|
|
58
|
+
renderLaunchKitLines,
|
|
59
|
+
renderMetricsLines,
|
|
26
60
|
resolveModel,
|
|
27
61
|
resolveModelKey,
|
|
62
|
+
rollbackDeployment,
|
|
28
63
|
runProver,
|
|
64
|
+
runRepoAuditStream,
|
|
29
65
|
saveKey,
|
|
66
|
+
searchLessons,
|
|
30
67
|
stateDir,
|
|
31
68
|
statusLine,
|
|
32
69
|
supportsThinking,
|
|
33
70
|
tasks,
|
|
71
|
+
toolLine,
|
|
34
72
|
userBox,
|
|
35
|
-
visionCapable
|
|
36
|
-
|
|
73
|
+
visionCapable,
|
|
74
|
+
waitlistCount,
|
|
75
|
+
webBaseUrl,
|
|
76
|
+
writeProjectLink,
|
|
77
|
+
writeWorkingTree
|
|
78
|
+
} from "./chunk-TLYCOIEA.js";
|
|
37
79
|
|
|
38
80
|
// src/cli.ts
|
|
39
81
|
import { createInterface } from "readline/promises";
|
|
40
|
-
import { stdin, stdout, argv, exit, cwd } from "process";
|
|
82
|
+
import { stdin, stdout as stdout3, argv, exit, cwd as cwd2 } from "process";
|
|
41
83
|
|
|
42
84
|
// src/cli-login.ts
|
|
43
85
|
import { createServer } from "http";
|
|
@@ -290,7 +332,7 @@ async function renderTree(absDir, relDir) {
|
|
|
290
332
|
return `${relDir || "."}/ (directory tree)
|
|
291
333
|
${lines.join("\n")}`;
|
|
292
334
|
}
|
|
293
|
-
async function expandMentions(message,
|
|
335
|
+
async function expandMentions(message, cwd3, tokenBudget = DEFAULT_TOKEN_BUDGET) {
|
|
294
336
|
const mentions = extractMentions(message);
|
|
295
337
|
if (mentions.length === 0) return { context: "", notice: null };
|
|
296
338
|
const blocks = [];
|
|
@@ -300,7 +342,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
300
342
|
for (const mention of mentions) {
|
|
301
343
|
if (spent >= tokenBudget) break;
|
|
302
344
|
const raw = mention.startsWith("~/") ? join(process.env.HOME ?? "", mention.slice(2)) : mention;
|
|
303
|
-
const direct = isAbsolute(raw) ? raw : resolve(
|
|
345
|
+
const direct = isAbsolute(raw) ? raw : resolve(cwd3, raw);
|
|
304
346
|
let isDir = false;
|
|
305
347
|
try {
|
|
306
348
|
isDir = (await stat(direct)).isDirectory();
|
|
@@ -308,7 +350,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
308
350
|
isDir = false;
|
|
309
351
|
}
|
|
310
352
|
if (isDir) {
|
|
311
|
-
const rel = relative(
|
|
353
|
+
const rel = relative(cwd3, direct);
|
|
312
354
|
const tree = await renderTree(direct, rel);
|
|
313
355
|
const cost = estTokens(tree);
|
|
314
356
|
if (spent + cost <= tokenBudget) {
|
|
@@ -321,7 +363,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
321
363
|
let files = [];
|
|
322
364
|
try {
|
|
323
365
|
files = await fg(raw, {
|
|
324
|
-
cwd:
|
|
366
|
+
cwd: cwd3,
|
|
325
367
|
ignore: IGNORE,
|
|
326
368
|
onlyFiles: true,
|
|
327
369
|
dot: true,
|
|
@@ -333,7 +375,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
333
375
|
}
|
|
334
376
|
if (files.length === 0) {
|
|
335
377
|
try {
|
|
336
|
-
if ((await stat(direct)).isFile()) files = [relative(
|
|
378
|
+
if ((await stat(direct)).isFile()) files = [relative(cwd3, direct)];
|
|
337
379
|
} catch {
|
|
338
380
|
}
|
|
339
381
|
}
|
|
@@ -344,7 +386,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
344
386
|
seen.add(rel);
|
|
345
387
|
let buf;
|
|
346
388
|
try {
|
|
347
|
-
buf = await readFile2(resolve(
|
|
389
|
+
buf = await readFile2(resolve(cwd3, rel), "utf8");
|
|
348
390
|
} catch {
|
|
349
391
|
continue;
|
|
350
392
|
}
|
|
@@ -424,11 +466,528 @@ function buildSubagentTool(deps) {
|
|
|
424
466
|
};
|
|
425
467
|
}
|
|
426
468
|
|
|
469
|
+
// src/eyes-cli.ts
|
|
470
|
+
function flagValue(args, flag) {
|
|
471
|
+
const i = args.indexOf(flag);
|
|
472
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : void 0;
|
|
473
|
+
}
|
|
474
|
+
function printFinding(f) {
|
|
475
|
+
line();
|
|
476
|
+
line(f.loaded ? c.green(" \u2713 loaded") + c.dim(` ${f.url}`) : c.red(" \u2717 not healthy") + c.dim(` ${f.url}`));
|
|
477
|
+
if (f.httpStatus !== null) line(c.dim(` HTTP ${f.httpStatus}`) + (f.title ? c.dim(` \xB7 ${f.title}`) : ""));
|
|
478
|
+
if (f.pageErrors.length) {
|
|
479
|
+
line(c.red(` page errors (${f.pageErrors.length}):`));
|
|
480
|
+
for (const e of f.pageErrors.slice(0, 8)) line(" " + c.dim(e));
|
|
481
|
+
}
|
|
482
|
+
if (f.consoleErrors.length) {
|
|
483
|
+
line(c.yellow(` console errors (${f.consoleErrors.length}):`));
|
|
484
|
+
for (const e of f.consoleErrors.slice(0, 8)) line(" " + c.dim(e));
|
|
485
|
+
}
|
|
486
|
+
if (f.failedRequests.length) {
|
|
487
|
+
line(c.yellow(` failed requests (${f.failedRequests.length}):`));
|
|
488
|
+
for (const r of f.failedRequests.slice(0, 8)) line(" " + c.dim(`${r.method} ${r.url}`));
|
|
489
|
+
}
|
|
490
|
+
if (f.screenshot) line(c.cyan(` screenshot \u2192 ${f.screenshot}`));
|
|
491
|
+
line(c.dim(" " + f.verdict));
|
|
492
|
+
}
|
|
493
|
+
async function runEyesSubcommand(raw) {
|
|
494
|
+
if (raw[0] !== "eyes" && raw[0] !== "see") return null;
|
|
495
|
+
const rest = raw.slice(1);
|
|
496
|
+
const wantsJson = rest.includes("--json");
|
|
497
|
+
const noShot = rest.includes("--no-screenshot");
|
|
498
|
+
const shotPath = flagValue(rest, "--out");
|
|
499
|
+
const url = rest.find((t) => !t.startsWith("-"));
|
|
500
|
+
if (!url) {
|
|
501
|
+
if (wantsJson) {
|
|
502
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "missing URL" }) + "\n");
|
|
503
|
+
} else {
|
|
504
|
+
line(c.red(" usage: wholestack eyes <url> [--json] [--no-screenshot] [--out file.png]"));
|
|
505
|
+
}
|
|
506
|
+
return 1;
|
|
507
|
+
}
|
|
508
|
+
const res = await inspectUrl({
|
|
509
|
+
cwd: process.cwd(),
|
|
510
|
+
url,
|
|
511
|
+
capture: !noShot,
|
|
512
|
+
screenshotPath: shotPath
|
|
513
|
+
});
|
|
514
|
+
if (!("ok" in res) || res.ok !== true) {
|
|
515
|
+
const error = res.error;
|
|
516
|
+
if (wantsJson) {
|
|
517
|
+
process.stdout.write(JSON.stringify({ ok: false, error }) + "\n");
|
|
518
|
+
} else {
|
|
519
|
+
const lines = error.split("\n");
|
|
520
|
+
line(c.red(" \u2717 " + lines[0]));
|
|
521
|
+
for (const l of lines.slice(1)) line(c.dim(" " + l));
|
|
522
|
+
}
|
|
523
|
+
return 1;
|
|
524
|
+
}
|
|
525
|
+
if (wantsJson) {
|
|
526
|
+
const { dataUrl, ...rest2 } = res;
|
|
527
|
+
void dataUrl;
|
|
528
|
+
process.stdout.write(JSON.stringify(rest2) + "\n");
|
|
529
|
+
return res.loaded ? 0 : 1;
|
|
530
|
+
}
|
|
531
|
+
printFinding(res);
|
|
532
|
+
return res.loaded ? 0 : 1;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/workers.ts
|
|
536
|
+
import { spawn as spawn2 } from "child_process";
|
|
537
|
+
import { mkdtemp, rm as rm2 } from "fs/promises";
|
|
538
|
+
import { tmpdir } from "os";
|
|
539
|
+
import { join as join2 } from "path";
|
|
540
|
+
import { generateText as generateText2, stepCountIs as stepCountIs2, tool as tool2 } from "ai";
|
|
541
|
+
import { z as z2 } from "zod";
|
|
542
|
+
var WORKER_TOOLS = [
|
|
543
|
+
"read_file",
|
|
544
|
+
"write_file",
|
|
545
|
+
"edit_file",
|
|
546
|
+
"multi_edit",
|
|
547
|
+
"list_dir",
|
|
548
|
+
"glob",
|
|
549
|
+
"grep",
|
|
550
|
+
"run_command",
|
|
551
|
+
"todo_write",
|
|
552
|
+
"todo_read"
|
|
553
|
+
];
|
|
554
|
+
function workerToolset(worktreeCtx) {
|
|
555
|
+
const all = buildTools(worktreeCtx);
|
|
556
|
+
const out = {};
|
|
557
|
+
for (const k of WORKER_TOOLS) if (all[k]) out[k] = all[k];
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
var WORKER_SYSTEM = "You are an autonomous engineer spawned by a lead engineer to complete ONE focused, self-contained slice of a larger task. You have a full coding toolset (read_file, write_file, edit_file, multi_edit, list_dir, glob, grep, run_command) and you are working in your OWN isolated git worktree \u2014 your edits cannot affect anyone else, so edit freely. Approach: (1) explore the relevant files, (2) make the change, (3) verify it (build/test/typecheck where it makes sense), (4) report. Stay strictly within your assigned slice \u2014 do not refactor unrelated code. When done, return a tight summary: what you changed, which files, and how you verified it.";
|
|
561
|
+
function git(args, cwd3, timeoutMs = 3e4) {
|
|
562
|
+
return new Promise((res) => {
|
|
563
|
+
const child = spawn2("git", args, { cwd: cwd3, shell: false });
|
|
564
|
+
let out = "";
|
|
565
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
566
|
+
const sink = (b) => {
|
|
567
|
+
out += b.toString();
|
|
568
|
+
};
|
|
569
|
+
child.stdout.on("data", sink);
|
|
570
|
+
child.stderr.on("data", sink);
|
|
571
|
+
child.on("error", (e) => {
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
res({ code: 1, out: e.message });
|
|
574
|
+
});
|
|
575
|
+
child.on("close", (code) => {
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
res({ code: code ?? 1, out });
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
async function repoRoot(dir) {
|
|
582
|
+
const r = await git(["rev-parse", "--show-toplevel"], dir);
|
|
583
|
+
if (r.code !== 0) return null;
|
|
584
|
+
const top = r.out.trim();
|
|
585
|
+
return top || null;
|
|
586
|
+
}
|
|
587
|
+
function slug(label) {
|
|
588
|
+
const base2 = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "worker";
|
|
589
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
590
|
+
return `${base2}-${rand}`;
|
|
591
|
+
}
|
|
592
|
+
var MAX_DIFF_CHARS = 24e3;
|
|
593
|
+
function clampDiff(s) {
|
|
594
|
+
if (s.length <= MAX_DIFF_CHARS) return s;
|
|
595
|
+
return s.slice(0, MAX_DIFF_CHARS) + `
|
|
596
|
+
\u2026 [diff truncated at ${MAX_DIFF_CHARS} chars \u2014 ${s.length} total]`;
|
|
597
|
+
}
|
|
598
|
+
async function runOneWorker(deps, root, label, task, maxSteps, timeoutMs) {
|
|
599
|
+
const name = slug(label);
|
|
600
|
+
const branch = `zeta-worker/${name}`;
|
|
601
|
+
let worktree = null;
|
|
602
|
+
try {
|
|
603
|
+
const parent = await mkdtemp(join2(tmpdir(), "zeta-worker-"));
|
|
604
|
+
worktree = join2(parent, name);
|
|
605
|
+
const add = await git(["worktree", "add", "-b", branch, worktree, "HEAD"], root);
|
|
606
|
+
if (add.code !== 0) {
|
|
607
|
+
return { label, ok: false, error: `git worktree add failed: ${add.out.trim()}` };
|
|
608
|
+
}
|
|
609
|
+
const worktreeCtx = {
|
|
610
|
+
...deps.ctx,
|
|
611
|
+
cwd: worktree,
|
|
612
|
+
permissions: new Permissions("yolo")
|
|
613
|
+
};
|
|
614
|
+
const tools = workerToolset(worktreeCtx);
|
|
615
|
+
const ac = new AbortController();
|
|
616
|
+
const killer = setTimeout(() => ac.abort(), timeoutMs);
|
|
617
|
+
let summary = "";
|
|
618
|
+
try {
|
|
619
|
+
const r = await generateText2({
|
|
620
|
+
model: deps.model,
|
|
621
|
+
system: WORKER_SYSTEM,
|
|
622
|
+
prompt: task,
|
|
623
|
+
tools,
|
|
624
|
+
stopWhen: stepCountIs2(maxSteps),
|
|
625
|
+
abortSignal: ac.signal
|
|
626
|
+
});
|
|
627
|
+
summary = r.text.trim();
|
|
628
|
+
} catch (e) {
|
|
629
|
+
summary = `(worker aborted or errored before reporting: ${e.message})`;
|
|
630
|
+
} finally {
|
|
631
|
+
clearTimeout(killer);
|
|
632
|
+
}
|
|
633
|
+
await git(["add", "-A"], worktree);
|
|
634
|
+
const statusR = await git(["status", "--porcelain"], worktree);
|
|
635
|
+
const files = statusR.out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
636
|
+
const diffR = await git(["diff", "--cached", "HEAD"], worktree);
|
|
637
|
+
const diff = diffR.code === 0 ? clampDiff(diffR.out) : "";
|
|
638
|
+
line(c.dim(` \u21B3 ${c.cyan(label)} done \xB7 ${files.length} file(s) changed`));
|
|
639
|
+
return {
|
|
640
|
+
label,
|
|
641
|
+
ok: true,
|
|
642
|
+
summary,
|
|
643
|
+
diff,
|
|
644
|
+
files,
|
|
645
|
+
worktree
|
|
646
|
+
};
|
|
647
|
+
} catch (e) {
|
|
648
|
+
line(c.dim(` \u21B3 ${c.cyan(label)} ${c.red("failed")}`));
|
|
649
|
+
return { label, ok: false, error: e.message, worktree: worktree ?? void 0 };
|
|
650
|
+
} finally {
|
|
651
|
+
if (worktree) {
|
|
652
|
+
await git(["worktree", "remove", "--force", worktree], root);
|
|
653
|
+
await git(["branch", "-D", branch], root);
|
|
654
|
+
const parent = join2(worktree, "..");
|
|
655
|
+
await rm2(parent, { recursive: true, force: true }).catch(() => {
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function buildWorkersTool(deps) {
|
|
661
|
+
const maxSteps = deps.maxSteps ?? 24;
|
|
662
|
+
const maxWorkers = deps.maxWorkers ?? 4;
|
|
663
|
+
const timeoutMs = deps.timeoutMs ?? 3e5;
|
|
664
|
+
return {
|
|
665
|
+
spawn_workers: tool2({
|
|
666
|
+
description: "Run several WRITABLE engineer sub-agents IN PARALLEL, each on its own focused, self-contained slice \u2014 each works in its OWN isolated git worktree so their edits can never stomp the main tree or each other. Each worker can read, write, edit, and run commands inside its sandbox, then returns a summary + the diff of what it changed. Nothing is auto-merged: review each returned diff and apply the good ones yourself. Use for parallel edit-heavy work (scaffold N modules, refactor N files at once). For a single linear edit, just do it directly.",
|
|
667
|
+
inputSchema: z2.object({
|
|
668
|
+
tasks: z2.array(
|
|
669
|
+
z2.object({
|
|
670
|
+
label: z2.string().describe("Short label for this slice, e.g. 'api-route' or 'refactor-auth'."),
|
|
671
|
+
task: z2.string().describe(
|
|
672
|
+
"The complete, self-contained instruction for this worker. It must be doable WITHOUT coordinating with the other workers (they run concurrently in separate worktrees)."
|
|
673
|
+
)
|
|
674
|
+
})
|
|
675
|
+
).min(1).max(maxWorkers).describe(`1\u2013${maxWorkers} INDEPENDENT, edit-heavy tasks to run concurrently in isolated worktrees.`)
|
|
676
|
+
}),
|
|
677
|
+
execute: async ({ tasks: tasks2 }) => {
|
|
678
|
+
if (deps.ctx.permissions.guardMutation() === "deny") {
|
|
679
|
+
return { ok: false, error: deps.ctx.permissions.planRefusal() };
|
|
680
|
+
}
|
|
681
|
+
const root = await repoRoot(deps.ctx.cwd);
|
|
682
|
+
if (!root) {
|
|
683
|
+
return {
|
|
684
|
+
ok: false,
|
|
685
|
+
error: "spawn_workers needs a git repository (it isolates each worker in a `git worktree`). Run `git init` in the workspace first, or make the edits directly."
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
line(c.dim(` \u21C9 spawning ${tasks2.length} writable worker(s) in isolated worktrees`));
|
|
689
|
+
const results = await Promise.all(
|
|
690
|
+
tasks2.map(({ label, task }) => runOneWorker(deps, root, label, task, maxSteps, timeoutMs))
|
|
691
|
+
);
|
|
692
|
+
const changed = results.filter((r) => r.ok && (r.files?.length ?? 0) > 0).length;
|
|
693
|
+
return {
|
|
694
|
+
ok: true,
|
|
695
|
+
count: results.length,
|
|
696
|
+
changed,
|
|
697
|
+
note: "Each result carries a `diff` (unified patch vs HEAD) and `files` (touched paths). The worktrees are already removed \u2014 apply the diffs you want into the main tree yourself (e.g. via write_file/edit_file or by piping the diff through `git apply`), then verify.",
|
|
698
|
+
results
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/ops-subcommand.ts
|
|
706
|
+
import { stdout, cwd } from "process";
|
|
707
|
+
function jsonOut(obj) {
|
|
708
|
+
stdout.write(JSON.stringify(obj) + "\n");
|
|
709
|
+
}
|
|
710
|
+
function base() {
|
|
711
|
+
return webBaseUrl().replace(/\/$/, "");
|
|
712
|
+
}
|
|
713
|
+
async function probe(url, timeoutMs) {
|
|
714
|
+
const controller = new AbortController();
|
|
715
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
716
|
+
const started = Date.now();
|
|
717
|
+
try {
|
|
718
|
+
const resp = await fetch(url, { redirect: "follow", signal: controller.signal });
|
|
719
|
+
return { status: resp.status, latencyMs: Date.now() - started };
|
|
720
|
+
} catch (e) {
|
|
721
|
+
const err = e;
|
|
722
|
+
return {
|
|
723
|
+
latencyMs: Date.now() - started,
|
|
724
|
+
error: err.name === "AbortError" ? `timed out after ${timeoutMs}ms` : err.message
|
|
725
|
+
};
|
|
726
|
+
} finally {
|
|
727
|
+
clearTimeout(timer);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async function runOpsSubcommand(raw) {
|
|
731
|
+
if (raw[0] !== "ops") return null;
|
|
732
|
+
const rest = raw.slice(1);
|
|
733
|
+
const verb = rest[0];
|
|
734
|
+
const wantsJson = rest.includes("--json");
|
|
735
|
+
const positional = rest.slice(1).filter((a) => !a.startsWith("-"));
|
|
736
|
+
if (verb === "health") {
|
|
737
|
+
const arg = positional[0];
|
|
738
|
+
if (!arg) {
|
|
739
|
+
line(c.red(" usage: wholestack ops health <url>"));
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
742
|
+
const origin = (/^https?:\/\//i.test(arg) ? arg : `https://${arg}`).replace(/\/$/, "");
|
|
743
|
+
const root = await probe(origin + "/", 15e3);
|
|
744
|
+
const health = await probe(origin + "/api/health", 15e3);
|
|
745
|
+
const healthy = root.status != null && root.status >= 200 && root.status < 400;
|
|
746
|
+
if (wantsJson) {
|
|
747
|
+
jsonOut({ url: origin, healthy, root, health });
|
|
748
|
+
return healthy ? 0 : 1;
|
|
749
|
+
}
|
|
750
|
+
line("");
|
|
751
|
+
if (root.error) line(" " + c.red(`\u2717 / ${root.error}`));
|
|
752
|
+
else
|
|
753
|
+
line(
|
|
754
|
+
" " + (healthy ? c.green("\u2713 /") : c.yellow("\u2022 /")) + c.dim(` ${root.status} ${root.latencyMs}ms`)
|
|
755
|
+
);
|
|
756
|
+
if (health.error) line(" " + c.dim(`\u2022 /api/health ${health.error}`));
|
|
757
|
+
else line(" " + c.dim(`\u2022 /api/health ${health.status} ${health.latencyMs}ms`));
|
|
758
|
+
return healthy ? 0 : 1;
|
|
759
|
+
}
|
|
760
|
+
if (verb === "status") {
|
|
761
|
+
const deploymentId = positional[0];
|
|
762
|
+
if (!deploymentId) {
|
|
763
|
+
line(c.red(" usage: wholestack ops status <deploymentId> [buildId]"));
|
|
764
|
+
return 1;
|
|
765
|
+
}
|
|
766
|
+
const buildId = positional[1] ?? readProjectLink(cwd())?.buildId;
|
|
767
|
+
if (!buildId) {
|
|
768
|
+
line(c.red(" no build id \u2014 pass one or run inside a linked project."));
|
|
769
|
+
return 1;
|
|
770
|
+
}
|
|
771
|
+
const r = await apiJson(
|
|
772
|
+
`${base()}/api/zeta/build/${encodeURIComponent(buildId)}/deploy?deploymentId=${encodeURIComponent(deploymentId)}`
|
|
773
|
+
);
|
|
774
|
+
if (wantsJson) {
|
|
775
|
+
jsonOut(r.ok ? { ok: true, ...r.data } : { ok: false, error: r.error });
|
|
776
|
+
return r.ok && r.data.status === "ready" ? 0 : 1;
|
|
777
|
+
}
|
|
778
|
+
line("");
|
|
779
|
+
if (!r.ok) {
|
|
780
|
+
line(" " + c.red(`\u2717 ${r.error}`));
|
|
781
|
+
return 1;
|
|
782
|
+
}
|
|
783
|
+
const status = r.data.status ?? "unknown";
|
|
784
|
+
line(" " + (status === "ready" ? c.green(`\u2713 ${status}`) : c.cyan(`\u2022 ${status}`)));
|
|
785
|
+
if (r.data.url) line(" " + c.cyan(r.data.url));
|
|
786
|
+
return status === "ready" ? 0 : status === "error" ? 1 : 0;
|
|
787
|
+
}
|
|
788
|
+
if (verb === "metrics") {
|
|
789
|
+
const r = await readMetrics(base());
|
|
790
|
+
if (wantsJson) {
|
|
791
|
+
jsonOut(r.ok ? { ok: true, metrics: r.metrics } : { ok: false, error: r.error });
|
|
792
|
+
return r.ok ? 0 : 1;
|
|
793
|
+
}
|
|
794
|
+
line("");
|
|
795
|
+
if (!r.ok || !r.metrics) {
|
|
796
|
+
line(" " + c.red(`\u2717 ${r.error ?? "no metrics"}`));
|
|
797
|
+
return 1;
|
|
798
|
+
}
|
|
799
|
+
const m = r.metrics;
|
|
800
|
+
line(
|
|
801
|
+
" connected: " + (m.connected.posthog ? c.green("posthog") : c.dim("posthog")) + " \xB7 " + (m.connected.stripe ? c.green("stripe") : c.dim("stripe"))
|
|
802
|
+
);
|
|
803
|
+
if (m.traffic) line(" " + c.dim(`visitors 7d ${m.traffic.visitors7d} \xB7 signups 7d ${m.traffic.signups7d}`));
|
|
804
|
+
if (m.revenue) line(" " + c.dim(`MRR $${m.revenue.mrr} \xB7 subs ${m.revenue.activeSubscriptions}`));
|
|
805
|
+
return 0;
|
|
806
|
+
}
|
|
807
|
+
if (verb === "errors") {
|
|
808
|
+
const r = await readMetrics(base());
|
|
809
|
+
const errs = r.ok && r.metrics ? r.metrics.errors ?? [] : [];
|
|
810
|
+
const connected = r.ok && r.metrics ? r.metrics.connected.posthog : false;
|
|
811
|
+
if (wantsJson) {
|
|
812
|
+
jsonOut(r.ok ? { ok: true, connected, errors: errs } : { ok: false, error: r.error });
|
|
813
|
+
return r.ok ? 0 : 1;
|
|
814
|
+
}
|
|
815
|
+
line("");
|
|
816
|
+
if (!r.ok || !r.metrics) {
|
|
817
|
+
line(" " + c.red(`\u2717 ${r.error ?? "no metrics"}`));
|
|
818
|
+
return 1;
|
|
819
|
+
}
|
|
820
|
+
if (!connected) {
|
|
821
|
+
line(" " + c.dim("no error provider connected \u2014 connect PostHog (or set SENTRY_* env)."));
|
|
822
|
+
return 0;
|
|
823
|
+
}
|
|
824
|
+
if (errs.length === 0) {
|
|
825
|
+
line(" " + c.green("\u2713 no recent errors"));
|
|
826
|
+
return 0;
|
|
827
|
+
}
|
|
828
|
+
for (const e of errs.slice(0, 10)) {
|
|
829
|
+
line(" " + c.red("\u2022 ") + e.title + c.dim(` \xD7${e.count}`));
|
|
830
|
+
}
|
|
831
|
+
return 0;
|
|
832
|
+
}
|
|
833
|
+
line(c.red(` unknown: wholestack ops ${verb ?? ""}`));
|
|
834
|
+
line(c.dim(" verbs: health <url> \xB7 status <deploymentId> [buildId] \xB7 metrics \xB7 errors (+ --json)"));
|
|
835
|
+
return 1;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/learned-tools.ts
|
|
839
|
+
import { tool as tool3 } from "ai";
|
|
840
|
+
import { z as z3 } from "zod";
|
|
841
|
+
import { basename } from "path";
|
|
842
|
+
var KIND_VALUES = ["worked", "failed", "quirk", "note"];
|
|
843
|
+
function defaultScope(ctx) {
|
|
844
|
+
return basename(ctx.cwd) || "global";
|
|
845
|
+
}
|
|
846
|
+
function buildLearnedMemoryTools(deps) {
|
|
847
|
+
const { ctx } = deps;
|
|
848
|
+
const { permissions } = ctx;
|
|
849
|
+
return {
|
|
850
|
+
remember: tool3({
|
|
851
|
+
description: "Record ONE durable lesson learned this session so future sessions inherit it (cross-session memory). Use after finishing a non-trivial task to capture: a pattern that WORKED, an approach that FAILED, or a project QUIRK. Keep it to a single atomic fact \u2014 one lesson per call. Auto-loaded into the system prompt at the start of every session.",
|
|
852
|
+
inputSchema: z3.object({
|
|
853
|
+
text: z3.string().min(4).describe("The lesson \u2014 one self-contained, durable fact. Be specific and actionable."),
|
|
854
|
+
kind: z3.enum(KIND_VALUES).default("note").describe("worked = a pattern that worked; failed = a dead end to avoid; quirk = a project gotcha; note = general."),
|
|
855
|
+
tags: z3.array(z3.string()).optional().describe("Optional keywords to make this lesson findable later, e.g. ['prisma','migrations']."),
|
|
856
|
+
scope: z3.string().optional().describe("Project/repo this applies to. Omit to use the current project; use 'global' for everywhere.")
|
|
857
|
+
}),
|
|
858
|
+
execute: async ({ text, kind, tags, scope }) => {
|
|
859
|
+
if (permissions.guardMutation() === "deny") return { ok: false, error: permissions.planRefusal() };
|
|
860
|
+
try {
|
|
861
|
+
const saved = rememberLesson({
|
|
862
|
+
text,
|
|
863
|
+
kind,
|
|
864
|
+
tags,
|
|
865
|
+
scope: scope ?? defaultScope(ctx)
|
|
866
|
+
});
|
|
867
|
+
toolLine("remember", `${saved.kind} \xB7 ${saved.scope}`);
|
|
868
|
+
line(c.dim(` \u21B3 saved ${c.cyan(saved.slug)}`));
|
|
869
|
+
return {
|
|
870
|
+
ok: true,
|
|
871
|
+
slug: saved.slug,
|
|
872
|
+
kind: saved.kind,
|
|
873
|
+
scope: saved.scope,
|
|
874
|
+
tags: saved.tags,
|
|
875
|
+
note: "Lesson stored. It will auto-load into the system prompt next session."
|
|
876
|
+
};
|
|
877
|
+
} catch (e) {
|
|
878
|
+
return { ok: false, error: e.message };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}),
|
|
882
|
+
recall: tool3({
|
|
883
|
+
description: "Search the lessons you learned in earlier sessions (cross-session memory). Use to check whether you've already solved something here, or to review what worked/failed before. Returns matching lessons newest-first. Lessons from the current project plus any 'global' lessons are always eligible.",
|
|
884
|
+
inputSchema: z3.object({
|
|
885
|
+
query: z3.string().optional().describe("Free-text search over lesson bodies, tags, and kind. Omit to list everything."),
|
|
886
|
+
scope: z3.string().optional().describe("Limit to a project/repo (plus global). Omit to use the current project."),
|
|
887
|
+
limit: z3.number().int().min(1).max(50).default(10).describe("Max lessons to return.")
|
|
888
|
+
}),
|
|
889
|
+
execute: async ({ query, scope, limit }) => {
|
|
890
|
+
try {
|
|
891
|
+
const results = searchLessons({ query, scope: scope ?? defaultScope(ctx), limit });
|
|
892
|
+
toolLine("recall", query ? `"${query}"` : "all lessons");
|
|
893
|
+
line(c.dim(` \u21B3 ${results.length} lesson(s)`));
|
|
894
|
+
return {
|
|
895
|
+
ok: true,
|
|
896
|
+
count: results.length,
|
|
897
|
+
lessons: results.map((l) => ({
|
|
898
|
+
slug: l.slug,
|
|
899
|
+
kind: l.kind,
|
|
900
|
+
scope: l.scope,
|
|
901
|
+
tags: l.tags,
|
|
902
|
+
text: l.text
|
|
903
|
+
}))
|
|
904
|
+
};
|
|
905
|
+
} catch (e) {
|
|
906
|
+
return { ok: false, error: e.message };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
})
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/skills-cli.ts
|
|
914
|
+
import { stdout as stdout2 } from "process";
|
|
915
|
+
async function runSkillsSubcommand(raw) {
|
|
916
|
+
if (raw[0] !== "skills") return null;
|
|
917
|
+
const rest = raw.slice(1);
|
|
918
|
+
const sub = rest[0] && !rest[0].startsWith("-") ? rest[0] : "list";
|
|
919
|
+
const registry = SkillRegistry.load(process.cwd());
|
|
920
|
+
if (sub === "show") {
|
|
921
|
+
const name = rest[1];
|
|
922
|
+
if (!name) {
|
|
923
|
+
line(c.red("usage: wholestack skills show <name>"));
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
926
|
+
const skill = registry.get(name);
|
|
927
|
+
if (!skill) {
|
|
928
|
+
line(c.red(`no skill named "${name}"`));
|
|
929
|
+
const suggestions = registry.suggest(name);
|
|
930
|
+
if (suggestions.length) line(c.dim(`did you mean: ${suggestions.join(", ")}`));
|
|
931
|
+
return 1;
|
|
932
|
+
}
|
|
933
|
+
if (rest.includes("--json")) {
|
|
934
|
+
stdout2.write(
|
|
935
|
+
JSON.stringify(
|
|
936
|
+
{
|
|
937
|
+
name: skill.name,
|
|
938
|
+
description: skill.description,
|
|
939
|
+
whenToUse: skill.whenToUse,
|
|
940
|
+
source: skill.source,
|
|
941
|
+
procedure: skill.body
|
|
942
|
+
},
|
|
943
|
+
null,
|
|
944
|
+
2
|
|
945
|
+
) + "\n"
|
|
946
|
+
);
|
|
947
|
+
return 0;
|
|
948
|
+
}
|
|
949
|
+
line(c.bold(skill.name) + c.dim(` \xB7 ${skill.source}`));
|
|
950
|
+
line(c.dim(skill.whenToUse));
|
|
951
|
+
line("");
|
|
952
|
+
line(skill.body);
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
const skills = registry.list();
|
|
956
|
+
if (rest.includes("--json")) {
|
|
957
|
+
stdout2.write(
|
|
958
|
+
JSON.stringify(
|
|
959
|
+
skills.map((s) => ({
|
|
960
|
+
name: s.name,
|
|
961
|
+
description: s.description,
|
|
962
|
+
whenToUse: s.whenToUse,
|
|
963
|
+
source: s.source
|
|
964
|
+
})),
|
|
965
|
+
null,
|
|
966
|
+
2
|
|
967
|
+
) + "\n"
|
|
968
|
+
);
|
|
969
|
+
return 0;
|
|
970
|
+
}
|
|
971
|
+
if (skills.length === 0) {
|
|
972
|
+
line(c.dim("no skills installed."));
|
|
973
|
+
line(c.dim("add *.md files under ~/.wholestack/skills or <cwd>/.wholestack/skills"));
|
|
974
|
+
return 0;
|
|
975
|
+
}
|
|
976
|
+
line(c.bold("skills"));
|
|
977
|
+
const width = Math.max(...skills.map((s) => s.name.length));
|
|
978
|
+
for (const s of skills) {
|
|
979
|
+
line(" " + c.cyan(s.name.padEnd(width)) + " " + s.whenToUse + c.dim(` \xB7 ${s.source}`));
|
|
980
|
+
}
|
|
981
|
+
line("");
|
|
982
|
+
line(c.dim("view one: wholestack skills show <name>"));
|
|
983
|
+
return 0;
|
|
984
|
+
}
|
|
985
|
+
|
|
427
986
|
// src/images.ts
|
|
428
987
|
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
429
|
-
import { resolve as resolve2, extname, relative as relative2, isAbsolute as isAbsolute2, join as
|
|
988
|
+
import { resolve as resolve2, extname, relative as relative2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
430
989
|
import { execFile } from "child_process";
|
|
431
|
-
import { tmpdir, platform as platform2 } from "os";
|
|
990
|
+
import { tmpdir as tmpdir2, platform as platform2 } from "os";
|
|
432
991
|
var EXT_TO_MEDIA = {
|
|
433
992
|
".png": "image/png",
|
|
434
993
|
".jpg": "image/jpeg",
|
|
@@ -447,7 +1006,7 @@ function run(cmd, args) {
|
|
|
447
1006
|
});
|
|
448
1007
|
}
|
|
449
1008
|
async function grabClipboardImage() {
|
|
450
|
-
const out =
|
|
1009
|
+
const out = join3(tmpdir2(), `zeta-clip-${process.pid}-${process.hrtime.bigint()}.png`);
|
|
451
1010
|
const os = platform2();
|
|
452
1011
|
if (os === "darwin") {
|
|
453
1012
|
const script = `set thePath to "${out}"
|
|
@@ -481,7 +1040,7 @@ function imageTokens(message) {
|
|
|
481
1040
|
for (let m = re.exec(message); m; m = re.exec(message)) out.add(m[1]);
|
|
482
1041
|
return [...out];
|
|
483
1042
|
}
|
|
484
|
-
async function scanImages(message,
|
|
1043
|
+
async function scanImages(message, cwd3) {
|
|
485
1044
|
const tokens = imageTokens(message);
|
|
486
1045
|
const images = [];
|
|
487
1046
|
const skipped = [];
|
|
@@ -510,8 +1069,8 @@ async function scanImages(message, cwd2) {
|
|
|
510
1069
|
}
|
|
511
1070
|
}
|
|
512
1071
|
for (const tok of tokens) {
|
|
513
|
-
const raw = tok.startsWith("~/") ?
|
|
514
|
-
const abs = isAbsolute2(raw) ? raw : resolve2(
|
|
1072
|
+
const raw = tok.startsWith("~/") ? join3(process.env.HOME ?? "", tok.slice(2)) : tok;
|
|
1073
|
+
const abs = isAbsolute2(raw) ? raw : resolve2(cwd3, raw);
|
|
515
1074
|
const mediaType = EXT_TO_MEDIA[extname(abs).toLowerCase()];
|
|
516
1075
|
if (!mediaType) continue;
|
|
517
1076
|
let bytes;
|
|
@@ -522,7 +1081,7 @@ async function scanImages(message, cwd2) {
|
|
|
522
1081
|
} catch {
|
|
523
1082
|
continue;
|
|
524
1083
|
}
|
|
525
|
-
const label = isAbsolute2(raw) ? raw : relative2(
|
|
1084
|
+
const label = isAbsolute2(raw) ? raw : relative2(cwd3, abs);
|
|
526
1085
|
if (bytes > PER_IMAGE_BYTES || total + bytes > TOTAL_BYTES) {
|
|
527
1086
|
skipped.push(`${label} (${Math.round(bytes / 1024)}KB \u2014 over the size cap)`);
|
|
528
1087
|
continue;
|
|
@@ -656,21 +1215,21 @@ ${text}`;
|
|
|
656
1215
|
${body}
|
|
657
1216
|
`;
|
|
658
1217
|
}
|
|
659
|
-
function writeTranscript(messages, meta, file,
|
|
660
|
-
const abs = resolve3(
|
|
1218
|
+
function writeTranscript(messages, meta, file, cwd3) {
|
|
1219
|
+
const abs = resolve3(cwd3, file);
|
|
661
1220
|
writeFileSync(abs, renderTranscript(messages, meta), "utf8");
|
|
662
1221
|
return abs;
|
|
663
1222
|
}
|
|
664
1223
|
|
|
665
1224
|
// src/cli.ts
|
|
666
1225
|
import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
667
|
-
import { join as
|
|
1226
|
+
import { join as join6 } from "path";
|
|
668
1227
|
|
|
669
1228
|
// src/update-check.ts
|
|
670
1229
|
import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
671
|
-
import { join as
|
|
1230
|
+
import { join as join4 } from "path";
|
|
672
1231
|
var PKG = "wholestack";
|
|
673
|
-
var CACHE =
|
|
1232
|
+
var CACHE = join4(stateDir(), "update-check.json");
|
|
674
1233
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
675
1234
|
function readCache() {
|
|
676
1235
|
try {
|
|
@@ -724,8 +1283,8 @@ async function checkForUpdate(current) {
|
|
|
724
1283
|
|
|
725
1284
|
// src/access.ts
|
|
726
1285
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
727
|
-
import { join as
|
|
728
|
-
var CACHE2 =
|
|
1286
|
+
import { join as join5 } from "path";
|
|
1287
|
+
var CACHE2 = join5(stateDir(), "access.json");
|
|
729
1288
|
var TTL_MS = 60 * 60 * 1e3;
|
|
730
1289
|
function readCache2() {
|
|
731
1290
|
try {
|
|
@@ -748,10 +1307,18 @@ function clearAccessCache() {
|
|
|
748
1307
|
}
|
|
749
1308
|
}
|
|
750
1309
|
async function verifyAccess(webUrl, token, force = false) {
|
|
751
|
-
if (!token)
|
|
1310
|
+
if (!token) {
|
|
1311
|
+
return { active: false, paid: false, tier: null, freeDeliveries: null, degraded: false };
|
|
1312
|
+
}
|
|
752
1313
|
const cached = force ? null : readCache2();
|
|
753
1314
|
if (cached && Date.now() - cached.checkedAt < TTL_MS) {
|
|
754
|
-
return {
|
|
1315
|
+
return {
|
|
1316
|
+
active: cached.active,
|
|
1317
|
+
paid: cached.paid,
|
|
1318
|
+
tier: cached.tier,
|
|
1319
|
+
freeDeliveries: cached.freeDeliveries,
|
|
1320
|
+
degraded: false
|
|
1321
|
+
};
|
|
755
1322
|
}
|
|
756
1323
|
try {
|
|
757
1324
|
const ctrl = new AbortController();
|
|
@@ -762,18 +1329,44 @@ async function verifyAccess(webUrl, token, force = false) {
|
|
|
762
1329
|
});
|
|
763
1330
|
clearTimeout(t);
|
|
764
1331
|
if (res.status >= 500) {
|
|
765
|
-
return {
|
|
1332
|
+
return {
|
|
1333
|
+
active: true,
|
|
1334
|
+
paid: cached?.paid ?? false,
|
|
1335
|
+
tier: cached?.tier ?? null,
|
|
1336
|
+
freeDeliveries: cached?.freeDeliveries ?? null,
|
|
1337
|
+
degraded: true
|
|
1338
|
+
};
|
|
766
1339
|
}
|
|
767
1340
|
if (!res.ok) {
|
|
768
|
-
writeCache2({
|
|
769
|
-
|
|
1341
|
+
writeCache2({
|
|
1342
|
+
checkedAt: Date.now(),
|
|
1343
|
+
active: false,
|
|
1344
|
+
paid: false,
|
|
1345
|
+
tier: null,
|
|
1346
|
+
freeDeliveries: null
|
|
1347
|
+
});
|
|
1348
|
+
return { active: false, paid: false, tier: null, freeDeliveries: null, degraded: false };
|
|
770
1349
|
}
|
|
771
1350
|
const json = await res.json();
|
|
772
1351
|
const active = Boolean(json.active);
|
|
773
|
-
|
|
774
|
-
|
|
1352
|
+
const paid = Boolean(json.paid);
|
|
1353
|
+
const freeDeliveries = json.freeDeliveries ?? null;
|
|
1354
|
+
writeCache2({
|
|
1355
|
+
checkedAt: Date.now(),
|
|
1356
|
+
active,
|
|
1357
|
+
paid,
|
|
1358
|
+
tier: json.tier ?? null,
|
|
1359
|
+
freeDeliveries
|
|
1360
|
+
});
|
|
1361
|
+
return { active, paid, tier: json.tier ?? null, freeDeliveries, degraded: false };
|
|
775
1362
|
} catch {
|
|
776
|
-
return {
|
|
1363
|
+
return {
|
|
1364
|
+
active: true,
|
|
1365
|
+
paid: cached?.paid ?? false,
|
|
1366
|
+
tier: cached?.tier ?? null,
|
|
1367
|
+
freeDeliveries: cached?.freeDeliveries ?? null,
|
|
1368
|
+
degraded: true
|
|
1369
|
+
};
|
|
777
1370
|
}
|
|
778
1371
|
}
|
|
779
1372
|
function showPaywall(loggedIn, webUrl) {
|
|
@@ -781,36 +1374,36 @@ function showPaywall(loggedIn, webUrl) {
|
|
|
781
1374
|
line();
|
|
782
1375
|
line(" " + c.cyan("\u256D" + "\u2500".repeat(w) + "\u256E"));
|
|
783
1376
|
const row = (s) => line(" " + c.cyan("\u2502") + " " + s.padEnd(w - 2) + " " + c.cyan("\u2502"));
|
|
784
|
-
row(c.bold("
|
|
1377
|
+
row(c.bold("wholestack needs you signed in"));
|
|
785
1378
|
row("");
|
|
786
1379
|
if (!loggedIn) {
|
|
787
1380
|
row(c.dim("You're not signed in."));
|
|
788
|
-
row("Run " + c.cyan("
|
|
1381
|
+
row("Run " + c.cyan("wholestack login") + c.dim(" to start."));
|
|
789
1382
|
} else {
|
|
790
|
-
row(c.dim("
|
|
791
|
-
row("
|
|
1383
|
+
row(c.dim("Your token was rejected or expired."));
|
|
1384
|
+
row("Run " + c.cyan("wholestack login") + c.dim(" again."));
|
|
792
1385
|
}
|
|
793
1386
|
row("");
|
|
794
|
-
row(c.dim("
|
|
1387
|
+
row(c.dim("Free: 3 delivered apps/month \xB7 then ") + c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
|
|
795
1388
|
line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
|
|
796
1389
|
line();
|
|
797
1390
|
}
|
|
798
1391
|
|
|
799
1392
|
// src/cli.ts
|
|
800
1393
|
import { readFileSync as readFileSync3 } from "fs";
|
|
801
|
-
import { basename, dirname as dirname2 } from "path";
|
|
1394
|
+
import { basename as basename2, dirname as dirname2 } from "path";
|
|
802
1395
|
import { fileURLToPath } from "url";
|
|
803
1396
|
var VERSION = (() => {
|
|
804
1397
|
try {
|
|
805
1398
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
806
|
-
return JSON.parse(readFileSync3(
|
|
1399
|
+
return JSON.parse(readFileSync3(join6(here, "..", "package.json"), "utf8")).version;
|
|
807
1400
|
} catch {
|
|
808
1401
|
return "0.0.0";
|
|
809
1402
|
}
|
|
810
1403
|
})();
|
|
811
1404
|
function gitBranch(dir) {
|
|
812
1405
|
try {
|
|
813
|
-
const head = readFileSync3(
|
|
1406
|
+
const head = readFileSync3(join6(dir, ".git", "HEAD"), "utf8").trim();
|
|
814
1407
|
const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
|
|
815
1408
|
return m ? m[1] : head.slice(0, 7);
|
|
816
1409
|
} catch {
|
|
@@ -871,13 +1464,45 @@ ${c.bold("Usage")}
|
|
|
871
1464
|
wholestack "build me a todo app" run one prompt and exit
|
|
872
1465
|
wholestack -m zeta-g1-max "..." drive it with the most capable brain
|
|
873
1466
|
|
|
1467
|
+
${c.bold("Projects")} ${c.dim("(one spine across terminal, dashboard and the web IDE)")}
|
|
1468
|
+
wholestack projects list your platform projects
|
|
1469
|
+
wholestack pull <id> [dir] download a project's working tree
|
|
1470
|
+
wholestack push [dir] sync local edits back to the web IDE
|
|
1471
|
+
wholestack open [buildId] open a generated build in the web IDE
|
|
1472
|
+
|
|
1473
|
+
${c.bold("Ship")} ${c.dim("(go live \u2014 same credits + entitlements as the web app)")}
|
|
1474
|
+
wholestack deploy [buildId] live preview deploy (polls to ready)
|
|
1475
|
+
wholestack promote [buildId] [--domain d] durable production deploy (real DB)
|
|
1476
|
+
wholestack rollback [buildId] --deployment <id> restore the previous production deploy
|
|
1477
|
+
${c.dim("buildId defaults to the linked build in this dir (after a delivery or `open`)")}
|
|
1478
|
+
|
|
1479
|
+
${c.bold("Launch")} ${c.dim("(the studio's Launch HQ \u2014 go-to-market, in the terminal)")}
|
|
1480
|
+
wholestack launch [buildId] [--prompt "\u2026"] draw the AI go-to-market plan
|
|
1481
|
+
wholestack launch-kit [buildId] show the build's startup kit (brand/pricing/copy/\u2026)
|
|
1482
|
+
wholestack legal <terms|privacy|cookie|refund> generate a SaaS legal doc (Markdown \u2192 stdout)
|
|
1483
|
+
wholestack metrics your REAL traffic + revenue (PostHog + Stripe)
|
|
1484
|
+
wholestack waitlist [buildId] live waitlist signup count
|
|
1485
|
+
wholestack businesses every business workspace you own
|
|
1486
|
+
wholestack business [buildId] one workspace + its readiness scorecard
|
|
1487
|
+
${c.dim("add --json for machine-readable output")}
|
|
1488
|
+
|
|
1489
|
+
${c.bold("Prove gate (web2)")} ${c.dim("(deterministic \xB7 fail-closed \xB7 blocking exit code)")}
|
|
1490
|
+
wholestack prove <dir> [--json] gate a checked-out repo (local lockstep mirror)
|
|
1491
|
+
wholestack prove --remote --github o/r [--ref b] hosted, HMAC-signed verdict via /api/v1/prove
|
|
1492
|
+
${c.dim("checks: secrets/sinks/unauth-mutations + authorization-completeness (no orphan routes)")}
|
|
1493
|
+
|
|
874
1494
|
${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmos \xB7 signed cert)")}
|
|
875
|
-
wholestack audit <dir> <Contract> full auto-detect security sweep
|
|
1495
|
+
wholestack audit <dir> <Contract> full auto-detect security sweep (local)
|
|
1496
|
+
wholestack audit repo [--github o/r] [--pick] remote repo audit + live SSE progress
|
|
876
1497
|
wholestack prove <dir> <Contract> --property <k> prove one invariant
|
|
877
1498
|
wholestack verify <dir> <Contract> --cert c.json passthrough + signed certificate
|
|
878
1499
|
${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
|
|
879
1500
|
wholestack --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
|
|
880
1501
|
|
|
1502
|
+
${c.bold("GitHub")} ${c.dim("(private repos via App installation)")}
|
|
1503
|
+
wholestack github installations list linked GitHub App installs
|
|
1504
|
+
wholestack github repos --installation <id> list repos for an installation
|
|
1505
|
+
|
|
881
1506
|
${c.bold("Session")}
|
|
882
1507
|
-m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
|
|
883
1508
|
(lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
|
|
@@ -915,10 +1540,218 @@ ${c.bold("In a session")} ${c.dim("(type /help for the full list)")}
|
|
|
915
1540
|
${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
|
|
916
1541
|
${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
|
|
917
1542
|
${c.dim("spawn_agents tool fan a task out to parallel read-only sub-agents")}`;
|
|
1543
|
+
function flagValue2(rest, flag) {
|
|
1544
|
+
const i = rest.indexOf(flag);
|
|
1545
|
+
return i >= 0 ? rest[i + 1] : void 0;
|
|
1546
|
+
}
|
|
1547
|
+
async function runGithubSubcommand(raw) {
|
|
1548
|
+
if (raw[0] !== "github") return null;
|
|
1549
|
+
const sub = raw[1];
|
|
1550
|
+
const base2 = webBaseUrl();
|
|
1551
|
+
if (sub === "installations") {
|
|
1552
|
+
const r = await fetchGithubInstallations(base2);
|
|
1553
|
+
if (!r.ok) {
|
|
1554
|
+
line(c.red(` ${r.error}`));
|
|
1555
|
+
return 1;
|
|
1556
|
+
}
|
|
1557
|
+
if (!r.installations.length) {
|
|
1558
|
+
line(c.dim(" no installations \u2014 connect GitHub in the web dashboard."));
|
|
1559
|
+
return 0;
|
|
1560
|
+
}
|
|
1561
|
+
line();
|
|
1562
|
+
for (const row of r.installations) {
|
|
1563
|
+
const suspended = row.suspendedAt ? c.yellow(" \xB7 suspended") : "";
|
|
1564
|
+
line(` ${c.cyan(String(row.installationId))} ${row.accountLogin}${suspended}`);
|
|
1565
|
+
}
|
|
1566
|
+
line();
|
|
1567
|
+
return 0;
|
|
1568
|
+
}
|
|
1569
|
+
if (sub === "repos") {
|
|
1570
|
+
const rawId = flagValue2(raw.slice(2), "--installation");
|
|
1571
|
+
const installationId = rawId ? Number(rawId) : NaN;
|
|
1572
|
+
if (!rawId || !Number.isFinite(installationId)) {
|
|
1573
|
+
line(c.red(" usage: wholestack github repos --installation <id>"));
|
|
1574
|
+
return 1;
|
|
1575
|
+
}
|
|
1576
|
+
const r = await fetchGithubRepos(base2, installationId);
|
|
1577
|
+
if (!r.ok) {
|
|
1578
|
+
line(c.red(` ${r.error}`));
|
|
1579
|
+
return 1;
|
|
1580
|
+
}
|
|
1581
|
+
line();
|
|
1582
|
+
for (const repo of r.repos) {
|
|
1583
|
+
line(
|
|
1584
|
+
` ${repo.fullName}${repo.private ? c.yellow(" \xB7 private") : ""} ${c.dim(repo.defaultBranch ?? "")}`
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
line();
|
|
1588
|
+
return 0;
|
|
1589
|
+
}
|
|
1590
|
+
line(c.red(" usage: wholestack github installations | wholestack github repos --installation <id>"));
|
|
1591
|
+
return 1;
|
|
1592
|
+
}
|
|
1593
|
+
async function runAuditRepoSubcommand(rest) {
|
|
1594
|
+
const base2 = webBaseUrl();
|
|
1595
|
+
const body = {};
|
|
1596
|
+
if (rest.includes("--pick")) {
|
|
1597
|
+
const picked = await pickGithubRepoInteractive(base2);
|
|
1598
|
+
if (!picked.ok) {
|
|
1599
|
+
line(c.red(` ${picked.error}`));
|
|
1600
|
+
return 1;
|
|
1601
|
+
}
|
|
1602
|
+
body.githubRepo = picked.githubRepo;
|
|
1603
|
+
body.installationId = picked.installationId;
|
|
1604
|
+
}
|
|
1605
|
+
const github = flagValue2(rest, "--github");
|
|
1606
|
+
if (github) body.githubRepo = github;
|
|
1607
|
+
const installationRaw = flagValue2(rest, "--installation");
|
|
1608
|
+
if (installationRaw) {
|
|
1609
|
+
const n = Number(installationRaw);
|
|
1610
|
+
if (!Number.isFinite(n)) {
|
|
1611
|
+
line(c.red(" --installation must be a number"));
|
|
1612
|
+
return 1;
|
|
1613
|
+
}
|
|
1614
|
+
body.installationId = n;
|
|
1615
|
+
}
|
|
1616
|
+
const gitUrl = flagValue2(rest, "--git-url");
|
|
1617
|
+
if (gitUrl) body.gitUrl = gitUrl;
|
|
1618
|
+
const sourcePath = flagValue2(rest, "--source");
|
|
1619
|
+
if (sourcePath) {
|
|
1620
|
+
const src = readSourceFile(sourcePath);
|
|
1621
|
+
if (!src.ok) {
|
|
1622
|
+
line(c.red(` ${src.error}`));
|
|
1623
|
+
return 1;
|
|
1624
|
+
}
|
|
1625
|
+
body.source = src.source;
|
|
1626
|
+
}
|
|
1627
|
+
const maxRaw = flagValue2(rest, "--max-contracts");
|
|
1628
|
+
if (maxRaw) {
|
|
1629
|
+
const n = Number(maxRaw);
|
|
1630
|
+
if (!Number.isFinite(n)) {
|
|
1631
|
+
line(c.red(" --max-contracts must be a number"));
|
|
1632
|
+
return 1;
|
|
1633
|
+
}
|
|
1634
|
+
body.maxContracts = n;
|
|
1635
|
+
}
|
|
1636
|
+
if (!body.source && !body.gitUrl && !body.githubRepo) {
|
|
1637
|
+
line(c.red(" provide --github owner/repo, --git-url, --source <file>, or --pick"));
|
|
1638
|
+
return 1;
|
|
1639
|
+
}
|
|
1640
|
+
line();
|
|
1641
|
+
line(c.cyan(" \u259F ShipGate repo audit") + c.dim(" \xB7 streaming progress"));
|
|
1642
|
+
line();
|
|
1643
|
+
const r = await runRepoAuditStream(base2, body);
|
|
1644
|
+
line();
|
|
1645
|
+
if (!r.ok) {
|
|
1646
|
+
line(c.red(` \u2717 ${r.error}`));
|
|
1647
|
+
if (r.status === 402 && r.upgradeUrl) {
|
|
1648
|
+
line(c.dim(` upgrade: ${base2}${r.upgradeUrl}`));
|
|
1649
|
+
}
|
|
1650
|
+
return 1;
|
|
1651
|
+
}
|
|
1652
|
+
const verdict = String(r.result.verdict ?? r.result.shipVerdict ?? "unknown");
|
|
1653
|
+
line(verdict === "SHIP" || verdict === "ship" ? c.green(" \u2713 SHIP") : c.yellow(` verdict: ${verdict}`));
|
|
1654
|
+
if (r.result.certificateUrl) line(c.dim(` certificate: ${r.result.certificateUrl}`));
|
|
1655
|
+
if (r.result.bundleId) line(c.dim(` bundle: ${base2}/api/audit/bundle/${r.result.bundleId}`));
|
|
1656
|
+
return 0;
|
|
1657
|
+
}
|
|
1658
|
+
async function runProveSubcommand(raw) {
|
|
1659
|
+
if (raw[0] !== "prove") return null;
|
|
1660
|
+
const rest = raw.slice(1);
|
|
1661
|
+
if (rest.includes("--property")) return null;
|
|
1662
|
+
const wantsJson = rest.includes("--json");
|
|
1663
|
+
if (rest.includes("--remote")) {
|
|
1664
|
+
const base2 = webBaseUrl().replace(/\/$/, "");
|
|
1665
|
+
const key = process.env.ZETA_API_KEY?.trim();
|
|
1666
|
+
if (!key) {
|
|
1667
|
+
line(c.red(" not signed in \u2014 run `wholestack login` first (--remote needs a token)."));
|
|
1668
|
+
return 1;
|
|
1669
|
+
}
|
|
1670
|
+
const body = {};
|
|
1671
|
+
const github = flagValue2(rest, "--github");
|
|
1672
|
+
if (github) body.githubRepo = github;
|
|
1673
|
+
const gitUrl = flagValue2(rest, "--git-url");
|
|
1674
|
+
if (gitUrl) body.gitUrl = gitUrl;
|
|
1675
|
+
const installationRaw = flagValue2(rest, "--installation");
|
|
1676
|
+
if (installationRaw) {
|
|
1677
|
+
const n = Number(installationRaw);
|
|
1678
|
+
if (!Number.isFinite(n)) {
|
|
1679
|
+
line(c.red(" --installation must be a number"));
|
|
1680
|
+
return 1;
|
|
1681
|
+
}
|
|
1682
|
+
body.installationId = n;
|
|
1683
|
+
}
|
|
1684
|
+
const ref = flagValue2(rest, "--ref");
|
|
1685
|
+
if (ref) body.ref = ref;
|
|
1686
|
+
if (!body.githubRepo && !body.gitUrl) {
|
|
1687
|
+
line(c.red(" --remote needs --github owner/repo or --git-url <url>"));
|
|
1688
|
+
return 1;
|
|
1689
|
+
}
|
|
1690
|
+
let resp;
|
|
1691
|
+
try {
|
|
1692
|
+
resp = await fetch(`${base2}/api/v1/prove`, {
|
|
1693
|
+
method: "POST",
|
|
1694
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
1695
|
+
body: JSON.stringify(body)
|
|
1696
|
+
});
|
|
1697
|
+
} catch (e) {
|
|
1698
|
+
line(c.red(` cannot reach the platform: ${e.message}`));
|
|
1699
|
+
return 1;
|
|
1700
|
+
}
|
|
1701
|
+
const data = await resp.json().catch(() => null);
|
|
1702
|
+
if (wantsJson) {
|
|
1703
|
+
stdout3.write(JSON.stringify(data) + "\n");
|
|
1704
|
+
return data?.verdict === "SHIP" ? 0 : 1;
|
|
1705
|
+
}
|
|
1706
|
+
if (resp.status === 401 || resp.status === 402 || resp.status === 503 || !data) {
|
|
1707
|
+
line(c.red(` \u2717 ${data?.error ?? `platform returned ${resp.status}`}`));
|
|
1708
|
+
if (data?.upgradeUrl) line(c.dim(` upgrade: ${base2}${data.upgradeUrl}`));
|
|
1709
|
+
return 1;
|
|
1710
|
+
}
|
|
1711
|
+
printProveVerdict(data.verdict ?? "NO_SHIP", data.score ?? 0, data.checks ?? [], data.blockingReasons ?? []);
|
|
1712
|
+
return data.verdict === "SHIP" ? 0 : 1;
|
|
1713
|
+
}
|
|
1714
|
+
const path = rest.find((a) => !a.startsWith("-")) ?? cwd2();
|
|
1715
|
+
if (!existsSync2(path)) {
|
|
1716
|
+
line(c.red(` path not found: ${path}`));
|
|
1717
|
+
return 1;
|
|
1718
|
+
}
|
|
1719
|
+
const result = proveLocalRepo(path);
|
|
1720
|
+
if (wantsJson) {
|
|
1721
|
+
stdout3.write(JSON.stringify(result) + "\n");
|
|
1722
|
+
return result.verdict === "SHIP" ? 0 : 1;
|
|
1723
|
+
}
|
|
1724
|
+
line();
|
|
1725
|
+
line(c.cyan(" \u259F ISL prove-gate") + c.dim(" \xB7 deterministic \xB7 fail-closed \xB7 foreign default-deny"));
|
|
1726
|
+
line();
|
|
1727
|
+
printProveVerdict(result.verdict, result.score, result.checks, result.blockingReasons);
|
|
1728
|
+
return result.verdict === "SHIP" ? 0 : 1;
|
|
1729
|
+
}
|
|
1730
|
+
function printProveVerdict(verdict, score, checks, blockingReasons) {
|
|
1731
|
+
for (const ch of checks) {
|
|
1732
|
+
const icon = ch.status === "pass" ? c.green("\u2713") : ch.status === "fail" ? c.red("\u2717") : c.dim("\u2205");
|
|
1733
|
+
const label = ch.status === "not-run" ? c.dim(ch.title + " (not run)") : ch.title;
|
|
1734
|
+
line(` ${icon} ${label}`);
|
|
1735
|
+
line(c.dim(` ${ch.detail}`));
|
|
1736
|
+
}
|
|
1737
|
+
line();
|
|
1738
|
+
if (blockingReasons.length) {
|
|
1739
|
+
line(c.red(` ${blockingReasons.length} blocking reason(s):`));
|
|
1740
|
+
for (const r of blockingReasons.slice(0, 25)) line(c.dim(` - ${r}`));
|
|
1741
|
+
if (blockingReasons.length > 25) line(c.dim(` \u2026+${blockingReasons.length - 25} more`));
|
|
1742
|
+
line();
|
|
1743
|
+
}
|
|
1744
|
+
line(
|
|
1745
|
+
verdict === "SHIP" ? c.green(` \u2713 SHIP`) + c.dim(` \xB7 score ${score}/100`) : c.red(` \u2717 NO_SHIP`) + c.dim(` \xB7 score ${score}/100`)
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
918
1748
|
async function runSecuritySubcommand(raw) {
|
|
919
1749
|
const verb = raw[0];
|
|
920
1750
|
if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
|
|
921
1751
|
const rest = raw.slice(1);
|
|
1752
|
+
if (verb === "audit" && rest[0] === "repo") {
|
|
1753
|
+
return runAuditRepoSubcommand(rest.slice(1));
|
|
1754
|
+
}
|
|
922
1755
|
const proverArgs = verb === "verify" && rest[0]?.endsWith(".json") ? ["verify", ...rest] : [...rest];
|
|
923
1756
|
if (verb === "prove" && !proverArgs.includes("--property")) {
|
|
924
1757
|
line(c.red(" prove needs --property <kind>"));
|
|
@@ -928,7 +1761,7 @@ async function runSecuritySubcommand(raw) {
|
|
|
928
1761
|
line();
|
|
929
1762
|
line(c.cyan(` \u259F ShipGate ${verb}`) + c.dim(" \xB7 forge \xB7 slither \xB7 firewall \xB7 halmos"));
|
|
930
1763
|
line();
|
|
931
|
-
const r = await runProver(proverArgs, { cwd:
|
|
1764
|
+
const r = await runProver(proverArgs, { cwd: cwd2(), inherit: true });
|
|
932
1765
|
if (r.missing) {
|
|
933
1766
|
line(c.red(` ${r.out}`));
|
|
934
1767
|
return 1;
|
|
@@ -940,7 +1773,7 @@ async function runSecuritySubcommand(raw) {
|
|
|
940
1773
|
function maybeYoloNotice() {
|
|
941
1774
|
try {
|
|
942
1775
|
const dir = stateDir();
|
|
943
|
-
const marker =
|
|
1776
|
+
const marker = join6(dir, "yolo-notice-seen");
|
|
944
1777
|
if (existsSync2(marker)) return;
|
|
945
1778
|
line(
|
|
946
1779
|
" " + 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"
|
|
@@ -950,9 +1783,6 @@ function maybeYoloNotice() {
|
|
|
950
1783
|
} catch {
|
|
951
1784
|
}
|
|
952
1785
|
}
|
|
953
|
-
function webBaseUrl() {
|
|
954
|
-
return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
|
|
955
|
-
}
|
|
956
1786
|
async function runLogin(raw) {
|
|
957
1787
|
if (raw[0] !== "login" && raw[0] !== "logout") return null;
|
|
958
1788
|
const rest = raw.slice(1);
|
|
@@ -1004,7 +1834,7 @@ async function runLogin(raw) {
|
|
|
1004
1834
|
line(c.red(" usage: wholestack login --member <key> | --brain <key> | --build <key>"));
|
|
1005
1835
|
return 1;
|
|
1006
1836
|
}
|
|
1007
|
-
const rl = createInterface({ input: stdin, output:
|
|
1837
|
+
const rl = createInterface({ input: stdin, output: stdout3 });
|
|
1008
1838
|
if (!name) {
|
|
1009
1839
|
const which = (await rl.question(c.cyan(" key for [member/brain/engine]: "))).trim().toLowerCase();
|
|
1010
1840
|
name = which.startsWith("m") ? "ZETA_API_KEY" : "CEREBRAS_API_KEY";
|
|
@@ -1023,6 +1853,262 @@ async function runLogin(raw) {
|
|
|
1023
1853
|
line(c.dim(" now just run `wholestack`."));
|
|
1024
1854
|
return 0;
|
|
1025
1855
|
}
|
|
1856
|
+
async function runProjectSubcommand(raw) {
|
|
1857
|
+
const verb = raw[0];
|
|
1858
|
+
if (verb !== "projects" && verb !== "pull" && verb !== "push" && verb !== "open") return null;
|
|
1859
|
+
const base2 = webBaseUrl();
|
|
1860
|
+
if (verb === "projects") {
|
|
1861
|
+
const r2 = await listProjects(base2);
|
|
1862
|
+
if (!r2.ok) {
|
|
1863
|
+
line(c.red(` ${r2.error}`));
|
|
1864
|
+
return 1;
|
|
1865
|
+
}
|
|
1866
|
+
if (!r2.projects?.length) {
|
|
1867
|
+
line(c.dim(' no projects yet \u2014 build something (`wholestack "an idea"`) or create one in the web IDE.'));
|
|
1868
|
+
return 0;
|
|
1869
|
+
}
|
|
1870
|
+
line();
|
|
1871
|
+
for (const p of r2.projects) {
|
|
1872
|
+
const when = p.updatedAt ? new Date(p.updatedAt).toISOString().slice(0, 10) : "";
|
|
1873
|
+
line(" " + c.cyan(p.id) + " " + c.bold(p.name) + (when ? c.dim(` \xB7 ${when}`) : ""));
|
|
1874
|
+
if (p.description) line(" " + c.dim(` ${p.description}`));
|
|
1875
|
+
}
|
|
1876
|
+
line();
|
|
1877
|
+
line(c.dim(" pull one with ") + c.cyan("wholestack pull <id>") + c.dim(" \xB7 open in the IDE at ") + c.cyan(`${base2}/ide/studio?projectId=<id>`));
|
|
1878
|
+
return 0;
|
|
1879
|
+
}
|
|
1880
|
+
if (verb === "pull") {
|
|
1881
|
+
const projectId = raw[1];
|
|
1882
|
+
if (!projectId) {
|
|
1883
|
+
line(c.red(" usage: wholestack pull <projectId> [dir]"));
|
|
1884
|
+
return 1;
|
|
1885
|
+
}
|
|
1886
|
+
const r2 = await pullWorkingTree(base2, projectId);
|
|
1887
|
+
if (!r2.ok) {
|
|
1888
|
+
line(c.red(` ${r2.error}`));
|
|
1889
|
+
return 1;
|
|
1890
|
+
}
|
|
1891
|
+
const dest = raw[2] ?? (r2.name ? r2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") : projectId);
|
|
1892
|
+
const written = writeWorkingTree(dest, r2.files ?? []);
|
|
1893
|
+
writeProjectLink(dest, { projectId });
|
|
1894
|
+
line(c.green(` \u2713 pulled ${written} files`) + c.dim(` \u2192 ${dest}/`));
|
|
1895
|
+
line(c.dim(" edit locally, then ") + c.cyan(`wholestack push ${dest === "." ? "" : dest}`).trimEnd() + c.dim(" to sync back to the web IDE."));
|
|
1896
|
+
return 0;
|
|
1897
|
+
}
|
|
1898
|
+
if (verb === "push") {
|
|
1899
|
+
const dir = raw[1] ?? ".";
|
|
1900
|
+
const link = readProjectLink(dir);
|
|
1901
|
+
const flagIdx = raw.indexOf("--project");
|
|
1902
|
+
const projectId = (flagIdx >= 0 ? raw[flagIdx + 1] : void 0) ?? link?.projectId;
|
|
1903
|
+
if (!projectId) {
|
|
1904
|
+
line(c.red(" no linked project \u2014 pull first, or pass --project <id>."));
|
|
1905
|
+
return 1;
|
|
1906
|
+
}
|
|
1907
|
+
const { files, skipped } = await collectLocalFiles(dir);
|
|
1908
|
+
if (!files.length) {
|
|
1909
|
+
line(c.red(" nothing to push (no readable text files found)."));
|
|
1910
|
+
return 1;
|
|
1911
|
+
}
|
|
1912
|
+
const r2 = await pushWorkingTree(base2, projectId, files);
|
|
1913
|
+
if (!r2.ok) {
|
|
1914
|
+
line(c.red(` ${r2.error}`));
|
|
1915
|
+
return 1;
|
|
1916
|
+
}
|
|
1917
|
+
writeProjectLink(dir, { projectId });
|
|
1918
|
+
line(c.green(` \u2713 pushed ${files.length} files`) + c.dim(skipped ? ` (${skipped} skipped: binary/oversize)` : ""));
|
|
1919
|
+
line(c.dim(" open it: ") + c.cyan(`${base2}/ide/studio?projectId=${projectId}`));
|
|
1920
|
+
return 0;
|
|
1921
|
+
}
|
|
1922
|
+
const buildId = raw[1] ?? readProjectLink(".")?.buildId;
|
|
1923
|
+
if (!buildId) {
|
|
1924
|
+
line(c.red(" usage: wholestack open <buildId> (or run it inside a delivered app dir)"));
|
|
1925
|
+
return 1;
|
|
1926
|
+
}
|
|
1927
|
+
const r = await openBuildInEditor(base2, buildId);
|
|
1928
|
+
if (!r.ok) {
|
|
1929
|
+
line(c.red(` ${r.error}`));
|
|
1930
|
+
return 1;
|
|
1931
|
+
}
|
|
1932
|
+
if (r.projectId) writeProjectLink(".", { buildId, projectId: r.projectId });
|
|
1933
|
+
line(c.green(" \u2713 project ready in the web IDE"));
|
|
1934
|
+
line(c.dim(" ") + c.cyan(`${base2}${r.editorUrl ?? `/ide/studio?projectId=${r.projectId}`}`));
|
|
1935
|
+
return 0;
|
|
1936
|
+
}
|
|
1937
|
+
async function runShipSubcommand(raw) {
|
|
1938
|
+
const verb = raw[0];
|
|
1939
|
+
if (verb !== "deploy" && verb !== "promote" && verb !== "rollback") return null;
|
|
1940
|
+
const base2 = webBaseUrl();
|
|
1941
|
+
const rest = raw.slice(1);
|
|
1942
|
+
const buildId = (rest.find((a) => !a.startsWith("-")) ?? readProjectLink(".")?.buildId)?.trim();
|
|
1943
|
+
if (!buildId) {
|
|
1944
|
+
line(c.red(` usage: wholestack ${verb} <buildId> (or run it inside a delivered app dir)`));
|
|
1945
|
+
return 1;
|
|
1946
|
+
}
|
|
1947
|
+
if (verb === "deploy") {
|
|
1948
|
+
line();
|
|
1949
|
+
line(c.cyan(" \u259F deploy") + c.dim(" \xB7 live preview \xB7 charges one build credit (paid once per build)"));
|
|
1950
|
+
const r2 = await deployBuild(base2, buildId, {
|
|
1951
|
+
poll: true,
|
|
1952
|
+
onPhase: (s) => line(c.dim(` \xB7 ${s}`))
|
|
1953
|
+
});
|
|
1954
|
+
return printDeploy(r2, base2, "deployed");
|
|
1955
|
+
}
|
|
1956
|
+
if (verb === "promote") {
|
|
1957
|
+
const domain = flagValue2(rest, "--domain");
|
|
1958
|
+
line();
|
|
1959
|
+
line(c.cyan(" \u259F promote") + c.dim(" \xB7 durable production \xB7 real database, dev-auth OFF"));
|
|
1960
|
+
const r2 = await promoteBuild(base2, buildId, domain ? { domain } : {});
|
|
1961
|
+
return printDeploy(r2, base2, "promoted to production");
|
|
1962
|
+
}
|
|
1963
|
+
const deploymentId = flagValue2(rest, "--deployment") ?? flagValue2(rest, "--deployment-id");
|
|
1964
|
+
if (!deploymentId) {
|
|
1965
|
+
line(c.red(" usage: wholestack rollback [buildId] --deployment <currentDeploymentId>"));
|
|
1966
|
+
return 1;
|
|
1967
|
+
}
|
|
1968
|
+
line();
|
|
1969
|
+
line(c.cyan(" \u259F rollback") + c.dim(" \xB7 restore the previous production deployment"));
|
|
1970
|
+
const r = await rollbackDeployment(base2, buildId, deploymentId);
|
|
1971
|
+
return printDeploy(r, base2, "rolled back");
|
|
1972
|
+
}
|
|
1973
|
+
function printDeploy(r, base2, doneVerb) {
|
|
1974
|
+
line();
|
|
1975
|
+
if (!r.ok) {
|
|
1976
|
+
line(c.red(` \u2717 ${r.error}`));
|
|
1977
|
+
if (r.hint) line(c.dim(` ${r.hint}`));
|
|
1978
|
+
if (r.upgradeUrl) line(c.dim(` upgrade: ${base2.replace(/\/$/, "")}${r.upgradeUrl}`));
|
|
1979
|
+
if (r.connectUrl) line(c.dim(` connect: ${base2.replace(/\/$/, "")}${r.connectUrl}`));
|
|
1980
|
+
return 1;
|
|
1981
|
+
}
|
|
1982
|
+
line(c.green(` \u2713 ${doneVerb}`) + (r.status ? c.dim(` \xB7 ${r.status}`) : ""));
|
|
1983
|
+
if (r.url) line(c.dim(" ") + c.cyan(r.url.startsWith("http") ? r.url : `https://${r.url}`));
|
|
1984
|
+
if (r.deploymentId) line(c.dim(` deployment ${r.deploymentId}`));
|
|
1985
|
+
if (r.dbNote) line(c.dim(` ${r.dbNote}`));
|
|
1986
|
+
else if (r.dbProvisioned) line(c.dim(" database provisioned + schema applied"));
|
|
1987
|
+
return 0;
|
|
1988
|
+
}
|
|
1989
|
+
async function runLaunchSubcommand(raw) {
|
|
1990
|
+
const verb = raw[0];
|
|
1991
|
+
if (verb !== "launch" && verb !== "launch-kit") return null;
|
|
1992
|
+
const base2 = webBaseUrl();
|
|
1993
|
+
const rest = raw.slice(1);
|
|
1994
|
+
const wantsJson = rest.includes("--json");
|
|
1995
|
+
const argBuildId = rest.find((a) => !a.startsWith("-"));
|
|
1996
|
+
const buildId = (argBuildId ?? readProjectLink(".")?.buildId)?.trim();
|
|
1997
|
+
if (verb === "launch") {
|
|
1998
|
+
const prompt = flagValue2(rest, "--prompt");
|
|
1999
|
+
line();
|
|
2000
|
+
line(c.cyan(" \u259F launch canvas") + c.dim(" \xB7 drawing your go-to-market plan\u2026"));
|
|
2001
|
+
const r2 = await drawCanvas(base2, { buildId, prompt });
|
|
2002
|
+
if (!r2.ok || !r2.plan) {
|
|
2003
|
+
line(c.red(` \u2717 ${r2.error ?? "no plan returned"}`));
|
|
2004
|
+
return 1;
|
|
2005
|
+
}
|
|
2006
|
+
if (wantsJson) {
|
|
2007
|
+
stdout3.write(JSON.stringify(r2.plan, null, 2) + "\n");
|
|
2008
|
+
return 0;
|
|
2009
|
+
}
|
|
2010
|
+
for (const ln of renderGtmPlanLines(r2.plan)) line(ln);
|
|
2011
|
+
return 0;
|
|
2012
|
+
}
|
|
2013
|
+
if (!buildId) {
|
|
2014
|
+
line(c.red(" usage: wholestack launch-kit <buildId> (or run it inside a delivered app dir)"));
|
|
2015
|
+
return 1;
|
|
2016
|
+
}
|
|
2017
|
+
const r = await getLaunchKit(base2, buildId);
|
|
2018
|
+
if (!r.ok || !r.kit) {
|
|
2019
|
+
line(c.red(` \u2717 ${r.error ?? "no launch kit"}`));
|
|
2020
|
+
return 1;
|
|
2021
|
+
}
|
|
2022
|
+
if (wantsJson) {
|
|
2023
|
+
stdout3.write(JSON.stringify(r.kit, null, 2) + "\n");
|
|
2024
|
+
return 0;
|
|
2025
|
+
}
|
|
2026
|
+
for (const ln of renderLaunchKitLines(r.kit)) line(ln);
|
|
2027
|
+
return 0;
|
|
2028
|
+
}
|
|
2029
|
+
async function runHqSubcommand(raw) {
|
|
2030
|
+
const verb = raw[0];
|
|
2031
|
+
if (!["legal", "metrics", "waitlist", "businesses", "business"].includes(verb)) return null;
|
|
2032
|
+
const base2 = webBaseUrl();
|
|
2033
|
+
const rest = raw.slice(1);
|
|
2034
|
+
const wantsJson = rest.includes("--json");
|
|
2035
|
+
const firstArg = rest.find((a) => !a.startsWith("-"));
|
|
2036
|
+
if (verb === "legal") {
|
|
2037
|
+
const kinds = ["terms", "privacy", "cookie", "refund"];
|
|
2038
|
+
const kind = firstArg;
|
|
2039
|
+
if (!kind || !kinds.includes(kind)) {
|
|
2040
|
+
line(c.red(" usage: wholestack legal <terms|privacy|cookie|refund> [--name <business>]"));
|
|
2041
|
+
return 1;
|
|
2042
|
+
}
|
|
2043
|
+
const r2 = await generateLegal(base2, { kind, businessName: flagValue2(rest, "--name") });
|
|
2044
|
+
if (!r2.ok || !r2.markdown) {
|
|
2045
|
+
line(c.red(` \u2717 ${r2.error ?? "no document produced"}`));
|
|
2046
|
+
return 1;
|
|
2047
|
+
}
|
|
2048
|
+
stdout3.write(r2.markdown.trimEnd() + "\n");
|
|
2049
|
+
return 0;
|
|
2050
|
+
}
|
|
2051
|
+
if (verb === "metrics") {
|
|
2052
|
+
const r2 = await readMetrics(base2);
|
|
2053
|
+
if (!r2.ok || !r2.metrics) {
|
|
2054
|
+
line(c.red(` \u2717 ${r2.error ?? "could not read metrics"}`));
|
|
2055
|
+
return 1;
|
|
2056
|
+
}
|
|
2057
|
+
if (wantsJson) {
|
|
2058
|
+
stdout3.write(JSON.stringify(r2.metrics, null, 2) + "\n");
|
|
2059
|
+
return 0;
|
|
2060
|
+
}
|
|
2061
|
+
for (const ln of renderMetricsLines(r2.metrics)) line(ln);
|
|
2062
|
+
return 0;
|
|
2063
|
+
}
|
|
2064
|
+
if (verb === "waitlist") {
|
|
2065
|
+
const buildId2 = (firstArg ?? readProjectLink(".")?.buildId)?.trim();
|
|
2066
|
+
if (!buildId2) {
|
|
2067
|
+
line(c.red(" usage: wholestack waitlist <buildId> (or run it inside a delivered app dir)"));
|
|
2068
|
+
return 1;
|
|
2069
|
+
}
|
|
2070
|
+
const r2 = await waitlistCount(base2, buildId2);
|
|
2071
|
+
if (!r2.ok) {
|
|
2072
|
+
line(c.red(` \u2717 ${r2.error}`));
|
|
2073
|
+
return 1;
|
|
2074
|
+
}
|
|
2075
|
+
line();
|
|
2076
|
+
line(" " + c.bold(String(r2.count ?? 0)) + c.dim(" waitlist signup(s)"));
|
|
2077
|
+
return 0;
|
|
2078
|
+
}
|
|
2079
|
+
if (verb === "businesses") {
|
|
2080
|
+
const r2 = await listBusinesses(base2);
|
|
2081
|
+
if (!r2.ok) {
|
|
2082
|
+
line(c.red(` \u2717 ${r2.error}`));
|
|
2083
|
+
return 1;
|
|
2084
|
+
}
|
|
2085
|
+
if (wantsJson) {
|
|
2086
|
+
stdout3.write(JSON.stringify(r2.businesses ?? [], null, 2) + "\n");
|
|
2087
|
+
return 0;
|
|
2088
|
+
}
|
|
2089
|
+
const rows = r2.businesses ?? [];
|
|
2090
|
+
if (!rows.length) {
|
|
2091
|
+
line(c.dim(" no businesses yet \u2014 draw a launch plan (`wholestack launch`) or build an app."));
|
|
2092
|
+
return 0;
|
|
2093
|
+
}
|
|
2094
|
+
line();
|
|
2095
|
+
for (const b of rows) {
|
|
2096
|
+
const id = String(b.id ?? "");
|
|
2097
|
+
const name = typeof b.name === "string" ? b.name : typeof b.brandName === "string" ? b.brandName : "";
|
|
2098
|
+
line(" " + c.cyan(id) + (name ? " " + c.bold(name) : ""));
|
|
2099
|
+
}
|
|
2100
|
+
line();
|
|
2101
|
+
return 0;
|
|
2102
|
+
}
|
|
2103
|
+
const buildId = (firstArg ?? readProjectLink(".")?.buildId)?.trim();
|
|
2104
|
+
const r = await getBusiness(base2, buildId ? { buildId } : {});
|
|
2105
|
+
if (!r.ok) {
|
|
2106
|
+
line(c.red(` \u2717 ${r.error}`));
|
|
2107
|
+
return 1;
|
|
2108
|
+
}
|
|
2109
|
+
stdout3.write(JSON.stringify({ business: r.business, readiness: r.readiness }, null, 2) + "\n");
|
|
2110
|
+
return 0;
|
|
2111
|
+
}
|
|
1026
2112
|
var EMPTY_PLUGINS = {
|
|
1027
2113
|
plugins: [],
|
|
1028
2114
|
systemText: "",
|
|
@@ -1049,8 +2135,26 @@ async function main() {
|
|
|
1049
2135
|
const rawArgs = argv.slice(2);
|
|
1050
2136
|
const loggedIn = await runLogin(rawArgs);
|
|
1051
2137
|
if (loggedIn !== null) exit(loggedIn);
|
|
2138
|
+
const gh = await runGithubSubcommand(rawArgs);
|
|
2139
|
+
if (gh !== null) exit(gh);
|
|
2140
|
+
const provePath = await runProveSubcommand(rawArgs);
|
|
2141
|
+
if (provePath !== null) exit(provePath);
|
|
1052
2142
|
const sub = await runSecuritySubcommand(rawArgs);
|
|
1053
2143
|
if (sub !== null) exit(sub);
|
|
2144
|
+
const proj = await runProjectSubcommand(rawArgs);
|
|
2145
|
+
if (proj !== null) exit(proj);
|
|
2146
|
+
const ship = await runShipSubcommand(rawArgs);
|
|
2147
|
+
if (ship !== null) exit(ship);
|
|
2148
|
+
const launch = await runLaunchSubcommand(rawArgs);
|
|
2149
|
+
if (launch !== null) exit(launch);
|
|
2150
|
+
const hq = await runHqSubcommand(rawArgs);
|
|
2151
|
+
if (hq !== null) exit(hq);
|
|
2152
|
+
const eyes = await runEyesSubcommand(rawArgs);
|
|
2153
|
+
if (eyes !== null) exit(eyes);
|
|
2154
|
+
const ops = await runOpsSubcommand(rawArgs);
|
|
2155
|
+
if (ops !== null) exit(ops);
|
|
2156
|
+
const skillsSub = await runSkillsSubcommand(rawArgs);
|
|
2157
|
+
if (skillsSub !== null) exit(skillsSub);
|
|
1054
2158
|
const args = parse(rawArgs);
|
|
1055
2159
|
if (args.version) {
|
|
1056
2160
|
line(`wholestack ${VERSION}`);
|
|
@@ -1064,9 +2168,9 @@ async function main() {
|
|
|
1064
2168
|
let token = process.env.ZETA_API_KEY;
|
|
1065
2169
|
let access = await verifyAccess(webBaseUrl(), token);
|
|
1066
2170
|
if (!access.active && stdin.isTTY) {
|
|
1067
|
-
const rl = createInterface({ input: stdin, output:
|
|
2171
|
+
const rl = createInterface({ input: stdin, output: stdout3 });
|
|
1068
2172
|
const ans = (await rl.question(
|
|
1069
|
-
"\n " + (token ? c.dim("
|
|
2173
|
+
"\n " + (token ? c.dim("Token invalid or expired. ") + "Log in again? " : "Log in now? ") + c.dim("[Y/n] ")
|
|
1070
2174
|
)).trim().toLowerCase();
|
|
1071
2175
|
rl.close();
|
|
1072
2176
|
if (ans === "" || ans === "y" || ans === "yes") {
|
|
@@ -1082,6 +2186,16 @@ async function main() {
|
|
|
1082
2186
|
showPaywall(Boolean(token), webBaseUrl());
|
|
1083
2187
|
exit(1);
|
|
1084
2188
|
}
|
|
2189
|
+
if (!access.paid && access.freeDeliveries) {
|
|
2190
|
+
const fd = access.freeDeliveries;
|
|
2191
|
+
line(
|
|
2192
|
+
c.dim(
|
|
2193
|
+
` free plan \xB7 ${fd.remaining}/${fd.limit} delivered apps left this month \xB7 `
|
|
2194
|
+
) + c.cyan(`${webBaseUrl().replace(/\/$/, "")}/pricing`)
|
|
2195
|
+
);
|
|
2196
|
+
} else if (access.paid && access.tier) {
|
|
2197
|
+
line(c.dim(` ${access.tier} plan`));
|
|
2198
|
+
}
|
|
1085
2199
|
}
|
|
1086
2200
|
let modelKey;
|
|
1087
2201
|
try {
|
|
@@ -1097,9 +2211,19 @@ async function main() {
|
|
|
1097
2211
|
line(c.red(e.message));
|
|
1098
2212
|
exit(1);
|
|
1099
2213
|
}
|
|
2214
|
+
if (args.model) {
|
|
2215
|
+
escalationBus.lock();
|
|
2216
|
+
} else {
|
|
2217
|
+
const firstPrompt = args.prompt.length > 0 ? args.prompt : void 0;
|
|
2218
|
+
const boot = pickBootModel(firstPrompt);
|
|
2219
|
+
if (boot && boot.key !== modelKey) {
|
|
2220
|
+
modelKey = boot.key;
|
|
2221
|
+
model = resolveModel(modelKey);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
1100
2224
|
const isTty = !!stdin.isTTY;
|
|
1101
2225
|
const oneShot = args.prompt.length > 0;
|
|
1102
|
-
const workdir =
|
|
2226
|
+
const workdir = cwd2();
|
|
1103
2227
|
const persistedMode = isPermissionMode(
|
|
1104
2228
|
process.env.ZETA_PERMISSION_MODE
|
|
1105
2229
|
) ? process.env.ZETA_PERMISSION_MODE : void 0;
|
|
@@ -1107,7 +2231,9 @@ async function main() {
|
|
|
1107
2231
|
const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
|
|
1108
2232
|
const memory = await loadProjectMemory(workdir);
|
|
1109
2233
|
const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
|
|
1110
|
-
const
|
|
2234
|
+
const skills = SkillRegistry.load(workdir);
|
|
2235
|
+
const learned = loadLearnedMemory({ scope: basename2(workdir) });
|
|
2236
|
+
const memoryText = [memory.text, learned.text, plugins.systemText, skills.promptHint()].filter(Boolean).join("\n\n") || void 0;
|
|
1111
2237
|
const mcp = await loadMcpTools({
|
|
1112
2238
|
cwd: workdir,
|
|
1113
2239
|
only: args.mcp,
|
|
@@ -1139,7 +2265,7 @@ async function main() {
|
|
|
1139
2265
|
},
|
|
1140
2266
|
// Starship-style context segments above the prompt box.
|
|
1141
2267
|
contextBar: () => {
|
|
1142
|
-
const seg = [c.cyan(modelLabel(modelKey)), c.dim(
|
|
2268
|
+
const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename2(workdir))];
|
|
1143
2269
|
const br = gitBranch(workdir);
|
|
1144
2270
|
if (br) seg.push(c.dim("\u2387 " + br));
|
|
1145
2271
|
seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
|
|
@@ -1161,7 +2287,21 @@ async function main() {
|
|
|
1161
2287
|
...buildWebTools(),
|
|
1162
2288
|
...mcp.tools,
|
|
1163
2289
|
// Parallel read-only sub-agents (needs the model handle the base tools lack).
|
|
1164
|
-
...buildSubagentTool({ model, modelKey, ctx: toolCtx })
|
|
2290
|
+
...buildSubagentTool({ model, modelKey, ctx: toolCtx }),
|
|
2291
|
+
// Parallel WRITABLE workers in isolated git worktrees (review-then-merge diffs).
|
|
2292
|
+
...buildWorkersTool({ model, modelKey, ctx: toolCtx }),
|
|
2293
|
+
// Self-verify + heal: run the web2 ShipGate (prove_app) + the app's own checks (verify_build).
|
|
2294
|
+
...buildVerifyTools({ ctx: toolCtx }),
|
|
2295
|
+
// Eyes — open a running app in a real browser and report what it sees.
|
|
2296
|
+
...buildEyesTool({ ctx: toolCtx }),
|
|
2297
|
+
// Living ops — operate the app after ship (health/deploy/metrics/errors).
|
|
2298
|
+
...buildOpsTools({ ctx: toolCtx }),
|
|
2299
|
+
// Cross-session learned memory (remember / recall).
|
|
2300
|
+
...buildLearnedMemoryTools({ ctx: toolCtx }),
|
|
2301
|
+
// Composable skills (list_skills / use_skill) from the boot-time registry.
|
|
2302
|
+
...buildSkillTools({ registry: skills }),
|
|
2303
|
+
// Auto model-routing: the escalate_model tool posts requests the loop drains.
|
|
2304
|
+
...buildEscalateTool({ ctx: toolCtx, currentKey: modelKey, bus: escalationBus })
|
|
1165
2305
|
};
|
|
1166
2306
|
let session;
|
|
1167
2307
|
let resumedMessages = null;
|
|
@@ -1189,7 +2329,7 @@ async function main() {
|
|
|
1189
2329
|
persona: args.persona,
|
|
1190
2330
|
thinking,
|
|
1191
2331
|
yolo: mode === "yolo",
|
|
1192
|
-
maxSteps:
|
|
2332
|
+
maxSteps: 50,
|
|
1193
2333
|
contextWindow: modelContextWindow(modelKey),
|
|
1194
2334
|
session
|
|
1195
2335
|
});
|
|
@@ -1240,6 +2380,19 @@ ${m.context}` : args.prompt;
|
|
|
1240
2380
|
const runTurn = async (text, images) => {
|
|
1241
2381
|
const t0 = Date.now();
|
|
1242
2382
|
await ic.runInterruptible((sig) => agent.send(text, sig, images));
|
|
2383
|
+
const pending = escalationBus.drain();
|
|
2384
|
+
if (pending && pending.key !== modelKey) {
|
|
2385
|
+
try {
|
|
2386
|
+
modelKey = pending.key;
|
|
2387
|
+
agent.setModel(resolveModel(modelKey), modelKey, modelContextWindow(modelKey));
|
|
2388
|
+
agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
|
|
2389
|
+
line(
|
|
2390
|
+
" " + c.green(`\u2192 ${modelLabel(modelKey)}`) + c.dim(` \xB7 auto-route: ${pending.reason}`) + "\n"
|
|
2391
|
+
);
|
|
2392
|
+
} catch (e) {
|
|
2393
|
+
line(" " + c.red(e.message));
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
1243
2396
|
statusLine({
|
|
1244
2397
|
model: modelLabel(modelKey),
|
|
1245
2398
|
tokens: agent.usage.outputTokens,
|
|
@@ -1276,6 +2429,7 @@ ${m.context}` : args.prompt;
|
|
|
1276
2429
|
const m = resolveModel(res.modelKey);
|
|
1277
2430
|
modelKey = res.modelKey;
|
|
1278
2431
|
agent.setModel(m, modelKey, modelContextWindow(modelKey));
|
|
2432
|
+
escalationBus.lock();
|
|
1279
2433
|
agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
|
|
1280
2434
|
line(
|
|
1281
2435
|
" " + c.green(`\u2192 ${modelLabel(modelKey)}`) + (agent.thinkingOn ? c.dim(" \xB7 thinking on") : "") + "\n"
|