wholestack 0.5.6 → 0.5.8
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 +23 -0
- package/dist/chunk-JXARRLAW.js +9563 -0
- package/dist/cli.js +1147 -60
- package/dist/index.d.ts +619 -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-K6MSDQJT.js +0 -5602
package/dist/cli.js
CHANGED
|
@@ -7,17 +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,
|
|
15
22
|
collectLocalFiles,
|
|
23
|
+
deployBuild,
|
|
24
|
+
drawCanvas,
|
|
25
|
+
escalationBus,
|
|
26
|
+
fetchGithubInstallations,
|
|
27
|
+
fetchGithubRepos,
|
|
28
|
+
generateLegal,
|
|
29
|
+
getBusiness,
|
|
30
|
+
getLaunchKit,
|
|
31
|
+
inspectUrl,
|
|
16
32
|
isPermissionMode,
|
|
17
33
|
killRunningApps,
|
|
18
34
|
line,
|
|
35
|
+
listBusinesses,
|
|
19
36
|
listProjects,
|
|
20
37
|
loadHookFiles,
|
|
38
|
+
loadLearnedMemory,
|
|
21
39
|
loadMcpTools,
|
|
22
40
|
loadPlugins,
|
|
23
41
|
loadProjectMemory,
|
|
@@ -26,26 +44,42 @@ import {
|
|
|
26
44
|
modelContextWindow,
|
|
27
45
|
modelLabel,
|
|
28
46
|
openBuildInEditor,
|
|
47
|
+
pickBootModel,
|
|
48
|
+
pickGithubRepoInteractive,
|
|
49
|
+
promoteBuild,
|
|
50
|
+
proveLocalRepo,
|
|
29
51
|
pullWorkingTree,
|
|
30
52
|
pushWorkingTree,
|
|
53
|
+
readMetrics,
|
|
31
54
|
readProjectLink,
|
|
55
|
+
readSourceFile,
|
|
56
|
+
rememberLesson,
|
|
57
|
+
renderGtmPlanLines,
|
|
58
|
+
renderLaunchKitLines,
|
|
59
|
+
renderMetricsLines,
|
|
32
60
|
resolveModel,
|
|
33
61
|
resolveModelKey,
|
|
62
|
+
rollbackDeployment,
|
|
34
63
|
runProver,
|
|
64
|
+
runRepoAuditStream,
|
|
35
65
|
saveKey,
|
|
66
|
+
searchLessons,
|
|
36
67
|
stateDir,
|
|
37
68
|
statusLine,
|
|
38
69
|
supportsThinking,
|
|
39
70
|
tasks,
|
|
71
|
+
toolLine,
|
|
40
72
|
userBox,
|
|
41
73
|
visionCapable,
|
|
74
|
+
waitlistCount,
|
|
75
|
+
webBaseUrl,
|
|
42
76
|
writeProjectLink,
|
|
43
77
|
writeWorkingTree
|
|
44
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-JXARRLAW.js";
|
|
45
79
|
|
|
46
80
|
// src/cli.ts
|
|
47
81
|
import { createInterface } from "readline/promises";
|
|
48
|
-
import { stdin, stdout, argv, exit, cwd } from "process";
|
|
82
|
+
import { stdin, stdout as stdout3, argv, exit, cwd as cwd2 } from "process";
|
|
49
83
|
|
|
50
84
|
// src/cli-login.ts
|
|
51
85
|
import { createServer } from "http";
|
|
@@ -298,7 +332,7 @@ async function renderTree(absDir, relDir) {
|
|
|
298
332
|
return `${relDir || "."}/ (directory tree)
|
|
299
333
|
${lines.join("\n")}`;
|
|
300
334
|
}
|
|
301
|
-
async function expandMentions(message,
|
|
335
|
+
async function expandMentions(message, cwd3, tokenBudget = DEFAULT_TOKEN_BUDGET) {
|
|
302
336
|
const mentions = extractMentions(message);
|
|
303
337
|
if (mentions.length === 0) return { context: "", notice: null };
|
|
304
338
|
const blocks = [];
|
|
@@ -308,7 +342,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
308
342
|
for (const mention of mentions) {
|
|
309
343
|
if (spent >= tokenBudget) break;
|
|
310
344
|
const raw = mention.startsWith("~/") ? join(process.env.HOME ?? "", mention.slice(2)) : mention;
|
|
311
|
-
const direct = isAbsolute(raw) ? raw : resolve(
|
|
345
|
+
const direct = isAbsolute(raw) ? raw : resolve(cwd3, raw);
|
|
312
346
|
let isDir = false;
|
|
313
347
|
try {
|
|
314
348
|
isDir = (await stat(direct)).isDirectory();
|
|
@@ -316,7 +350,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
316
350
|
isDir = false;
|
|
317
351
|
}
|
|
318
352
|
if (isDir) {
|
|
319
|
-
const rel = relative(
|
|
353
|
+
const rel = relative(cwd3, direct);
|
|
320
354
|
const tree = await renderTree(direct, rel);
|
|
321
355
|
const cost = estTokens(tree);
|
|
322
356
|
if (spent + cost <= tokenBudget) {
|
|
@@ -329,7 +363,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
329
363
|
let files = [];
|
|
330
364
|
try {
|
|
331
365
|
files = await fg(raw, {
|
|
332
|
-
cwd:
|
|
366
|
+
cwd: cwd3,
|
|
333
367
|
ignore: IGNORE,
|
|
334
368
|
onlyFiles: true,
|
|
335
369
|
dot: true,
|
|
@@ -341,7 +375,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
341
375
|
}
|
|
342
376
|
if (files.length === 0) {
|
|
343
377
|
try {
|
|
344
|
-
if ((await stat(direct)).isFile()) files = [relative(
|
|
378
|
+
if ((await stat(direct)).isFile()) files = [relative(cwd3, direct)];
|
|
345
379
|
} catch {
|
|
346
380
|
}
|
|
347
381
|
}
|
|
@@ -352,7 +386,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
|
|
|
352
386
|
seen.add(rel);
|
|
353
387
|
let buf;
|
|
354
388
|
try {
|
|
355
|
-
buf = await readFile2(resolve(
|
|
389
|
+
buf = await readFile2(resolve(cwd3, rel), "utf8");
|
|
356
390
|
} catch {
|
|
357
391
|
continue;
|
|
358
392
|
}
|
|
@@ -432,11 +466,528 @@ function buildSubagentTool(deps) {
|
|
|
432
466
|
};
|
|
433
467
|
}
|
|
434
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
|
+
|
|
435
986
|
// src/images.ts
|
|
436
987
|
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
437
|
-
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";
|
|
438
989
|
import { execFile } from "child_process";
|
|
439
|
-
import { tmpdir, platform as platform2 } from "os";
|
|
990
|
+
import { tmpdir as tmpdir2, platform as platform2 } from "os";
|
|
440
991
|
var EXT_TO_MEDIA = {
|
|
441
992
|
".png": "image/png",
|
|
442
993
|
".jpg": "image/jpeg",
|
|
@@ -455,7 +1006,7 @@ function run(cmd, args) {
|
|
|
455
1006
|
});
|
|
456
1007
|
}
|
|
457
1008
|
async function grabClipboardImage() {
|
|
458
|
-
const out =
|
|
1009
|
+
const out = join3(tmpdir2(), `zeta-clip-${process.pid}-${process.hrtime.bigint()}.png`);
|
|
459
1010
|
const os = platform2();
|
|
460
1011
|
if (os === "darwin") {
|
|
461
1012
|
const script = `set thePath to "${out}"
|
|
@@ -489,7 +1040,7 @@ function imageTokens(message) {
|
|
|
489
1040
|
for (let m = re.exec(message); m; m = re.exec(message)) out.add(m[1]);
|
|
490
1041
|
return [...out];
|
|
491
1042
|
}
|
|
492
|
-
async function scanImages(message,
|
|
1043
|
+
async function scanImages(message, cwd3) {
|
|
493
1044
|
const tokens = imageTokens(message);
|
|
494
1045
|
const images = [];
|
|
495
1046
|
const skipped = [];
|
|
@@ -518,8 +1069,8 @@ async function scanImages(message, cwd2) {
|
|
|
518
1069
|
}
|
|
519
1070
|
}
|
|
520
1071
|
for (const tok of tokens) {
|
|
521
|
-
const raw = tok.startsWith("~/") ?
|
|
522
|
-
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);
|
|
523
1074
|
const mediaType = EXT_TO_MEDIA[extname(abs).toLowerCase()];
|
|
524
1075
|
if (!mediaType) continue;
|
|
525
1076
|
let bytes;
|
|
@@ -530,7 +1081,7 @@ async function scanImages(message, cwd2) {
|
|
|
530
1081
|
} catch {
|
|
531
1082
|
continue;
|
|
532
1083
|
}
|
|
533
|
-
const label = isAbsolute2(raw) ? raw : relative2(
|
|
1084
|
+
const label = isAbsolute2(raw) ? raw : relative2(cwd3, abs);
|
|
534
1085
|
if (bytes > PER_IMAGE_BYTES || total + bytes > TOTAL_BYTES) {
|
|
535
1086
|
skipped.push(`${label} (${Math.round(bytes / 1024)}KB \u2014 over the size cap)`);
|
|
536
1087
|
continue;
|
|
@@ -664,21 +1215,21 @@ ${text}`;
|
|
|
664
1215
|
${body}
|
|
665
1216
|
`;
|
|
666
1217
|
}
|
|
667
|
-
function writeTranscript(messages, meta, file,
|
|
668
|
-
const abs = resolve3(
|
|
1218
|
+
function writeTranscript(messages, meta, file, cwd3) {
|
|
1219
|
+
const abs = resolve3(cwd3, file);
|
|
669
1220
|
writeFileSync(abs, renderTranscript(messages, meta), "utf8");
|
|
670
1221
|
return abs;
|
|
671
1222
|
}
|
|
672
1223
|
|
|
673
1224
|
// src/cli.ts
|
|
674
1225
|
import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
675
|
-
import { join as
|
|
1226
|
+
import { join as join6 } from "path";
|
|
676
1227
|
|
|
677
1228
|
// src/update-check.ts
|
|
678
1229
|
import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
679
|
-
import { join as
|
|
1230
|
+
import { join as join4 } from "path";
|
|
680
1231
|
var PKG = "wholestack";
|
|
681
|
-
var CACHE =
|
|
1232
|
+
var CACHE = join4(stateDir(), "update-check.json");
|
|
682
1233
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
683
1234
|
function readCache() {
|
|
684
1235
|
try {
|
|
@@ -732,8 +1283,8 @@ async function checkForUpdate(current) {
|
|
|
732
1283
|
|
|
733
1284
|
// src/access.ts
|
|
734
1285
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
735
|
-
import { join as
|
|
736
|
-
var CACHE2 =
|
|
1286
|
+
import { join as join5 } from "path";
|
|
1287
|
+
var CACHE2 = join5(stateDir(), "access.json");
|
|
737
1288
|
var TTL_MS = 60 * 60 * 1e3;
|
|
738
1289
|
function readCache2() {
|
|
739
1290
|
try {
|
|
@@ -756,10 +1307,18 @@ function clearAccessCache() {
|
|
|
756
1307
|
}
|
|
757
1308
|
}
|
|
758
1309
|
async function verifyAccess(webUrl, token, force = false) {
|
|
759
|
-
if (!token)
|
|
1310
|
+
if (!token) {
|
|
1311
|
+
return { active: false, paid: false, tier: null, freeDeliveries: null, degraded: false };
|
|
1312
|
+
}
|
|
760
1313
|
const cached = force ? null : readCache2();
|
|
761
1314
|
if (cached && Date.now() - cached.checkedAt < TTL_MS) {
|
|
762
|
-
return {
|
|
1315
|
+
return {
|
|
1316
|
+
active: cached.active,
|
|
1317
|
+
paid: cached.paid,
|
|
1318
|
+
tier: cached.tier,
|
|
1319
|
+
freeDeliveries: cached.freeDeliveries,
|
|
1320
|
+
degraded: false
|
|
1321
|
+
};
|
|
763
1322
|
}
|
|
764
1323
|
try {
|
|
765
1324
|
const ctrl = new AbortController();
|
|
@@ -770,18 +1329,44 @@ async function verifyAccess(webUrl, token, force = false) {
|
|
|
770
1329
|
});
|
|
771
1330
|
clearTimeout(t);
|
|
772
1331
|
if (res.status >= 500) {
|
|
773
|
-
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
|
+
};
|
|
774
1339
|
}
|
|
775
1340
|
if (!res.ok) {
|
|
776
|
-
writeCache2({
|
|
777
|
-
|
|
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 };
|
|
778
1349
|
}
|
|
779
1350
|
const json = await res.json();
|
|
780
1351
|
const active = Boolean(json.active);
|
|
781
|
-
|
|
782
|
-
|
|
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 };
|
|
783
1362
|
} catch {
|
|
784
|
-
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
|
+
};
|
|
785
1370
|
}
|
|
786
1371
|
}
|
|
787
1372
|
function showPaywall(loggedIn, webUrl) {
|
|
@@ -789,36 +1374,38 @@ function showPaywall(loggedIn, webUrl) {
|
|
|
789
1374
|
line();
|
|
790
1375
|
line(" " + c.cyan("\u256D" + "\u2500".repeat(w) + "\u256E"));
|
|
791
1376
|
const row = (s) => line(" " + c.cyan("\u2502") + " " + s.padEnd(w - 2) + " " + c.cyan("\u2502"));
|
|
792
|
-
row(c.bold("
|
|
1377
|
+
row(c.bold("wholestack needs you signed in"));
|
|
793
1378
|
row("");
|
|
794
1379
|
if (!loggedIn) {
|
|
795
1380
|
row(c.dim("You're not signed in."));
|
|
796
|
-
row("Run " + c.cyan("
|
|
1381
|
+
row("Run " + c.cyan("wholestack login") + c.dim(" to start."));
|
|
797
1382
|
} else {
|
|
798
|
-
row(c.dim("
|
|
799
|
-
row("
|
|
1383
|
+
row(c.dim("Your token was rejected or expired."));
|
|
1384
|
+
row("Run " + c.cyan("wholestack login") + c.dim(" again."));
|
|
800
1385
|
}
|
|
801
1386
|
row("");
|
|
802
|
-
row(c.dim("
|
|
1387
|
+
row(c.dim("Free: make \xB7 edit \xB7 preview (watermarked)"));
|
|
1388
|
+
row(c.dim("Pro $29/mo: ship unlimited \u2014 own the code"));
|
|
1389
|
+
row(c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
|
|
803
1390
|
line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
|
|
804
1391
|
line();
|
|
805
1392
|
}
|
|
806
1393
|
|
|
807
1394
|
// src/cli.ts
|
|
808
1395
|
import { readFileSync as readFileSync3 } from "fs";
|
|
809
|
-
import { basename, dirname as dirname2 } from "path";
|
|
1396
|
+
import { basename as basename2, dirname as dirname2 } from "path";
|
|
810
1397
|
import { fileURLToPath } from "url";
|
|
811
1398
|
var VERSION = (() => {
|
|
812
1399
|
try {
|
|
813
1400
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
814
|
-
return JSON.parse(readFileSync3(
|
|
1401
|
+
return JSON.parse(readFileSync3(join6(here, "..", "package.json"), "utf8")).version;
|
|
815
1402
|
} catch {
|
|
816
1403
|
return "0.0.0";
|
|
817
1404
|
}
|
|
818
1405
|
})();
|
|
819
1406
|
function gitBranch(dir) {
|
|
820
1407
|
try {
|
|
821
|
-
const head = readFileSync3(
|
|
1408
|
+
const head = readFileSync3(join6(dir, ".git", "HEAD"), "utf8").trim();
|
|
822
1409
|
const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
|
|
823
1410
|
return m ? m[1] : head.slice(0, 7);
|
|
824
1411
|
} catch {
|
|
@@ -885,13 +1472,40 @@ ${c.bold("Projects")} ${c.dim("(one spine across terminal, dashboard and the web
|
|
|
885
1472
|
wholestack push [dir] sync local edits back to the web IDE
|
|
886
1473
|
wholestack open [buildId] open a generated build in the web IDE
|
|
887
1474
|
|
|
1475
|
+
${c.bold("Ship")} ${c.dim("(go live \u2014 same credits + entitlements as the web app)")}
|
|
1476
|
+
wholestack deploy [buildId] live preview deploy (polls to ready)
|
|
1477
|
+
wholestack promote [buildId] [--domain d] durable production deploy (real DB)
|
|
1478
|
+
wholestack rollback [buildId] --deployment <id> restore the previous production deploy
|
|
1479
|
+
${c.dim("buildId defaults to the linked build in this dir (after a delivery or `open`)")}
|
|
1480
|
+
|
|
1481
|
+
${c.bold("Launch")} ${c.dim("(the studio's Launch HQ \u2014 go-to-market, in the terminal)")}
|
|
1482
|
+
wholestack launch [buildId] [--prompt "\u2026"] draw the AI go-to-market plan
|
|
1483
|
+
wholestack launch-kit [buildId] show the build's startup kit (brand/pricing/copy/\u2026)
|
|
1484
|
+
wholestack styles [idea] list Launch Styles (+ the auto-pick for an idea)
|
|
1485
|
+
wholestack legal <terms|privacy|cookie|refund> generate a SaaS legal doc (Markdown \u2192 stdout)
|
|
1486
|
+
wholestack metrics your REAL traffic + revenue (PostHog + Stripe)
|
|
1487
|
+
wholestack waitlist [buildId] live waitlist signup count
|
|
1488
|
+
wholestack businesses every business workspace you own
|
|
1489
|
+
wholestack business [buildId] one workspace + its readiness scorecard
|
|
1490
|
+
${c.dim("add --json for machine-readable output")}
|
|
1491
|
+
|
|
1492
|
+
${c.bold("Prove gate (web2)")} ${c.dim("(deterministic \xB7 fail-closed \xB7 blocking exit code)")}
|
|
1493
|
+
wholestack prove <dir> [--json] gate a checked-out repo (local lockstep mirror)
|
|
1494
|
+
wholestack prove --remote --github o/r [--ref b] hosted, HMAC-signed verdict via /api/v1/prove
|
|
1495
|
+
${c.dim("checks: secrets/sinks/unauth-mutations + authorization-completeness (no orphan routes)")}
|
|
1496
|
+
|
|
888
1497
|
${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmos \xB7 signed cert)")}
|
|
889
|
-
wholestack audit <dir> <Contract> full auto-detect security sweep
|
|
1498
|
+
wholestack audit <dir> <Contract> full auto-detect security sweep (local)
|
|
1499
|
+
wholestack audit repo [--github o/r] [--pick] remote repo audit + live SSE progress
|
|
890
1500
|
wholestack prove <dir> <Contract> --property <k> prove one invariant
|
|
891
1501
|
wholestack verify <dir> <Contract> --cert c.json passthrough + signed certificate
|
|
892
1502
|
${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
|
|
893
1503
|
wholestack --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
|
|
894
1504
|
|
|
1505
|
+
${c.bold("GitHub")} ${c.dim("(private repos via App installation)")}
|
|
1506
|
+
wholestack github installations list linked GitHub App installs
|
|
1507
|
+
wholestack github repos --installation <id> list repos for an installation
|
|
1508
|
+
|
|
895
1509
|
${c.bold("Session")}
|
|
896
1510
|
-m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
|
|
897
1511
|
(lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
|
|
@@ -929,10 +1543,218 @@ ${c.bold("In a session")} ${c.dim("(type /help for the full list)")}
|
|
|
929
1543
|
${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
|
|
930
1544
|
${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
|
|
931
1545
|
${c.dim("spawn_agents tool fan a task out to parallel read-only sub-agents")}`;
|
|
1546
|
+
function flagValue2(rest, flag) {
|
|
1547
|
+
const i = rest.indexOf(flag);
|
|
1548
|
+
return i >= 0 ? rest[i + 1] : void 0;
|
|
1549
|
+
}
|
|
1550
|
+
async function runGithubSubcommand(raw) {
|
|
1551
|
+
if (raw[0] !== "github") return null;
|
|
1552
|
+
const sub = raw[1];
|
|
1553
|
+
const base2 = webBaseUrl();
|
|
1554
|
+
if (sub === "installations") {
|
|
1555
|
+
const r = await fetchGithubInstallations(base2);
|
|
1556
|
+
if (!r.ok) {
|
|
1557
|
+
line(c.red(` ${r.error}`));
|
|
1558
|
+
return 1;
|
|
1559
|
+
}
|
|
1560
|
+
if (!r.installations.length) {
|
|
1561
|
+
line(c.dim(" no installations \u2014 connect GitHub in the web dashboard."));
|
|
1562
|
+
return 0;
|
|
1563
|
+
}
|
|
1564
|
+
line();
|
|
1565
|
+
for (const row of r.installations) {
|
|
1566
|
+
const suspended = row.suspendedAt ? c.yellow(" \xB7 suspended") : "";
|
|
1567
|
+
line(` ${c.cyan(String(row.installationId))} ${row.accountLogin}${suspended}`);
|
|
1568
|
+
}
|
|
1569
|
+
line();
|
|
1570
|
+
return 0;
|
|
1571
|
+
}
|
|
1572
|
+
if (sub === "repos") {
|
|
1573
|
+
const rawId = flagValue2(raw.slice(2), "--installation");
|
|
1574
|
+
const installationId = rawId ? Number(rawId) : NaN;
|
|
1575
|
+
if (!rawId || !Number.isFinite(installationId)) {
|
|
1576
|
+
line(c.red(" usage: wholestack github repos --installation <id>"));
|
|
1577
|
+
return 1;
|
|
1578
|
+
}
|
|
1579
|
+
const r = await fetchGithubRepos(base2, installationId);
|
|
1580
|
+
if (!r.ok) {
|
|
1581
|
+
line(c.red(` ${r.error}`));
|
|
1582
|
+
return 1;
|
|
1583
|
+
}
|
|
1584
|
+
line();
|
|
1585
|
+
for (const repo of r.repos) {
|
|
1586
|
+
line(
|
|
1587
|
+
` ${repo.fullName}${repo.private ? c.yellow(" \xB7 private") : ""} ${c.dim(repo.defaultBranch ?? "")}`
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
line();
|
|
1591
|
+
return 0;
|
|
1592
|
+
}
|
|
1593
|
+
line(c.red(" usage: wholestack github installations | wholestack github repos --installation <id>"));
|
|
1594
|
+
return 1;
|
|
1595
|
+
}
|
|
1596
|
+
async function runAuditRepoSubcommand(rest) {
|
|
1597
|
+
const base2 = webBaseUrl();
|
|
1598
|
+
const body = {};
|
|
1599
|
+
if (rest.includes("--pick")) {
|
|
1600
|
+
const picked = await pickGithubRepoInteractive(base2);
|
|
1601
|
+
if (!picked.ok) {
|
|
1602
|
+
line(c.red(` ${picked.error}`));
|
|
1603
|
+
return 1;
|
|
1604
|
+
}
|
|
1605
|
+
body.githubRepo = picked.githubRepo;
|
|
1606
|
+
body.installationId = picked.installationId;
|
|
1607
|
+
}
|
|
1608
|
+
const github = flagValue2(rest, "--github");
|
|
1609
|
+
if (github) body.githubRepo = github;
|
|
1610
|
+
const installationRaw = flagValue2(rest, "--installation");
|
|
1611
|
+
if (installationRaw) {
|
|
1612
|
+
const n = Number(installationRaw);
|
|
1613
|
+
if (!Number.isFinite(n)) {
|
|
1614
|
+
line(c.red(" --installation must be a number"));
|
|
1615
|
+
return 1;
|
|
1616
|
+
}
|
|
1617
|
+
body.installationId = n;
|
|
1618
|
+
}
|
|
1619
|
+
const gitUrl = flagValue2(rest, "--git-url");
|
|
1620
|
+
if (gitUrl) body.gitUrl = gitUrl;
|
|
1621
|
+
const sourcePath = flagValue2(rest, "--source");
|
|
1622
|
+
if (sourcePath) {
|
|
1623
|
+
const src = readSourceFile(sourcePath);
|
|
1624
|
+
if (!src.ok) {
|
|
1625
|
+
line(c.red(` ${src.error}`));
|
|
1626
|
+
return 1;
|
|
1627
|
+
}
|
|
1628
|
+
body.source = src.source;
|
|
1629
|
+
}
|
|
1630
|
+
const maxRaw = flagValue2(rest, "--max-contracts");
|
|
1631
|
+
if (maxRaw) {
|
|
1632
|
+
const n = Number(maxRaw);
|
|
1633
|
+
if (!Number.isFinite(n)) {
|
|
1634
|
+
line(c.red(" --max-contracts must be a number"));
|
|
1635
|
+
return 1;
|
|
1636
|
+
}
|
|
1637
|
+
body.maxContracts = n;
|
|
1638
|
+
}
|
|
1639
|
+
if (!body.source && !body.gitUrl && !body.githubRepo) {
|
|
1640
|
+
line(c.red(" provide --github owner/repo, --git-url, --source <file>, or --pick"));
|
|
1641
|
+
return 1;
|
|
1642
|
+
}
|
|
1643
|
+
line();
|
|
1644
|
+
line(c.cyan(" \u259F ShipGate repo audit") + c.dim(" \xB7 streaming progress"));
|
|
1645
|
+
line();
|
|
1646
|
+
const r = await runRepoAuditStream(base2, body);
|
|
1647
|
+
line();
|
|
1648
|
+
if (!r.ok) {
|
|
1649
|
+
line(c.red(` \u2717 ${r.error}`));
|
|
1650
|
+
if (r.status === 402 && r.upgradeUrl) {
|
|
1651
|
+
line(c.dim(` upgrade: ${base2}${r.upgradeUrl}`));
|
|
1652
|
+
}
|
|
1653
|
+
return 1;
|
|
1654
|
+
}
|
|
1655
|
+
const verdict = String(r.result.verdict ?? r.result.shipVerdict ?? "unknown");
|
|
1656
|
+
line(verdict === "SHIP" || verdict === "ship" ? c.green(" \u2713 SHIP") : c.yellow(` verdict: ${verdict}`));
|
|
1657
|
+
if (r.result.certificateUrl) line(c.dim(` certificate: ${r.result.certificateUrl}`));
|
|
1658
|
+
if (r.result.bundleId) line(c.dim(` bundle: ${base2}/api/audit/bundle/${r.result.bundleId}`));
|
|
1659
|
+
return 0;
|
|
1660
|
+
}
|
|
1661
|
+
async function runProveSubcommand(raw) {
|
|
1662
|
+
if (raw[0] !== "prove") return null;
|
|
1663
|
+
const rest = raw.slice(1);
|
|
1664
|
+
if (rest.includes("--property")) return null;
|
|
1665
|
+
const wantsJson = rest.includes("--json");
|
|
1666
|
+
if (rest.includes("--remote")) {
|
|
1667
|
+
const base2 = webBaseUrl().replace(/\/$/, "");
|
|
1668
|
+
const key = process.env.ZETA_API_KEY?.trim();
|
|
1669
|
+
if (!key) {
|
|
1670
|
+
line(c.red(" not signed in \u2014 run `wholestack login` first (--remote needs a token)."));
|
|
1671
|
+
return 1;
|
|
1672
|
+
}
|
|
1673
|
+
const body = {};
|
|
1674
|
+
const github = flagValue2(rest, "--github");
|
|
1675
|
+
if (github) body.githubRepo = github;
|
|
1676
|
+
const gitUrl = flagValue2(rest, "--git-url");
|
|
1677
|
+
if (gitUrl) body.gitUrl = gitUrl;
|
|
1678
|
+
const installationRaw = flagValue2(rest, "--installation");
|
|
1679
|
+
if (installationRaw) {
|
|
1680
|
+
const n = Number(installationRaw);
|
|
1681
|
+
if (!Number.isFinite(n)) {
|
|
1682
|
+
line(c.red(" --installation must be a number"));
|
|
1683
|
+
return 1;
|
|
1684
|
+
}
|
|
1685
|
+
body.installationId = n;
|
|
1686
|
+
}
|
|
1687
|
+
const ref = flagValue2(rest, "--ref");
|
|
1688
|
+
if (ref) body.ref = ref;
|
|
1689
|
+
if (!body.githubRepo && !body.gitUrl) {
|
|
1690
|
+
line(c.red(" --remote needs --github owner/repo or --git-url <url>"));
|
|
1691
|
+
return 1;
|
|
1692
|
+
}
|
|
1693
|
+
let resp;
|
|
1694
|
+
try {
|
|
1695
|
+
resp = await fetch(`${base2}/api/v1/prove`, {
|
|
1696
|
+
method: "POST",
|
|
1697
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
1698
|
+
body: JSON.stringify(body)
|
|
1699
|
+
});
|
|
1700
|
+
} catch (e) {
|
|
1701
|
+
line(c.red(` cannot reach the platform: ${e.message}`));
|
|
1702
|
+
return 1;
|
|
1703
|
+
}
|
|
1704
|
+
const data = await resp.json().catch(() => null);
|
|
1705
|
+
if (wantsJson) {
|
|
1706
|
+
stdout3.write(JSON.stringify(data) + "\n");
|
|
1707
|
+
return data?.verdict === "SHIP" ? 0 : 1;
|
|
1708
|
+
}
|
|
1709
|
+
if (resp.status === 401 || resp.status === 402 || resp.status === 503 || !data) {
|
|
1710
|
+
line(c.red(` \u2717 ${data?.error ?? `platform returned ${resp.status}`}`));
|
|
1711
|
+
if (data?.upgradeUrl) line(c.dim(` upgrade: ${base2}${data.upgradeUrl}`));
|
|
1712
|
+
return 1;
|
|
1713
|
+
}
|
|
1714
|
+
printProveVerdict(data.verdict ?? "NO_SHIP", data.score ?? 0, data.checks ?? [], data.blockingReasons ?? []);
|
|
1715
|
+
return data.verdict === "SHIP" ? 0 : 1;
|
|
1716
|
+
}
|
|
1717
|
+
const path = rest.find((a) => !a.startsWith("-")) ?? cwd2();
|
|
1718
|
+
if (!existsSync2(path)) {
|
|
1719
|
+
line(c.red(` path not found: ${path}`));
|
|
1720
|
+
return 1;
|
|
1721
|
+
}
|
|
1722
|
+
const result = proveLocalRepo(path);
|
|
1723
|
+
if (wantsJson) {
|
|
1724
|
+
stdout3.write(JSON.stringify(result) + "\n");
|
|
1725
|
+
return result.verdict === "SHIP" ? 0 : 1;
|
|
1726
|
+
}
|
|
1727
|
+
line();
|
|
1728
|
+
line(c.cyan(" \u259F ISL prove-gate") + c.dim(" \xB7 deterministic \xB7 fail-closed \xB7 foreign default-deny"));
|
|
1729
|
+
line();
|
|
1730
|
+
printProveVerdict(result.verdict, result.score, result.checks, result.blockingReasons);
|
|
1731
|
+
return result.verdict === "SHIP" ? 0 : 1;
|
|
1732
|
+
}
|
|
1733
|
+
function printProveVerdict(verdict, score, checks, blockingReasons) {
|
|
1734
|
+
for (const ch of checks) {
|
|
1735
|
+
const icon = ch.status === "pass" ? c.green("\u2713") : ch.status === "fail" ? c.red("\u2717") : c.dim("\u2205");
|
|
1736
|
+
const label = ch.status === "not-run" ? c.dim(ch.title + " (not run)") : ch.title;
|
|
1737
|
+
line(` ${icon} ${label}`);
|
|
1738
|
+
line(c.dim(` ${ch.detail}`));
|
|
1739
|
+
}
|
|
1740
|
+
line();
|
|
1741
|
+
if (blockingReasons.length) {
|
|
1742
|
+
line(c.red(` ${blockingReasons.length} blocking reason(s):`));
|
|
1743
|
+
for (const r of blockingReasons.slice(0, 25)) line(c.dim(` - ${r}`));
|
|
1744
|
+
if (blockingReasons.length > 25) line(c.dim(` \u2026+${blockingReasons.length - 25} more`));
|
|
1745
|
+
line();
|
|
1746
|
+
}
|
|
1747
|
+
line(
|
|
1748
|
+
verdict === "SHIP" ? c.green(` \u2713 SHIP`) + c.dim(` \xB7 score ${score}/100`) : c.red(` \u2717 NO_SHIP`) + c.dim(` \xB7 score ${score}/100`)
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
932
1751
|
async function runSecuritySubcommand(raw) {
|
|
933
1752
|
const verb = raw[0];
|
|
934
1753
|
if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
|
|
935
1754
|
const rest = raw.slice(1);
|
|
1755
|
+
if (verb === "audit" && rest[0] === "repo") {
|
|
1756
|
+
return runAuditRepoSubcommand(rest.slice(1));
|
|
1757
|
+
}
|
|
936
1758
|
const proverArgs = verb === "verify" && rest[0]?.endsWith(".json") ? ["verify", ...rest] : [...rest];
|
|
937
1759
|
if (verb === "prove" && !proverArgs.includes("--property")) {
|
|
938
1760
|
line(c.red(" prove needs --property <kind>"));
|
|
@@ -942,7 +1764,7 @@ async function runSecuritySubcommand(raw) {
|
|
|
942
1764
|
line();
|
|
943
1765
|
line(c.cyan(` \u259F ShipGate ${verb}`) + c.dim(" \xB7 forge \xB7 slither \xB7 firewall \xB7 halmos"));
|
|
944
1766
|
line();
|
|
945
|
-
const r = await runProver(proverArgs, { cwd:
|
|
1767
|
+
const r = await runProver(proverArgs, { cwd: cwd2(), inherit: true });
|
|
946
1768
|
if (r.missing) {
|
|
947
1769
|
line(c.red(` ${r.out}`));
|
|
948
1770
|
return 1;
|
|
@@ -954,7 +1776,7 @@ async function runSecuritySubcommand(raw) {
|
|
|
954
1776
|
function maybeYoloNotice() {
|
|
955
1777
|
try {
|
|
956
1778
|
const dir = stateDir();
|
|
957
|
-
const marker =
|
|
1779
|
+
const marker = join6(dir, "yolo-notice-seen");
|
|
958
1780
|
if (existsSync2(marker)) return;
|
|
959
1781
|
line(
|
|
960
1782
|
" " + 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"
|
|
@@ -964,9 +1786,6 @@ function maybeYoloNotice() {
|
|
|
964
1786
|
} catch {
|
|
965
1787
|
}
|
|
966
1788
|
}
|
|
967
|
-
function webBaseUrl() {
|
|
968
|
-
return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
|
|
969
|
-
}
|
|
970
1789
|
async function runLogin(raw) {
|
|
971
1790
|
if (raw[0] !== "login" && raw[0] !== "logout") return null;
|
|
972
1791
|
const rest = raw.slice(1);
|
|
@@ -1018,7 +1837,7 @@ async function runLogin(raw) {
|
|
|
1018
1837
|
line(c.red(" usage: wholestack login --member <key> | --brain <key> | --build <key>"));
|
|
1019
1838
|
return 1;
|
|
1020
1839
|
}
|
|
1021
|
-
const rl = createInterface({ input: stdin, output:
|
|
1840
|
+
const rl = createInterface({ input: stdin, output: stdout3 });
|
|
1022
1841
|
if (!name) {
|
|
1023
1842
|
const which = (await rl.question(c.cyan(" key for [member/brain/engine]: "))).trim().toLowerCase();
|
|
1024
1843
|
name = which.startsWith("m") ? "ZETA_API_KEY" : "CEREBRAS_API_KEY";
|
|
@@ -1040,9 +1859,9 @@ async function runLogin(raw) {
|
|
|
1040
1859
|
async function runProjectSubcommand(raw) {
|
|
1041
1860
|
const verb = raw[0];
|
|
1042
1861
|
if (verb !== "projects" && verb !== "pull" && verb !== "push" && verb !== "open") return null;
|
|
1043
|
-
const
|
|
1862
|
+
const base2 = webBaseUrl();
|
|
1044
1863
|
if (verb === "projects") {
|
|
1045
|
-
const r2 = await listProjects(
|
|
1864
|
+
const r2 = await listProjects(base2);
|
|
1046
1865
|
if (!r2.ok) {
|
|
1047
1866
|
line(c.red(` ${r2.error}`));
|
|
1048
1867
|
return 1;
|
|
@@ -1058,7 +1877,7 @@ async function runProjectSubcommand(raw) {
|
|
|
1058
1877
|
if (p.description) line(" " + c.dim(` ${p.description}`));
|
|
1059
1878
|
}
|
|
1060
1879
|
line();
|
|
1061
|
-
line(c.dim(" pull one with ") + c.cyan("wholestack pull <id>") + c.dim(" \xB7 open in the IDE at ") + c.cyan(`${
|
|
1880
|
+
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>`));
|
|
1062
1881
|
return 0;
|
|
1063
1882
|
}
|
|
1064
1883
|
if (verb === "pull") {
|
|
@@ -1067,7 +1886,7 @@ async function runProjectSubcommand(raw) {
|
|
|
1067
1886
|
line(c.red(" usage: wholestack pull <projectId> [dir]"));
|
|
1068
1887
|
return 1;
|
|
1069
1888
|
}
|
|
1070
|
-
const r2 = await pullWorkingTree(
|
|
1889
|
+
const r2 = await pullWorkingTree(base2, projectId);
|
|
1071
1890
|
if (!r2.ok) {
|
|
1072
1891
|
line(c.red(` ${r2.error}`));
|
|
1073
1892
|
return 1;
|
|
@@ -1093,14 +1912,14 @@ async function runProjectSubcommand(raw) {
|
|
|
1093
1912
|
line(c.red(" nothing to push (no readable text files found)."));
|
|
1094
1913
|
return 1;
|
|
1095
1914
|
}
|
|
1096
|
-
const r2 = await pushWorkingTree(
|
|
1915
|
+
const r2 = await pushWorkingTree(base2, projectId, files);
|
|
1097
1916
|
if (!r2.ok) {
|
|
1098
1917
|
line(c.red(` ${r2.error}`));
|
|
1099
1918
|
return 1;
|
|
1100
1919
|
}
|
|
1101
1920
|
writeProjectLink(dir, { projectId });
|
|
1102
1921
|
line(c.green(` \u2713 pushed ${files.length} files`) + c.dim(skipped ? ` (${skipped} skipped: binary/oversize)` : ""));
|
|
1103
|
-
line(c.dim(" open it: ") + c.cyan(`${
|
|
1922
|
+
line(c.dim(" open it: ") + c.cyan(`${base2}/ide/studio?projectId=${projectId}`));
|
|
1104
1923
|
return 0;
|
|
1105
1924
|
}
|
|
1106
1925
|
const buildId = raw[1] ?? readProjectLink(".")?.buildId;
|
|
@@ -1108,14 +1927,216 @@ async function runProjectSubcommand(raw) {
|
|
|
1108
1927
|
line(c.red(" usage: wholestack open <buildId> (or run it inside a delivered app dir)"));
|
|
1109
1928
|
return 1;
|
|
1110
1929
|
}
|
|
1111
|
-
const r = await openBuildInEditor(
|
|
1930
|
+
const r = await openBuildInEditor(base2, buildId);
|
|
1112
1931
|
if (!r.ok) {
|
|
1113
1932
|
line(c.red(` ${r.error}`));
|
|
1114
1933
|
return 1;
|
|
1115
1934
|
}
|
|
1116
1935
|
if (r.projectId) writeProjectLink(".", { buildId, projectId: r.projectId });
|
|
1117
1936
|
line(c.green(" \u2713 project ready in the web IDE"));
|
|
1118
|
-
line(c.dim(" ") + c.cyan(`${
|
|
1937
|
+
line(c.dim(" ") + c.cyan(`${base2}${r.editorUrl ?? `/ide/studio?projectId=${r.projectId}`}`));
|
|
1938
|
+
return 0;
|
|
1939
|
+
}
|
|
1940
|
+
async function runShipSubcommand(raw) {
|
|
1941
|
+
const verb = raw[0];
|
|
1942
|
+
if (verb !== "deploy" && verb !== "promote" && verb !== "rollback") return null;
|
|
1943
|
+
const base2 = webBaseUrl();
|
|
1944
|
+
const rest = raw.slice(1);
|
|
1945
|
+
const buildId = (rest.find((a) => !a.startsWith("-")) ?? readProjectLink(".")?.buildId)?.trim();
|
|
1946
|
+
if (!buildId) {
|
|
1947
|
+
line(c.red(` usage: wholestack ${verb} <buildId> (or run it inside a delivered app dir)`));
|
|
1948
|
+
return 1;
|
|
1949
|
+
}
|
|
1950
|
+
if (verb === "deploy") {
|
|
1951
|
+
line();
|
|
1952
|
+
line(c.cyan(" \u259F deploy") + c.dim(" \xB7 deploy & host with us (managed add-on) \u2014 or own the code on Pro and self-host"));
|
|
1953
|
+
const r2 = await deployBuild(base2, buildId, {
|
|
1954
|
+
poll: true,
|
|
1955
|
+
onPhase: (s) => line(c.dim(` \xB7 ${s}`))
|
|
1956
|
+
});
|
|
1957
|
+
return printDeploy(r2, base2, "deployed");
|
|
1958
|
+
}
|
|
1959
|
+
if (verb === "promote") {
|
|
1960
|
+
const domain = flagValue2(rest, "--domain");
|
|
1961
|
+
line();
|
|
1962
|
+
line(c.cyan(" \u259F promote") + c.dim(" \xB7 durable production \xB7 real database, dev-auth OFF"));
|
|
1963
|
+
const r2 = await promoteBuild(base2, buildId, domain ? { domain } : {});
|
|
1964
|
+
return printDeploy(r2, base2, "promoted to production");
|
|
1965
|
+
}
|
|
1966
|
+
const deploymentId = flagValue2(rest, "--deployment") ?? flagValue2(rest, "--deployment-id");
|
|
1967
|
+
if (!deploymentId) {
|
|
1968
|
+
line(c.red(" usage: wholestack rollback [buildId] --deployment <currentDeploymentId>"));
|
|
1969
|
+
return 1;
|
|
1970
|
+
}
|
|
1971
|
+
line();
|
|
1972
|
+
line(c.cyan(" \u259F rollback") + c.dim(" \xB7 restore the previous production deployment"));
|
|
1973
|
+
const r = await rollbackDeployment(base2, buildId, deploymentId);
|
|
1974
|
+
return printDeploy(r, base2, "rolled back");
|
|
1975
|
+
}
|
|
1976
|
+
function printDeploy(r, base2, doneVerb) {
|
|
1977
|
+
line();
|
|
1978
|
+
if (!r.ok) {
|
|
1979
|
+
line(c.red(` \u2717 ${r.error}`));
|
|
1980
|
+
if (r.hint) line(c.dim(` ${r.hint}`));
|
|
1981
|
+
if (r.upgradeUrl) line(c.dim(` upgrade: ${base2.replace(/\/$/, "")}${r.upgradeUrl}`));
|
|
1982
|
+
if (r.connectUrl) line(c.dim(` connect: ${base2.replace(/\/$/, "")}${r.connectUrl}`));
|
|
1983
|
+
return 1;
|
|
1984
|
+
}
|
|
1985
|
+
line(c.green(` \u2713 ${doneVerb}`) + (r.status ? c.dim(` \xB7 ${r.status}`) : ""));
|
|
1986
|
+
if (r.url) line(c.dim(" ") + c.cyan(r.url.startsWith("http") ? r.url : `https://${r.url}`));
|
|
1987
|
+
if (r.deploymentId) line(c.dim(` deployment ${r.deploymentId}`));
|
|
1988
|
+
if (r.dbNote) line(c.dim(` ${r.dbNote}`));
|
|
1989
|
+
else if (r.dbProvisioned) line(c.dim(" database provisioned + schema applied"));
|
|
1990
|
+
return 0;
|
|
1991
|
+
}
|
|
1992
|
+
async function runLaunchSubcommand(raw) {
|
|
1993
|
+
const verb = raw[0];
|
|
1994
|
+
if (verb !== "launch" && verb !== "launch-kit") return null;
|
|
1995
|
+
const base2 = webBaseUrl();
|
|
1996
|
+
const rest = raw.slice(1);
|
|
1997
|
+
const wantsJson = rest.includes("--json");
|
|
1998
|
+
const argBuildId = rest.find((a) => !a.startsWith("-"));
|
|
1999
|
+
const buildId = (argBuildId ?? readProjectLink(".")?.buildId)?.trim();
|
|
2000
|
+
if (verb === "launch") {
|
|
2001
|
+
const prompt = flagValue2(rest, "--prompt");
|
|
2002
|
+
line();
|
|
2003
|
+
line(c.cyan(" \u259F launch canvas") + c.dim(" \xB7 drawing your go-to-market plan\u2026"));
|
|
2004
|
+
const r2 = await drawCanvas(base2, { buildId, prompt });
|
|
2005
|
+
if (!r2.ok || !r2.plan) {
|
|
2006
|
+
line(c.red(` \u2717 ${r2.error ?? "no plan returned"}`));
|
|
2007
|
+
return 1;
|
|
2008
|
+
}
|
|
2009
|
+
if (wantsJson) {
|
|
2010
|
+
stdout3.write(JSON.stringify(r2.plan, null, 2) + "\n");
|
|
2011
|
+
return 0;
|
|
2012
|
+
}
|
|
2013
|
+
for (const ln of renderGtmPlanLines(r2.plan)) line(ln);
|
|
2014
|
+
return 0;
|
|
2015
|
+
}
|
|
2016
|
+
if (!buildId) {
|
|
2017
|
+
line(c.red(" usage: wholestack launch-kit <buildId> (or run it inside a delivered app dir)"));
|
|
2018
|
+
return 1;
|
|
2019
|
+
}
|
|
2020
|
+
const r = await getLaunchKit(base2, buildId);
|
|
2021
|
+
if (!r.ok || !r.kit) {
|
|
2022
|
+
line(c.red(` \u2717 ${r.error ?? "no launch kit"}`));
|
|
2023
|
+
return 1;
|
|
2024
|
+
}
|
|
2025
|
+
if (wantsJson) {
|
|
2026
|
+
stdout3.write(JSON.stringify(r.kit, null, 2) + "\n");
|
|
2027
|
+
return 0;
|
|
2028
|
+
}
|
|
2029
|
+
for (const ln of renderLaunchKitLines(r.kit)) line(ln);
|
|
2030
|
+
return 0;
|
|
2031
|
+
}
|
|
2032
|
+
async function runHqSubcommand(raw) {
|
|
2033
|
+
const verb = raw[0];
|
|
2034
|
+
if (!["legal", "metrics", "waitlist", "businesses", "business", "styles"].includes(verb)) return null;
|
|
2035
|
+
const base2 = webBaseUrl();
|
|
2036
|
+
const rest = raw.slice(1);
|
|
2037
|
+
const wantsJson = rest.includes("--json");
|
|
2038
|
+
const firstArg = rest.find((a) => !a.startsWith("-"));
|
|
2039
|
+
if (verb === "legal") {
|
|
2040
|
+
const kinds = ["terms", "privacy", "cookie", "refund"];
|
|
2041
|
+
const kind = firstArg;
|
|
2042
|
+
if (!kind || !kinds.includes(kind)) {
|
|
2043
|
+
line(c.red(" usage: wholestack legal <terms|privacy|cookie|refund> [--name <business>]"));
|
|
2044
|
+
return 1;
|
|
2045
|
+
}
|
|
2046
|
+
const r2 = await generateLegal(base2, { kind, businessName: flagValue2(rest, "--name") });
|
|
2047
|
+
if (!r2.ok || !r2.markdown) {
|
|
2048
|
+
line(c.red(` \u2717 ${r2.error ?? "no document produced"}`));
|
|
2049
|
+
return 1;
|
|
2050
|
+
}
|
|
2051
|
+
stdout3.write(r2.markdown.trimEnd() + "\n");
|
|
2052
|
+
return 0;
|
|
2053
|
+
}
|
|
2054
|
+
if (verb === "styles") {
|
|
2055
|
+
const idea = rest.filter((a) => !a.startsWith("-")).join(" ").trim();
|
|
2056
|
+
let data;
|
|
2057
|
+
try {
|
|
2058
|
+
const res = await fetch(`${base2}/api/zeta/styles${idea ? `?idea=${encodeURIComponent(idea)}` : ""}`);
|
|
2059
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2060
|
+
data = await res.json();
|
|
2061
|
+
} catch (e) {
|
|
2062
|
+
line(c.red(` \u2717 couldn't load styles: ${e.message}`));
|
|
2063
|
+
return 1;
|
|
2064
|
+
}
|
|
2065
|
+
if (wantsJson) {
|
|
2066
|
+
stdout3.write(JSON.stringify(data, null, 2) + "\n");
|
|
2067
|
+
return 0;
|
|
2068
|
+
}
|
|
2069
|
+
if (data.recommended) {
|
|
2070
|
+
line(" " + c.bold("Recommended: ") + c.cyan(data.recommended.id) + c.dim(` \u2014 ${data.recommended.why}`));
|
|
2071
|
+
if (data.recommended.alternatives?.length) {
|
|
2072
|
+
line(" " + c.dim(` also: ${data.recommended.alternatives.join(", ")}`));
|
|
2073
|
+
}
|
|
2074
|
+
line("");
|
|
2075
|
+
}
|
|
2076
|
+
const styles = data.styles ?? [];
|
|
2077
|
+
line(" " + c.dim(`${styles.length} Launch Styles \u2014 build in one with: wholestack build \u2026 --style <id>`));
|
|
2078
|
+
for (const s of styles) line(" " + c.cyan(String(s.id).padEnd(20)) + c.dim(s.aesthetic));
|
|
2079
|
+
return 0;
|
|
2080
|
+
}
|
|
2081
|
+
if (verb === "metrics") {
|
|
2082
|
+
const r2 = await readMetrics(base2);
|
|
2083
|
+
if (!r2.ok || !r2.metrics) {
|
|
2084
|
+
line(c.red(` \u2717 ${r2.error ?? "could not read metrics"}`));
|
|
2085
|
+
return 1;
|
|
2086
|
+
}
|
|
2087
|
+
if (wantsJson) {
|
|
2088
|
+
stdout3.write(JSON.stringify(r2.metrics, null, 2) + "\n");
|
|
2089
|
+
return 0;
|
|
2090
|
+
}
|
|
2091
|
+
for (const ln of renderMetricsLines(r2.metrics)) line(ln);
|
|
2092
|
+
return 0;
|
|
2093
|
+
}
|
|
2094
|
+
if (verb === "waitlist") {
|
|
2095
|
+
const buildId2 = (firstArg ?? readProjectLink(".")?.buildId)?.trim();
|
|
2096
|
+
if (!buildId2) {
|
|
2097
|
+
line(c.red(" usage: wholestack waitlist <buildId> (or run it inside a delivered app dir)"));
|
|
2098
|
+
return 1;
|
|
2099
|
+
}
|
|
2100
|
+
const r2 = await waitlistCount(base2, buildId2);
|
|
2101
|
+
if (!r2.ok) {
|
|
2102
|
+
line(c.red(` \u2717 ${r2.error}`));
|
|
2103
|
+
return 1;
|
|
2104
|
+
}
|
|
2105
|
+
line();
|
|
2106
|
+
line(" " + c.bold(String(r2.count ?? 0)) + c.dim(" waitlist signup(s)"));
|
|
2107
|
+
return 0;
|
|
2108
|
+
}
|
|
2109
|
+
if (verb === "businesses") {
|
|
2110
|
+
const r2 = await listBusinesses(base2);
|
|
2111
|
+
if (!r2.ok) {
|
|
2112
|
+
line(c.red(` \u2717 ${r2.error}`));
|
|
2113
|
+
return 1;
|
|
2114
|
+
}
|
|
2115
|
+
if (wantsJson) {
|
|
2116
|
+
stdout3.write(JSON.stringify(r2.businesses ?? [], null, 2) + "\n");
|
|
2117
|
+
return 0;
|
|
2118
|
+
}
|
|
2119
|
+
const rows = r2.businesses ?? [];
|
|
2120
|
+
if (!rows.length) {
|
|
2121
|
+
line(c.dim(" no businesses yet \u2014 draw a launch plan (`wholestack launch`) or build an app."));
|
|
2122
|
+
return 0;
|
|
2123
|
+
}
|
|
2124
|
+
line();
|
|
2125
|
+
for (const b of rows) {
|
|
2126
|
+
const id = String(b.id ?? "");
|
|
2127
|
+
const name = typeof b.name === "string" ? b.name : typeof b.brandName === "string" ? b.brandName : "";
|
|
2128
|
+
line(" " + c.cyan(id) + (name ? " " + c.bold(name) : ""));
|
|
2129
|
+
}
|
|
2130
|
+
line();
|
|
2131
|
+
return 0;
|
|
2132
|
+
}
|
|
2133
|
+
const buildId = (firstArg ?? readProjectLink(".")?.buildId)?.trim();
|
|
2134
|
+
const r = await getBusiness(base2, buildId ? { buildId } : {});
|
|
2135
|
+
if (!r.ok) {
|
|
2136
|
+
line(c.red(` \u2717 ${r.error}`));
|
|
2137
|
+
return 1;
|
|
2138
|
+
}
|
|
2139
|
+
stdout3.write(JSON.stringify({ business: r.business, readiness: r.readiness }, null, 2) + "\n");
|
|
1119
2140
|
return 0;
|
|
1120
2141
|
}
|
|
1121
2142
|
var EMPTY_PLUGINS = {
|
|
@@ -1144,10 +2165,26 @@ async function main() {
|
|
|
1144
2165
|
const rawArgs = argv.slice(2);
|
|
1145
2166
|
const loggedIn = await runLogin(rawArgs);
|
|
1146
2167
|
if (loggedIn !== null) exit(loggedIn);
|
|
2168
|
+
const gh = await runGithubSubcommand(rawArgs);
|
|
2169
|
+
if (gh !== null) exit(gh);
|
|
2170
|
+
const provePath = await runProveSubcommand(rawArgs);
|
|
2171
|
+
if (provePath !== null) exit(provePath);
|
|
1147
2172
|
const sub = await runSecuritySubcommand(rawArgs);
|
|
1148
2173
|
if (sub !== null) exit(sub);
|
|
1149
2174
|
const proj = await runProjectSubcommand(rawArgs);
|
|
1150
2175
|
if (proj !== null) exit(proj);
|
|
2176
|
+
const ship = await runShipSubcommand(rawArgs);
|
|
2177
|
+
if (ship !== null) exit(ship);
|
|
2178
|
+
const launch = await runLaunchSubcommand(rawArgs);
|
|
2179
|
+
if (launch !== null) exit(launch);
|
|
2180
|
+
const hq = await runHqSubcommand(rawArgs);
|
|
2181
|
+
if (hq !== null) exit(hq);
|
|
2182
|
+
const eyes = await runEyesSubcommand(rawArgs);
|
|
2183
|
+
if (eyes !== null) exit(eyes);
|
|
2184
|
+
const ops = await runOpsSubcommand(rawArgs);
|
|
2185
|
+
if (ops !== null) exit(ops);
|
|
2186
|
+
const skillsSub = await runSkillsSubcommand(rawArgs);
|
|
2187
|
+
if (skillsSub !== null) exit(skillsSub);
|
|
1151
2188
|
const args = parse(rawArgs);
|
|
1152
2189
|
if (args.version) {
|
|
1153
2190
|
line(`wholestack ${VERSION}`);
|
|
@@ -1161,9 +2198,12 @@ async function main() {
|
|
|
1161
2198
|
let token = process.env.ZETA_API_KEY;
|
|
1162
2199
|
let access = await verifyAccess(webBaseUrl(), token);
|
|
1163
2200
|
if (!access.active && stdin.isTTY) {
|
|
1164
|
-
|
|
2201
|
+
line();
|
|
2202
|
+
line(" " + c.bold("Sign in to use Wholestack"));
|
|
2203
|
+
line(" " + c.dim(token ? "Your session expired." : "The agent is free \u2014 make, edit & preview unlimited. Sign in to start."));
|
|
2204
|
+
const rl = createInterface({ input: stdin, output: stdout3 });
|
|
1165
2205
|
const ans = (await rl.question(
|
|
1166
|
-
"
|
|
2206
|
+
" " + c.dim("Open the browser to sign in with Google? ") + c.dim("[Y/n] ")
|
|
1167
2207
|
)).trim().toLowerCase();
|
|
1168
2208
|
rl.close();
|
|
1169
2209
|
if (ans === "" || ans === "y" || ans === "yes") {
|
|
@@ -1179,6 +2219,13 @@ async function main() {
|
|
|
1179
2219
|
showPaywall(Boolean(token), webBaseUrl());
|
|
1180
2220
|
exit(1);
|
|
1181
2221
|
}
|
|
2222
|
+
if (!access.paid) {
|
|
2223
|
+
line(
|
|
2224
|
+
c.dim(" Free plan \xB7 make \xB7 edit \xB7 preview unlimited \xB7 ship with Pro \u2192 ") + c.cyan(`${webBaseUrl().replace(/\/$/, "")}/pricing`)
|
|
2225
|
+
);
|
|
2226
|
+
} else if (access.paid && access.tier) {
|
|
2227
|
+
line(c.dim(` ${access.tier} plan`));
|
|
2228
|
+
}
|
|
1182
2229
|
}
|
|
1183
2230
|
let modelKey;
|
|
1184
2231
|
try {
|
|
@@ -1194,9 +2241,19 @@ async function main() {
|
|
|
1194
2241
|
line(c.red(e.message));
|
|
1195
2242
|
exit(1);
|
|
1196
2243
|
}
|
|
2244
|
+
if (args.model) {
|
|
2245
|
+
escalationBus.lock();
|
|
2246
|
+
} else {
|
|
2247
|
+
const firstPrompt = args.prompt.length > 0 ? args.prompt : void 0;
|
|
2248
|
+
const boot = pickBootModel(firstPrompt);
|
|
2249
|
+
if (boot && boot.key !== modelKey) {
|
|
2250
|
+
modelKey = boot.key;
|
|
2251
|
+
model = resolveModel(modelKey);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
1197
2254
|
const isTty = !!stdin.isTTY;
|
|
1198
2255
|
const oneShot = args.prompt.length > 0;
|
|
1199
|
-
const workdir =
|
|
2256
|
+
const workdir = cwd2();
|
|
1200
2257
|
const persistedMode = isPermissionMode(
|
|
1201
2258
|
process.env.ZETA_PERMISSION_MODE
|
|
1202
2259
|
) ? process.env.ZETA_PERMISSION_MODE : void 0;
|
|
@@ -1204,7 +2261,9 @@ async function main() {
|
|
|
1204
2261
|
const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
|
|
1205
2262
|
const memory = await loadProjectMemory(workdir);
|
|
1206
2263
|
const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
|
|
1207
|
-
const
|
|
2264
|
+
const skills = SkillRegistry.load(workdir);
|
|
2265
|
+
const learned = loadLearnedMemory({ scope: basename2(workdir) });
|
|
2266
|
+
const memoryText = [memory.text, learned.text, plugins.systemText, skills.promptHint()].filter(Boolean).join("\n\n") || void 0;
|
|
1208
2267
|
const mcp = await loadMcpTools({
|
|
1209
2268
|
cwd: workdir,
|
|
1210
2269
|
only: args.mcp,
|
|
@@ -1236,7 +2295,7 @@ async function main() {
|
|
|
1236
2295
|
},
|
|
1237
2296
|
// Starship-style context segments above the prompt box.
|
|
1238
2297
|
contextBar: () => {
|
|
1239
|
-
const seg = [c.cyan(modelLabel(modelKey)), c.dim(
|
|
2298
|
+
const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename2(workdir))];
|
|
1240
2299
|
const br = gitBranch(workdir);
|
|
1241
2300
|
if (br) seg.push(c.dim("\u2387 " + br));
|
|
1242
2301
|
seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
|
|
@@ -1258,7 +2317,21 @@ async function main() {
|
|
|
1258
2317
|
...buildWebTools(),
|
|
1259
2318
|
...mcp.tools,
|
|
1260
2319
|
// Parallel read-only sub-agents (needs the model handle the base tools lack).
|
|
1261
|
-
...buildSubagentTool({ model, modelKey, ctx: toolCtx })
|
|
2320
|
+
...buildSubagentTool({ model, modelKey, ctx: toolCtx }),
|
|
2321
|
+
// Parallel WRITABLE workers in isolated git worktrees (review-then-merge diffs).
|
|
2322
|
+
...buildWorkersTool({ model, modelKey, ctx: toolCtx }),
|
|
2323
|
+
// Self-verify + heal: run the web2 ShipGate (prove_app) + the app's own checks (verify_build).
|
|
2324
|
+
...buildVerifyTools({ ctx: toolCtx }),
|
|
2325
|
+
// Eyes — open a running app in a real browser and report what it sees.
|
|
2326
|
+
...buildEyesTool({ ctx: toolCtx }),
|
|
2327
|
+
// Living ops — operate the app after ship (health/deploy/metrics/errors).
|
|
2328
|
+
...buildOpsTools({ ctx: toolCtx }),
|
|
2329
|
+
// Cross-session learned memory (remember / recall).
|
|
2330
|
+
...buildLearnedMemoryTools({ ctx: toolCtx }),
|
|
2331
|
+
// Composable skills (list_skills / use_skill) from the boot-time registry.
|
|
2332
|
+
...buildSkillTools({ registry: skills }),
|
|
2333
|
+
// Auto model-routing: the escalate_model tool posts requests the loop drains.
|
|
2334
|
+
...buildEscalateTool({ ctx: toolCtx, currentKey: modelKey, bus: escalationBus })
|
|
1262
2335
|
};
|
|
1263
2336
|
let session;
|
|
1264
2337
|
let resumedMessages = null;
|
|
@@ -1286,7 +2359,7 @@ async function main() {
|
|
|
1286
2359
|
persona: args.persona,
|
|
1287
2360
|
thinking,
|
|
1288
2361
|
yolo: mode === "yolo",
|
|
1289
|
-
maxSteps:
|
|
2362
|
+
maxSteps: 50,
|
|
1290
2363
|
contextWindow: modelContextWindow(modelKey),
|
|
1291
2364
|
session
|
|
1292
2365
|
});
|
|
@@ -1337,6 +2410,19 @@ ${m.context}` : args.prompt;
|
|
|
1337
2410
|
const runTurn = async (text, images) => {
|
|
1338
2411
|
const t0 = Date.now();
|
|
1339
2412
|
await ic.runInterruptible((sig) => agent.send(text, sig, images));
|
|
2413
|
+
const pending = escalationBus.drain();
|
|
2414
|
+
if (pending && pending.key !== modelKey) {
|
|
2415
|
+
try {
|
|
2416
|
+
modelKey = pending.key;
|
|
2417
|
+
agent.setModel(resolveModel(modelKey), modelKey, modelContextWindow(modelKey));
|
|
2418
|
+
agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
|
|
2419
|
+
line(
|
|
2420
|
+
" " + c.green(`\u2192 ${modelLabel(modelKey)}`) + c.dim(` \xB7 auto-route: ${pending.reason}`) + "\n"
|
|
2421
|
+
);
|
|
2422
|
+
} catch (e) {
|
|
2423
|
+
line(" " + c.red(e.message));
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
1340
2426
|
statusLine({
|
|
1341
2427
|
model: modelLabel(modelKey),
|
|
1342
2428
|
tokens: agent.usage.outputTokens,
|
|
@@ -1373,6 +2459,7 @@ ${m.context}` : args.prompt;
|
|
|
1373
2459
|
const m = resolveModel(res.modelKey);
|
|
1374
2460
|
modelKey = res.modelKey;
|
|
1375
2461
|
agent.setModel(m, modelKey, modelContextWindow(modelKey));
|
|
2462
|
+
escalationBus.lock();
|
|
1376
2463
|
agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
|
|
1377
2464
|
line(
|
|
1378
2465
|
" " + c.green(`\u2192 ${modelLabel(modelKey)}`) + (agent.thinkingOn ? c.dim(" \xB7 thinking on") : "") + "\n"
|