wholestack 0.5.6 → 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 +23 -0
- package/dist/{chunk-K6MSDQJT.js → chunk-TLYCOIEA.js} +4110 -306
- package/dist/cli.js +1117 -60
- 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/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-TLYCOIEA.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,36 @@ 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: 3 delivered apps/month \xB7 then ") + c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
|
|
803
1388
|
line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
|
|
804
1389
|
line();
|
|
805
1390
|
}
|
|
806
1391
|
|
|
807
1392
|
// src/cli.ts
|
|
808
1393
|
import { readFileSync as readFileSync3 } from "fs";
|
|
809
|
-
import { basename, dirname as dirname2 } from "path";
|
|
1394
|
+
import { basename as basename2, dirname as dirname2 } from "path";
|
|
810
1395
|
import { fileURLToPath } from "url";
|
|
811
1396
|
var VERSION = (() => {
|
|
812
1397
|
try {
|
|
813
1398
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
814
|
-
return JSON.parse(readFileSync3(
|
|
1399
|
+
return JSON.parse(readFileSync3(join6(here, "..", "package.json"), "utf8")).version;
|
|
815
1400
|
} catch {
|
|
816
1401
|
return "0.0.0";
|
|
817
1402
|
}
|
|
818
1403
|
})();
|
|
819
1404
|
function gitBranch(dir) {
|
|
820
1405
|
try {
|
|
821
|
-
const head = readFileSync3(
|
|
1406
|
+
const head = readFileSync3(join6(dir, ".git", "HEAD"), "utf8").trim();
|
|
822
1407
|
const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
|
|
823
1408
|
return m ? m[1] : head.slice(0, 7);
|
|
824
1409
|
} catch {
|
|
@@ -885,13 +1470,39 @@ ${c.bold("Projects")} ${c.dim("(one spine across terminal, dashboard and the web
|
|
|
885
1470
|
wholestack push [dir] sync local edits back to the web IDE
|
|
886
1471
|
wholestack open [buildId] open a generated build in the web IDE
|
|
887
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
|
+
|
|
888
1494
|
${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
|
|
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
|
|
890
1497
|
wholestack prove <dir> <Contract> --property <k> prove one invariant
|
|
891
1498
|
wholestack verify <dir> <Contract> --cert c.json passthrough + signed certificate
|
|
892
1499
|
${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
|
|
893
1500
|
wholestack --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
|
|
894
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
|
+
|
|
895
1506
|
${c.bold("Session")}
|
|
896
1507
|
-m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
|
|
897
1508
|
(lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
|
|
@@ -929,10 +1540,218 @@ ${c.bold("In a session")} ${c.dim("(type /help for the full list)")}
|
|
|
929
1540
|
${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
|
|
930
1541
|
${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
|
|
931
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
|
+
}
|
|
932
1748
|
async function runSecuritySubcommand(raw) {
|
|
933
1749
|
const verb = raw[0];
|
|
934
1750
|
if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
|
|
935
1751
|
const rest = raw.slice(1);
|
|
1752
|
+
if (verb === "audit" && rest[0] === "repo") {
|
|
1753
|
+
return runAuditRepoSubcommand(rest.slice(1));
|
|
1754
|
+
}
|
|
936
1755
|
const proverArgs = verb === "verify" && rest[0]?.endsWith(".json") ? ["verify", ...rest] : [...rest];
|
|
937
1756
|
if (verb === "prove" && !proverArgs.includes("--property")) {
|
|
938
1757
|
line(c.red(" prove needs --property <kind>"));
|
|
@@ -942,7 +1761,7 @@ async function runSecuritySubcommand(raw) {
|
|
|
942
1761
|
line();
|
|
943
1762
|
line(c.cyan(` \u259F ShipGate ${verb}`) + c.dim(" \xB7 forge \xB7 slither \xB7 firewall \xB7 halmos"));
|
|
944
1763
|
line();
|
|
945
|
-
const r = await runProver(proverArgs, { cwd:
|
|
1764
|
+
const r = await runProver(proverArgs, { cwd: cwd2(), inherit: true });
|
|
946
1765
|
if (r.missing) {
|
|
947
1766
|
line(c.red(` ${r.out}`));
|
|
948
1767
|
return 1;
|
|
@@ -954,7 +1773,7 @@ async function runSecuritySubcommand(raw) {
|
|
|
954
1773
|
function maybeYoloNotice() {
|
|
955
1774
|
try {
|
|
956
1775
|
const dir = stateDir();
|
|
957
|
-
const marker =
|
|
1776
|
+
const marker = join6(dir, "yolo-notice-seen");
|
|
958
1777
|
if (existsSync2(marker)) return;
|
|
959
1778
|
line(
|
|
960
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"
|
|
@@ -964,9 +1783,6 @@ function maybeYoloNotice() {
|
|
|
964
1783
|
} catch {
|
|
965
1784
|
}
|
|
966
1785
|
}
|
|
967
|
-
function webBaseUrl() {
|
|
968
|
-
return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
|
|
969
|
-
}
|
|
970
1786
|
async function runLogin(raw) {
|
|
971
1787
|
if (raw[0] !== "login" && raw[0] !== "logout") return null;
|
|
972
1788
|
const rest = raw.slice(1);
|
|
@@ -1018,7 +1834,7 @@ async function runLogin(raw) {
|
|
|
1018
1834
|
line(c.red(" usage: wholestack login --member <key> | --brain <key> | --build <key>"));
|
|
1019
1835
|
return 1;
|
|
1020
1836
|
}
|
|
1021
|
-
const rl = createInterface({ input: stdin, output:
|
|
1837
|
+
const rl = createInterface({ input: stdin, output: stdout3 });
|
|
1022
1838
|
if (!name) {
|
|
1023
1839
|
const which = (await rl.question(c.cyan(" key for [member/brain/engine]: "))).trim().toLowerCase();
|
|
1024
1840
|
name = which.startsWith("m") ? "ZETA_API_KEY" : "CEREBRAS_API_KEY";
|
|
@@ -1040,9 +1856,9 @@ async function runLogin(raw) {
|
|
|
1040
1856
|
async function runProjectSubcommand(raw) {
|
|
1041
1857
|
const verb = raw[0];
|
|
1042
1858
|
if (verb !== "projects" && verb !== "pull" && verb !== "push" && verb !== "open") return null;
|
|
1043
|
-
const
|
|
1859
|
+
const base2 = webBaseUrl();
|
|
1044
1860
|
if (verb === "projects") {
|
|
1045
|
-
const r2 = await listProjects(
|
|
1861
|
+
const r2 = await listProjects(base2);
|
|
1046
1862
|
if (!r2.ok) {
|
|
1047
1863
|
line(c.red(` ${r2.error}`));
|
|
1048
1864
|
return 1;
|
|
@@ -1058,7 +1874,7 @@ async function runProjectSubcommand(raw) {
|
|
|
1058
1874
|
if (p.description) line(" " + c.dim(` ${p.description}`));
|
|
1059
1875
|
}
|
|
1060
1876
|
line();
|
|
1061
|
-
line(c.dim(" pull one with ") + c.cyan("wholestack pull <id>") + c.dim(" \xB7 open in the IDE at ") + c.cyan(`${
|
|
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>`));
|
|
1062
1878
|
return 0;
|
|
1063
1879
|
}
|
|
1064
1880
|
if (verb === "pull") {
|
|
@@ -1067,7 +1883,7 @@ async function runProjectSubcommand(raw) {
|
|
|
1067
1883
|
line(c.red(" usage: wholestack pull <projectId> [dir]"));
|
|
1068
1884
|
return 1;
|
|
1069
1885
|
}
|
|
1070
|
-
const r2 = await pullWorkingTree(
|
|
1886
|
+
const r2 = await pullWorkingTree(base2, projectId);
|
|
1071
1887
|
if (!r2.ok) {
|
|
1072
1888
|
line(c.red(` ${r2.error}`));
|
|
1073
1889
|
return 1;
|
|
@@ -1093,14 +1909,14 @@ async function runProjectSubcommand(raw) {
|
|
|
1093
1909
|
line(c.red(" nothing to push (no readable text files found)."));
|
|
1094
1910
|
return 1;
|
|
1095
1911
|
}
|
|
1096
|
-
const r2 = await pushWorkingTree(
|
|
1912
|
+
const r2 = await pushWorkingTree(base2, projectId, files);
|
|
1097
1913
|
if (!r2.ok) {
|
|
1098
1914
|
line(c.red(` ${r2.error}`));
|
|
1099
1915
|
return 1;
|
|
1100
1916
|
}
|
|
1101
1917
|
writeProjectLink(dir, { projectId });
|
|
1102
1918
|
line(c.green(` \u2713 pushed ${files.length} files`) + c.dim(skipped ? ` (${skipped} skipped: binary/oversize)` : ""));
|
|
1103
|
-
line(c.dim(" open it: ") + c.cyan(`${
|
|
1919
|
+
line(c.dim(" open it: ") + c.cyan(`${base2}/ide/studio?projectId=${projectId}`));
|
|
1104
1920
|
return 0;
|
|
1105
1921
|
}
|
|
1106
1922
|
const buildId = raw[1] ?? readProjectLink(".")?.buildId;
|
|
@@ -1108,14 +1924,189 @@ async function runProjectSubcommand(raw) {
|
|
|
1108
1924
|
line(c.red(" usage: wholestack open <buildId> (or run it inside a delivered app dir)"));
|
|
1109
1925
|
return 1;
|
|
1110
1926
|
}
|
|
1111
|
-
const r = await openBuildInEditor(
|
|
1927
|
+
const r = await openBuildInEditor(base2, buildId);
|
|
1112
1928
|
if (!r.ok) {
|
|
1113
1929
|
line(c.red(` ${r.error}`));
|
|
1114
1930
|
return 1;
|
|
1115
1931
|
}
|
|
1116
1932
|
if (r.projectId) writeProjectLink(".", { buildId, projectId: r.projectId });
|
|
1117
1933
|
line(c.green(" \u2713 project ready in the web IDE"));
|
|
1118
|
-
line(c.dim(" ") + c.cyan(`${
|
|
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");
|
|
1119
2110
|
return 0;
|
|
1120
2111
|
}
|
|
1121
2112
|
var EMPTY_PLUGINS = {
|
|
@@ -1144,10 +2135,26 @@ async function main() {
|
|
|
1144
2135
|
const rawArgs = argv.slice(2);
|
|
1145
2136
|
const loggedIn = await runLogin(rawArgs);
|
|
1146
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);
|
|
1147
2142
|
const sub = await runSecuritySubcommand(rawArgs);
|
|
1148
2143
|
if (sub !== null) exit(sub);
|
|
1149
2144
|
const proj = await runProjectSubcommand(rawArgs);
|
|
1150
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);
|
|
1151
2158
|
const args = parse(rawArgs);
|
|
1152
2159
|
if (args.version) {
|
|
1153
2160
|
line(`wholestack ${VERSION}`);
|
|
@@ -1161,9 +2168,9 @@ async function main() {
|
|
|
1161
2168
|
let token = process.env.ZETA_API_KEY;
|
|
1162
2169
|
let access = await verifyAccess(webBaseUrl(), token);
|
|
1163
2170
|
if (!access.active && stdin.isTTY) {
|
|
1164
|
-
const rl = createInterface({ input: stdin, output:
|
|
2171
|
+
const rl = createInterface({ input: stdin, output: stdout3 });
|
|
1165
2172
|
const ans = (await rl.question(
|
|
1166
|
-
"\n " + (token ? c.dim("
|
|
2173
|
+
"\n " + (token ? c.dim("Token invalid or expired. ") + "Log in again? " : "Log in now? ") + c.dim("[Y/n] ")
|
|
1167
2174
|
)).trim().toLowerCase();
|
|
1168
2175
|
rl.close();
|
|
1169
2176
|
if (ans === "" || ans === "y" || ans === "yes") {
|
|
@@ -1179,6 +2186,16 @@ async function main() {
|
|
|
1179
2186
|
showPaywall(Boolean(token), webBaseUrl());
|
|
1180
2187
|
exit(1);
|
|
1181
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
|
+
}
|
|
1182
2199
|
}
|
|
1183
2200
|
let modelKey;
|
|
1184
2201
|
try {
|
|
@@ -1194,9 +2211,19 @@ async function main() {
|
|
|
1194
2211
|
line(c.red(e.message));
|
|
1195
2212
|
exit(1);
|
|
1196
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
|
+
}
|
|
1197
2224
|
const isTty = !!stdin.isTTY;
|
|
1198
2225
|
const oneShot = args.prompt.length > 0;
|
|
1199
|
-
const workdir =
|
|
2226
|
+
const workdir = cwd2();
|
|
1200
2227
|
const persistedMode = isPermissionMode(
|
|
1201
2228
|
process.env.ZETA_PERMISSION_MODE
|
|
1202
2229
|
) ? process.env.ZETA_PERMISSION_MODE : void 0;
|
|
@@ -1204,7 +2231,9 @@ async function main() {
|
|
|
1204
2231
|
const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
|
|
1205
2232
|
const memory = await loadProjectMemory(workdir);
|
|
1206
2233
|
const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
|
|
1207
|
-
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;
|
|
1208
2237
|
const mcp = await loadMcpTools({
|
|
1209
2238
|
cwd: workdir,
|
|
1210
2239
|
only: args.mcp,
|
|
@@ -1236,7 +2265,7 @@ async function main() {
|
|
|
1236
2265
|
},
|
|
1237
2266
|
// Starship-style context segments above the prompt box.
|
|
1238
2267
|
contextBar: () => {
|
|
1239
|
-
const seg = [c.cyan(modelLabel(modelKey)), c.dim(
|
|
2268
|
+
const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename2(workdir))];
|
|
1240
2269
|
const br = gitBranch(workdir);
|
|
1241
2270
|
if (br) seg.push(c.dim("\u2387 " + br));
|
|
1242
2271
|
seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
|
|
@@ -1258,7 +2287,21 @@ async function main() {
|
|
|
1258
2287
|
...buildWebTools(),
|
|
1259
2288
|
...mcp.tools,
|
|
1260
2289
|
// Parallel read-only sub-agents (needs the model handle the base tools lack).
|
|
1261
|
-
...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 })
|
|
1262
2305
|
};
|
|
1263
2306
|
let session;
|
|
1264
2307
|
let resumedMessages = null;
|
|
@@ -1286,7 +2329,7 @@ async function main() {
|
|
|
1286
2329
|
persona: args.persona,
|
|
1287
2330
|
thinking,
|
|
1288
2331
|
yolo: mode === "yolo",
|
|
1289
|
-
maxSteps:
|
|
2332
|
+
maxSteps: 50,
|
|
1290
2333
|
contextWindow: modelContextWindow(modelKey),
|
|
1291
2334
|
session
|
|
1292
2335
|
});
|
|
@@ -1337,6 +2380,19 @@ ${m.context}` : args.prompt;
|
|
|
1337
2380
|
const runTurn = async (text, images) => {
|
|
1338
2381
|
const t0 = Date.now();
|
|
1339
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
|
+
}
|
|
1340
2396
|
statusLine({
|
|
1341
2397
|
model: modelLabel(modelKey),
|
|
1342
2398
|
tokens: agent.usage.outputTokens,
|
|
@@ -1373,6 +2429,7 @@ ${m.context}` : args.prompt;
|
|
|
1373
2429
|
const m = resolveModel(res.modelKey);
|
|
1374
2430
|
modelKey = res.modelKey;
|
|
1375
2431
|
agent.setModel(m, modelKey, modelContextWindow(modelKey));
|
|
2432
|
+
escalationBus.lock();
|
|
1376
2433
|
agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
|
|
1377
2434
|
line(
|
|
1378
2435
|
" " + c.green(`\u2192 ${modelLabel(modelKey)}`) + (agent.thinkingOn ? c.dim(" \xB7 thinking on") : "") + "\n"
|