wholestack 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7DJJXUV4.js → chunk-TDSLCPQL.js} +1115 -54
- package/dist/cli.js +812 -24
- package/dist/index.d.ts +101 -5
- package/dist/index.js +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Session,
|
|
10
10
|
announcePlanMode,
|
|
11
11
|
banner,
|
|
12
|
+
buildTools,
|
|
12
13
|
buildWebTools,
|
|
13
14
|
c,
|
|
14
15
|
isPermissionMode,
|
|
@@ -26,8 +27,10 @@ import {
|
|
|
26
27
|
runProver,
|
|
27
28
|
statusLine,
|
|
28
29
|
supportsThinking,
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
tasks,
|
|
31
|
+
userBox,
|
|
32
|
+
visionCapable
|
|
33
|
+
} from "./chunk-TDSLCPQL.js";
|
|
31
34
|
|
|
32
35
|
// src/cli.ts
|
|
33
36
|
import { createInterface } from "readline/promises";
|
|
@@ -120,7 +123,7 @@ async function loginBrowser(opts) {
|
|
|
120
123
|
const state = randomBytes(16).toString("hex");
|
|
121
124
|
const device = hostname().slice(0, 60);
|
|
122
125
|
const timeoutMs = (opts.timeoutSec ?? 240) * 1e3;
|
|
123
|
-
return new Promise((
|
|
126
|
+
return new Promise((resolve4) => {
|
|
124
127
|
let settled = false;
|
|
125
128
|
const finish = (token) => {
|
|
126
129
|
if (settled) return;
|
|
@@ -130,20 +133,33 @@ async function loginBrowser(opts) {
|
|
|
130
133
|
server.close();
|
|
131
134
|
} catch {
|
|
132
135
|
}
|
|
133
|
-
|
|
136
|
+
resolve4(token);
|
|
134
137
|
};
|
|
135
138
|
const server = createServer((req, res) => {
|
|
136
139
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
140
|
+
const corsHeaders = {
|
|
141
|
+
"access-control-allow-origin": "*",
|
|
142
|
+
"access-control-allow-methods": "GET, OPTIONS",
|
|
143
|
+
"access-control-allow-headers": "*",
|
|
144
|
+
"access-control-allow-private-network": "true"
|
|
145
|
+
};
|
|
146
|
+
if (req.method === "OPTIONS") {
|
|
147
|
+
res.writeHead(204, corsHeaders).end();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
137
150
|
if (url.pathname !== "/callback") {
|
|
138
|
-
res.writeHead(404).end();
|
|
151
|
+
res.writeHead(404, corsHeaders).end();
|
|
139
152
|
return;
|
|
140
153
|
}
|
|
141
154
|
if (url.searchParams.get("state") !== state) {
|
|
142
|
-
res.writeHead(400).end("state mismatch");
|
|
155
|
+
res.writeHead(400, corsHeaders).end("state mismatch");
|
|
143
156
|
return;
|
|
144
157
|
}
|
|
145
158
|
const token = url.searchParams.get("token")?.trim();
|
|
146
|
-
res.writeHead(token ? 200 : 400, {
|
|
159
|
+
res.writeHead(token ? 200 : 400, {
|
|
160
|
+
...corsHeaders,
|
|
161
|
+
"content-type": "text/html; charset=utf-8"
|
|
162
|
+
});
|
|
147
163
|
res.end(token ? SUCCESS_HTML : "missing token");
|
|
148
164
|
if (token) {
|
|
149
165
|
const path = saveKey("ZETA_API_KEY", token);
|
|
@@ -266,7 +282,624 @@ var CheckpointStore = class {
|
|
|
266
282
|
}
|
|
267
283
|
};
|
|
268
284
|
|
|
285
|
+
// src/mentions.ts
|
|
286
|
+
import { readFile as readFile2, stat, readdir } from "fs/promises";
|
|
287
|
+
import { resolve, relative, join as join2, isAbsolute } from "path";
|
|
288
|
+
import fg from "fast-glob";
|
|
289
|
+
var IGNORE = [
|
|
290
|
+
"**/node_modules/**",
|
|
291
|
+
"**/.git/**",
|
|
292
|
+
"**/dist/**",
|
|
293
|
+
"**/.next/**",
|
|
294
|
+
"**/build/**",
|
|
295
|
+
"**/.turbo/**"
|
|
296
|
+
];
|
|
297
|
+
var CHARS_PER_TOKEN = 4;
|
|
298
|
+
var DEFAULT_TOKEN_BUDGET = 25e3;
|
|
299
|
+
var PER_FILE_TOKEN_CAP = 8e3;
|
|
300
|
+
var MAX_FILES_PER_MENTION = 60;
|
|
301
|
+
var MAX_TREE_ENTRIES = 200;
|
|
302
|
+
function extractMentions(line2) {
|
|
303
|
+
const out = [];
|
|
304
|
+
const re = /(^|\s)@([~A-Za-z0-9._\-/*]+)/g;
|
|
305
|
+
for (let m = re.exec(line2); m; m = re.exec(line2)) {
|
|
306
|
+
let p = m[2];
|
|
307
|
+
p = p.replace(/[.,;:)]+$/g, (tail) => tail.length ? "" : tail);
|
|
308
|
+
if (p) out.push(p);
|
|
309
|
+
}
|
|
310
|
+
return [...new Set(out)];
|
|
311
|
+
}
|
|
312
|
+
function estTokens(s) {
|
|
313
|
+
return Math.ceil(s.length / CHARS_PER_TOKEN);
|
|
314
|
+
}
|
|
315
|
+
function looksBinary(buf) {
|
|
316
|
+
if (buf.includes("\0")) return true;
|
|
317
|
+
let ctrl = 0;
|
|
318
|
+
const n = Math.min(buf.length, 1e3);
|
|
319
|
+
for (let i = 0; i < n; i++) {
|
|
320
|
+
const code = buf.charCodeAt(i);
|
|
321
|
+
if (code < 9 || code > 13 && code < 32) ctrl++;
|
|
322
|
+
}
|
|
323
|
+
return ctrl / Math.max(1, n) > 0.3;
|
|
324
|
+
}
|
|
325
|
+
async function renderTree(absDir, relDir) {
|
|
326
|
+
const lines = [];
|
|
327
|
+
let count = 0;
|
|
328
|
+
async function walk(dir, prefix, depth) {
|
|
329
|
+
if (count >= MAX_TREE_ENTRIES || depth > 3) return;
|
|
330
|
+
let entries;
|
|
331
|
+
try {
|
|
332
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
333
|
+
} catch {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const filtered = entries.filter((e) => !["node_modules", ".git", "dist", ".next", "build", ".turbo"].includes(e.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
337
|
+
for (const e of filtered) {
|
|
338
|
+
if (count >= MAX_TREE_ENTRIES) {
|
|
339
|
+
lines.push(`${prefix}\u2026 (truncated)`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
count++;
|
|
343
|
+
lines.push(`${prefix}${e.name}${e.isDirectory() ? "/" : ""}`);
|
|
344
|
+
if (e.isDirectory()) await walk(join2(dir, e.name), prefix + " ", depth + 1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
await walk(absDir, "", 0);
|
|
348
|
+
return `${relDir || "."}/ (directory tree)
|
|
349
|
+
${lines.join("\n")}`;
|
|
350
|
+
}
|
|
351
|
+
async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET) {
|
|
352
|
+
const mentions = extractMentions(message);
|
|
353
|
+
if (mentions.length === 0) return { context: "", notice: null };
|
|
354
|
+
const blocks = [];
|
|
355
|
+
const attached = [];
|
|
356
|
+
const seen = /* @__PURE__ */ new Set();
|
|
357
|
+
let spent = 0;
|
|
358
|
+
for (const mention of mentions) {
|
|
359
|
+
if (spent >= tokenBudget) break;
|
|
360
|
+
const raw = mention.startsWith("~/") ? join2(process.env.HOME ?? "", mention.slice(2)) : mention;
|
|
361
|
+
const direct = isAbsolute(raw) ? raw : resolve(cwd2, raw);
|
|
362
|
+
let isDir = false;
|
|
363
|
+
try {
|
|
364
|
+
isDir = (await stat(direct)).isDirectory();
|
|
365
|
+
} catch {
|
|
366
|
+
isDir = false;
|
|
367
|
+
}
|
|
368
|
+
if (isDir) {
|
|
369
|
+
const rel = relative(cwd2, direct);
|
|
370
|
+
const tree = await renderTree(direct, rel);
|
|
371
|
+
const cost = estTokens(tree);
|
|
372
|
+
if (spent + cost <= tokenBudget) {
|
|
373
|
+
blocks.push(tree);
|
|
374
|
+
spent += cost;
|
|
375
|
+
attached.push(`${rel || "."}/ (tree)`);
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
let files = [];
|
|
380
|
+
try {
|
|
381
|
+
files = await fg(raw, {
|
|
382
|
+
cwd: cwd2,
|
|
383
|
+
ignore: IGNORE,
|
|
384
|
+
onlyFiles: true,
|
|
385
|
+
dot: true,
|
|
386
|
+
suppressErrors: true,
|
|
387
|
+
absolute: false
|
|
388
|
+
});
|
|
389
|
+
} catch {
|
|
390
|
+
files = [];
|
|
391
|
+
}
|
|
392
|
+
if (files.length === 0) {
|
|
393
|
+
try {
|
|
394
|
+
if ((await stat(direct)).isFile()) files = [relative(cwd2, direct)];
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
files = files.sort().slice(0, MAX_FILES_PER_MENTION);
|
|
399
|
+
for (const rel of files) {
|
|
400
|
+
if (spent >= tokenBudget) break;
|
|
401
|
+
if (seen.has(rel)) continue;
|
|
402
|
+
seen.add(rel);
|
|
403
|
+
let buf;
|
|
404
|
+
try {
|
|
405
|
+
buf = await readFile2(resolve(cwd2, rel), "utf8");
|
|
406
|
+
} catch {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (looksBinary(buf)) continue;
|
|
410
|
+
const cap = Math.min(PER_FILE_TOKEN_CAP, tokenBudget - spent);
|
|
411
|
+
let body = buf;
|
|
412
|
+
let truncated = false;
|
|
413
|
+
if (estTokens(body) > cap) {
|
|
414
|
+
body = body.slice(0, cap * CHARS_PER_TOKEN);
|
|
415
|
+
truncated = true;
|
|
416
|
+
}
|
|
417
|
+
const fence = body.includes("```") ? "````" : "```";
|
|
418
|
+
const header = truncated ? `${rel} (truncated to ~${cap} tokens of ${estTokens(buf)})` : rel;
|
|
419
|
+
blocks.push(`${header}
|
|
420
|
+
${fence}
|
|
421
|
+
${body}
|
|
422
|
+
${fence}`);
|
|
423
|
+
spent += estTokens(body);
|
|
424
|
+
attached.push(truncated ? `${rel} (truncated)` : rel);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (blocks.length === 0) return { context: "", notice: null };
|
|
428
|
+
const context = "The user attached these files with @-mentions. Use them as primary context for this turn:\n\n" + blocks.join("\n\n");
|
|
429
|
+
const notice = `attached ${attached.length} item(s): ${attached.slice(0, 8).join(", ")}${attached.length > 8 ? `, +${attached.length - 8} more` : ""} (~${spent} tokens)`;
|
|
430
|
+
return { context, notice };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/subagent.ts
|
|
434
|
+
import { generateText, stepCountIs, tool } from "ai";
|
|
435
|
+
import { z } from "zod";
|
|
436
|
+
var READONLY_TOOLS = ["read_file", "glob", "grep", "list_dir"];
|
|
437
|
+
function readonlyToolset(ctx) {
|
|
438
|
+
const all = buildTools(ctx);
|
|
439
|
+
const out = {};
|
|
440
|
+
for (const k of READONLY_TOOLS) if (all[k]) out[k] = all[k];
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
var SUBAGENT_SYSTEM = "You are a focused sub-agent spawned by a lead engineer. You have READ-ONLY tools (read_file, glob, grep, list_dir). Investigate ONLY the task you are given, then return a tight, factual answer: what you found, with file:line evidence where it applies. Do not pad, do not speculate beyond the evidence, do not attempt edits.";
|
|
444
|
+
function buildSubagentTool(deps) {
|
|
445
|
+
const maxSteps = deps.maxSteps ?? 12;
|
|
446
|
+
const maxAgents = deps.maxAgents ?? 6;
|
|
447
|
+
const subtools = readonlyToolset(deps.ctx);
|
|
448
|
+
return {
|
|
449
|
+
spawn_agents: tool({
|
|
450
|
+
description: "Run several READ-ONLY sub-agents IN PARALLEL, each on its own focused task, and get their answers back together. Use for breadth \u2014 searching/reviewing/investigating multiple areas at once is far faster than doing them one by one yourself. Each sub-agent can read, glob, and grep but CANNOT edit. Returns one result per task.",
|
|
451
|
+
inputSchema: z.object({
|
|
452
|
+
tasks: z.array(
|
|
453
|
+
z.object({
|
|
454
|
+
label: z.string().describe("Short label for this slice, e.g. 'auth' or 'billing routes'."),
|
|
455
|
+
task: z.string().describe("The self-contained instruction for this sub-agent.")
|
|
456
|
+
})
|
|
457
|
+
).min(1).max(maxAgents).describe(`1\u2013${maxAgents} independent tasks to run concurrently.`)
|
|
458
|
+
}),
|
|
459
|
+
execute: async ({ tasks: tasks2 }) => {
|
|
460
|
+
line(c.dim(` \u21C9 spawning ${tasks2.length} sub-agent(s) in parallel`));
|
|
461
|
+
const results = await Promise.all(
|
|
462
|
+
tasks2.map(async ({ label, task }) => {
|
|
463
|
+
try {
|
|
464
|
+
const r = await generateText({
|
|
465
|
+
model: deps.model,
|
|
466
|
+
system: SUBAGENT_SYSTEM,
|
|
467
|
+
prompt: task,
|
|
468
|
+
tools: subtools,
|
|
469
|
+
stopWhen: stepCountIs(maxSteps)
|
|
470
|
+
});
|
|
471
|
+
line(c.dim(` \u21B3 ${c.cyan(label)} done`));
|
|
472
|
+
return { label, ok: true, result: r.text.trim() };
|
|
473
|
+
} catch (e) {
|
|
474
|
+
line(c.dim(` \u21B3 ${c.cyan(label)} ${c.red("failed")}`));
|
|
475
|
+
return { label, ok: false, error: e.message };
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
);
|
|
479
|
+
return { ok: true, count: results.length, results };
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/images.ts
|
|
486
|
+
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
487
|
+
import { resolve as resolve2, extname, relative as relative2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
488
|
+
import { execFile } from "child_process";
|
|
489
|
+
import { tmpdir, platform as platform2 } from "os";
|
|
490
|
+
var EXT_TO_MEDIA = {
|
|
491
|
+
".png": "image/png",
|
|
492
|
+
".jpg": "image/jpeg",
|
|
493
|
+
".jpeg": "image/jpeg",
|
|
494
|
+
".gif": "image/gif",
|
|
495
|
+
".webp": "image/webp"
|
|
496
|
+
};
|
|
497
|
+
var PER_IMAGE_BYTES = 6 * 1024 * 1024;
|
|
498
|
+
var TOTAL_BYTES = 15 * 1024 * 1024;
|
|
499
|
+
function wantsClipboard(message) {
|
|
500
|
+
return /(^|\s)@(clip|clipboard|paste)\b/i.test(message);
|
|
501
|
+
}
|
|
502
|
+
function run(cmd, args) {
|
|
503
|
+
return new Promise((res) => {
|
|
504
|
+
execFile(cmd, args, (err) => res(!err));
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async function grabClipboardImage() {
|
|
508
|
+
const out = join3(tmpdir(), `zeta-clip-${process.pid}-${process.hrtime.bigint()}.png`);
|
|
509
|
+
const os = platform2();
|
|
510
|
+
if (os === "darwin") {
|
|
511
|
+
const script = `set thePath to "${out}"
|
|
512
|
+
try
|
|
513
|
+
set pngData to (the clipboard as \xABclass PNGf\xBB)
|
|
514
|
+
on error
|
|
515
|
+
return "NOIMAGE"
|
|
516
|
+
end try
|
|
517
|
+
set fp to open for access (POSIX file thePath) with write permission
|
|
518
|
+
write pngData to fp
|
|
519
|
+
close access fp
|
|
520
|
+
return "OK"`;
|
|
521
|
+
const ok = await run("osascript", ["-e", script]);
|
|
522
|
+
if (!ok) return null;
|
|
523
|
+
} else if (os === "linux") {
|
|
524
|
+
const ok = await run("bash", ["-c", `xclip -selection clipboard -t image/png -o > "${out}"`]);
|
|
525
|
+
if (!ok) return null;
|
|
526
|
+
} else {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
const st = await stat2(out);
|
|
531
|
+
return st.isFile() && st.size > 0 ? out : null;
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function imageTokens(message) {
|
|
537
|
+
const out = /* @__PURE__ */ new Set();
|
|
538
|
+
const re = /@?([~A-Za-z0-9._\-/]+\.(?:png|jpe?g|gif|webp))\b/gi;
|
|
539
|
+
for (let m = re.exec(message); m; m = re.exec(message)) out.add(m[1]);
|
|
540
|
+
return [...out];
|
|
541
|
+
}
|
|
542
|
+
async function scanImages(message, cwd2) {
|
|
543
|
+
const tokens = imageTokens(message);
|
|
544
|
+
const images = [];
|
|
545
|
+
const skipped = [];
|
|
546
|
+
let total = 0;
|
|
547
|
+
if (wantsClipboard(message)) {
|
|
548
|
+
const clip = await grabClipboardImage();
|
|
549
|
+
if (clip) {
|
|
550
|
+
try {
|
|
551
|
+
const buf = await readFile3(clip);
|
|
552
|
+
if (buf.length <= PER_IMAGE_BYTES) {
|
|
553
|
+
images.push({
|
|
554
|
+
path: "clipboard",
|
|
555
|
+
dataUrl: `data:image/png;base64,${buf.toString("base64")}`,
|
|
556
|
+
mediaType: "image/png",
|
|
557
|
+
bytes: buf.length
|
|
558
|
+
});
|
|
559
|
+
total += buf.length;
|
|
560
|
+
} else {
|
|
561
|
+
skipped.push("clipboard (over the size cap)");
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
skipped.push("clipboard (unreadable)");
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
skipped.push("clipboard (no image found)");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const tok of tokens) {
|
|
571
|
+
const raw = tok.startsWith("~/") ? join3(process.env.HOME ?? "", tok.slice(2)) : tok;
|
|
572
|
+
const abs = isAbsolute2(raw) ? raw : resolve2(cwd2, raw);
|
|
573
|
+
const mediaType = EXT_TO_MEDIA[extname(abs).toLowerCase()];
|
|
574
|
+
if (!mediaType) continue;
|
|
575
|
+
let bytes;
|
|
576
|
+
try {
|
|
577
|
+
const st = await stat2(abs);
|
|
578
|
+
if (!st.isFile()) continue;
|
|
579
|
+
bytes = st.size;
|
|
580
|
+
} catch {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const label = isAbsolute2(raw) ? raw : relative2(cwd2, abs);
|
|
584
|
+
if (bytes > PER_IMAGE_BYTES || total + bytes > TOTAL_BYTES) {
|
|
585
|
+
skipped.push(`${label} (${Math.round(bytes / 1024)}KB \u2014 over the size cap)`);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const buf = await readFile3(abs);
|
|
590
|
+
const dataUrl = `data:${mediaType};base64,${buf.toString("base64")}`;
|
|
591
|
+
images.push({ path: label, dataUrl, mediaType, bytes });
|
|
592
|
+
total += bytes;
|
|
593
|
+
} catch {
|
|
594
|
+
skipped.push(`${label} (unreadable)`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return { images, skipped };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/weburl.ts
|
|
601
|
+
var CHARS_PER_TOKEN2 = 4;
|
|
602
|
+
var DEFAULT_TOKEN_BUDGET2 = 12e3;
|
|
603
|
+
var PER_URL_TOKEN_CAP = 6e3;
|
|
604
|
+
var FETCH_TIMEOUT_MS = 12e3;
|
|
605
|
+
var MAX_BYTES = 4 * 1024 * 1024;
|
|
606
|
+
function estTokens2(s) {
|
|
607
|
+
return Math.ceil(s.length / CHARS_PER_TOKEN2);
|
|
608
|
+
}
|
|
609
|
+
function extractUrls(message) {
|
|
610
|
+
const out = /* @__PURE__ */ new Set();
|
|
611
|
+
const re = /@?(https?:\/\/[^\s<>")]+)/gi;
|
|
612
|
+
for (let m = re.exec(message); m; m = re.exec(message)) {
|
|
613
|
+
out.add(m[1].replace(/[.,;:)\]]+$/, ""));
|
|
614
|
+
}
|
|
615
|
+
return [...out];
|
|
616
|
+
}
|
|
617
|
+
function htmlToText(html) {
|
|
618
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<\/(p|div|li|h[1-6]|tr|br|section|article)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
619
|
+
}
|
|
620
|
+
async function fetchReadable(url) {
|
|
621
|
+
const ctrl = new AbortController();
|
|
622
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
623
|
+
try {
|
|
624
|
+
const res = await fetch(url, {
|
|
625
|
+
signal: ctrl.signal,
|
|
626
|
+
redirect: "follow",
|
|
627
|
+
headers: { "user-agent": "zeta-g-cli/0.4 (+web-context)" }
|
|
628
|
+
});
|
|
629
|
+
if (!res.ok) return null;
|
|
630
|
+
const ctype = res.headers.get("content-type") ?? "";
|
|
631
|
+
const raw = await res.text();
|
|
632
|
+
const body = raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) : raw;
|
|
633
|
+
const text = /html/i.test(ctype) ? htmlToText(body) : body.trim();
|
|
634
|
+
return { text, finalUrl: res.url || url };
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
} finally {
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async function expandUrls(message, tokenBudget = DEFAULT_TOKEN_BUDGET2) {
|
|
642
|
+
const urls = extractUrls(message);
|
|
643
|
+
if (urls.length === 0) return { context: "", notice: null };
|
|
644
|
+
const blocks = [];
|
|
645
|
+
const fetched = [];
|
|
646
|
+
const failed = [];
|
|
647
|
+
let spent = 0;
|
|
648
|
+
for (const url of urls) {
|
|
649
|
+
if (spent >= tokenBudget) break;
|
|
650
|
+
const r = await fetchReadable(url);
|
|
651
|
+
if (!r || !r.text) {
|
|
652
|
+
failed.push(url);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const cap = Math.min(PER_URL_TOKEN_CAP, tokenBudget - spent);
|
|
656
|
+
let body = r.text;
|
|
657
|
+
let truncated = false;
|
|
658
|
+
if (estTokens2(body) > cap) {
|
|
659
|
+
body = body.slice(0, cap * CHARS_PER_TOKEN2);
|
|
660
|
+
truncated = true;
|
|
661
|
+
}
|
|
662
|
+
const header = truncated ? `${r.finalUrl} (truncated to ~${cap} tokens)` : r.finalUrl;
|
|
663
|
+
blocks.push(`${header}
|
|
664
|
+
${body}`);
|
|
665
|
+
spent += estTokens2(body);
|
|
666
|
+
fetched.push(r.finalUrl);
|
|
667
|
+
}
|
|
668
|
+
if (blocks.length === 0) {
|
|
669
|
+
return { context: "", notice: failed.length ? `couldn't fetch: ${failed.join(", ")}` : null };
|
|
670
|
+
}
|
|
671
|
+
const context = "The user linked these pages. Use them as context for this turn:\n\n" + blocks.join("\n\n---\n\n");
|
|
672
|
+
const noticeParts = [`fetched ${fetched.length} URL(s) (~${spent} tokens)`];
|
|
673
|
+
if (failed.length) noticeParts.push(`failed: ${failed.length}`);
|
|
674
|
+
return { context, notice: noticeParts.join(" \xB7 ") };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/export.ts
|
|
678
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
679
|
+
import { resolve as resolve3 } from "path";
|
|
680
|
+
function renderContent(content) {
|
|
681
|
+
if (typeof content === "string") return content.trim();
|
|
682
|
+
if (!Array.isArray(content)) return "";
|
|
683
|
+
const parts = [];
|
|
684
|
+
for (const p of content) {
|
|
685
|
+
const type = p.type;
|
|
686
|
+
if (type === "text" && typeof p.text === "string") parts.push(p.text.trim());
|
|
687
|
+
else if (type === "image") parts.push("_[image]_");
|
|
688
|
+
else if (type === "tool-call") parts.push(`\`\u2192 ${String(p.toolName ?? "tool")}\``);
|
|
689
|
+
else if (type === "tool-result") parts.push(`\`\u2713 ${String(p.toolName ?? "tool")}\``);
|
|
690
|
+
}
|
|
691
|
+
return parts.filter(Boolean).join("\n\n");
|
|
692
|
+
}
|
|
693
|
+
function renderTranscript(messages, meta) {
|
|
694
|
+
const head = [
|
|
695
|
+
`# zeta-g transcript`,
|
|
696
|
+
``,
|
|
697
|
+
`- model: ${meta.model}`,
|
|
698
|
+
meta.startedAt ? `- started: ${meta.startedAt}` : "",
|
|
699
|
+
`- messages: ${messages.length}`,
|
|
700
|
+
``,
|
|
701
|
+
`---`,
|
|
702
|
+
``
|
|
703
|
+
].filter((l) => l !== "").join("\n");
|
|
704
|
+
const body = messages.map((m) => {
|
|
705
|
+
const text = renderContent(m.content);
|
|
706
|
+
if (!text) return "";
|
|
707
|
+
const who = m.role === "user" ? "\u{1F9D1} User" : m.role === "assistant" ? "\u{1F916} Zeta-G" : m.role === "tool" ? "\u{1F527} Tool" : m.role;
|
|
708
|
+
return `### ${who}
|
|
709
|
+
|
|
710
|
+
${text}`;
|
|
711
|
+
}).filter(Boolean).join("\n\n");
|
|
712
|
+
return `${head}
|
|
713
|
+
|
|
714
|
+
${body}
|
|
715
|
+
`;
|
|
716
|
+
}
|
|
717
|
+
function writeTranscript(messages, meta, file, cwd2) {
|
|
718
|
+
const abs = resolve3(cwd2, file);
|
|
719
|
+
writeFileSync2(abs, renderTranscript(messages, meta), "utf8");
|
|
720
|
+
return abs;
|
|
721
|
+
}
|
|
722
|
+
|
|
269
723
|
// src/cli.ts
|
|
724
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
725
|
+
import { homedir as homedir4 } from "os";
|
|
726
|
+
import { join as join6 } from "path";
|
|
727
|
+
|
|
728
|
+
// src/update-check.ts
|
|
729
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
730
|
+
import { homedir as homedir2 } from "os";
|
|
731
|
+
import { join as join4 } from "path";
|
|
732
|
+
var PKG = "wholestack";
|
|
733
|
+
var CACHE = join4(homedir2(), ".zeta-g", "update-check.json");
|
|
734
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
735
|
+
function readCache() {
|
|
736
|
+
try {
|
|
737
|
+
return JSON.parse(readFileSync2(CACHE, "utf8"));
|
|
738
|
+
} catch {
|
|
739
|
+
return { lastCheck: 0, latest: null };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function writeCache(c2) {
|
|
743
|
+
try {
|
|
744
|
+
mkdirSync2(join4(homedir2(), ".zeta-g"), { recursive: true });
|
|
745
|
+
writeFileSync3(CACHE, JSON.stringify(c2));
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function isNewer(a, b) {
|
|
750
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
751
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
752
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
753
|
+
const x = pa[i] ?? 0;
|
|
754
|
+
const y = pb[i] ?? 0;
|
|
755
|
+
if (x !== y) return x > y;
|
|
756
|
+
}
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
async function checkForUpdate(current) {
|
|
760
|
+
if (process.env.ZETA_NO_UPDATE_CHECK || !existsSync2) return null;
|
|
761
|
+
const cache = readCache();
|
|
762
|
+
const fresh = Date.now() - cache.lastCheck < DAY_MS;
|
|
763
|
+
let latest = cache.latest;
|
|
764
|
+
if (!fresh) {
|
|
765
|
+
try {
|
|
766
|
+
const ctrl = new AbortController();
|
|
767
|
+
const t = setTimeout(() => ctrl.abort(), 1200);
|
|
768
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
|
|
769
|
+
signal: ctrl.signal,
|
|
770
|
+
headers: { accept: "application/json" }
|
|
771
|
+
});
|
|
772
|
+
clearTimeout(t);
|
|
773
|
+
if (res.ok) {
|
|
774
|
+
const json = await res.json();
|
|
775
|
+
latest = json.version ?? null;
|
|
776
|
+
}
|
|
777
|
+
} catch {
|
|
778
|
+
latest = cache.latest;
|
|
779
|
+
}
|
|
780
|
+
writeCache({ lastCheck: Date.now(), latest });
|
|
781
|
+
}
|
|
782
|
+
return latest && isNewer(latest, current) ? latest : null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/access.ts
|
|
786
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
787
|
+
import { homedir as homedir3 } from "os";
|
|
788
|
+
import { join as join5 } from "path";
|
|
789
|
+
var CACHE2 = join5(homedir3(), ".zeta-g", "access.json");
|
|
790
|
+
var TTL_MS = 60 * 60 * 1e3;
|
|
791
|
+
function readCache2() {
|
|
792
|
+
try {
|
|
793
|
+
return JSON.parse(readFileSync3(CACHE2, "utf8"));
|
|
794
|
+
} catch {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function writeCache2(c2) {
|
|
799
|
+
try {
|
|
800
|
+
mkdirSync3(join5(homedir3(), ".zeta-g"), { recursive: true });
|
|
801
|
+
writeFileSync4(CACHE2, JSON.stringify(c2));
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function clearAccessCache() {
|
|
806
|
+
try {
|
|
807
|
+
rmSync(CACHE2, { force: true });
|
|
808
|
+
} catch {
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async function verifyAccess(webUrl, token, force = false) {
|
|
812
|
+
if (!token) return { active: false, tier: null, degraded: false };
|
|
813
|
+
const cached = force ? null : readCache2();
|
|
814
|
+
if (cached && Date.now() - cached.checkedAt < TTL_MS) {
|
|
815
|
+
return { active: cached.active, tier: cached.tier, degraded: false };
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const ctrl = new AbortController();
|
|
819
|
+
const t = setTimeout(() => ctrl.abort(), 2500);
|
|
820
|
+
const res = await fetch(`${webUrl.replace(/\/$/, "")}/api/cli/verify`, {
|
|
821
|
+
headers: { authorization: `Bearer ${token}` },
|
|
822
|
+
signal: ctrl.signal
|
|
823
|
+
});
|
|
824
|
+
clearTimeout(t);
|
|
825
|
+
if (res.status >= 500) {
|
|
826
|
+
return { active: true, tier: cached?.tier ?? null, degraded: true };
|
|
827
|
+
}
|
|
828
|
+
if (!res.ok) {
|
|
829
|
+
writeCache2({ checkedAt: Date.now(), active: false, tier: null });
|
|
830
|
+
return { active: false, tier: null, degraded: false };
|
|
831
|
+
}
|
|
832
|
+
const json = await res.json();
|
|
833
|
+
const active = Boolean(json.active);
|
|
834
|
+
writeCache2({ checkedAt: Date.now(), active, tier: json.tier ?? null });
|
|
835
|
+
return { active, tier: json.tier ?? null, degraded: false };
|
|
836
|
+
} catch {
|
|
837
|
+
return { active: true, tier: cached?.tier ?? null, degraded: true };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function showPaywall(loggedIn, webUrl) {
|
|
841
|
+
const w = 52;
|
|
842
|
+
line();
|
|
843
|
+
line(" " + c.cyan("\u256D" + "\u2500".repeat(w) + "\u256E"));
|
|
844
|
+
const row = (s) => line(" " + c.cyan("\u2502") + " " + s.padEnd(w - 2) + " " + c.cyan("\u2502"));
|
|
845
|
+
row(c.bold("zeta needs a subscription"));
|
|
846
|
+
row("");
|
|
847
|
+
if (!loggedIn) {
|
|
848
|
+
row(c.dim("You're not signed in."));
|
|
849
|
+
row("Run " + c.cyan("zeta login") + c.dim(" then subscribe."));
|
|
850
|
+
} else {
|
|
851
|
+
row(c.dim("No active paid plan on this account."));
|
|
852
|
+
row("Subscribe to use zeta in the terminal.");
|
|
853
|
+
}
|
|
854
|
+
row("");
|
|
855
|
+
row(c.dim("Plans: ") + c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
|
|
856
|
+
line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
|
|
857
|
+
line();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/art.ts
|
|
861
|
+
var ART_PACK = [
|
|
862
|
+
[" \u2588\u2588\u2588\u2588\u2588\u2588\u2588", " \u2588\u2588\u2554\u255D", " \u2588\u2588\u2554\u255D ", " \u2588\u2588\u2554\u255D ", " \u2588\u2588\u2588\u2588\u2588\u2588\u2588 "],
|
|
863
|
+
[" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", " \u2502 \u2571\u2571 \u2502", " \u2502 \u2571\u2571 \u2502", " \u2502\u2571\u2571 \u2502", " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"],
|
|
864
|
+
[" \u259F\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2599", " \u259F\u2588\u259B ", " \u259F\u2588\u259B ", " \u259F\u2588\u259B ", " \u259C\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259B "],
|
|
865
|
+
[" \u250C\u2500\u2510\u250C\u2500\u2510\u250C\u252C\u2510\u250C\u2500\u2510", " \u250C\u2500\u2518\u251C\u2524 \u2502 \u251C\u2500\u2524", " \u2514\u2500\u2518\u2514\u2500\u2518 \u2534 \u2534 \u2534"],
|
|
866
|
+
[" \u2571\u2572 ", " \u2571\u2500\u2500\u2572 ", " \u2571 \u2571\u2571 \u2572 ", "\u2571 \u2571\u2571 \u2572 ", "\u2572\u2571\u2571\u2500\u2500\u2500\u2500\u2500\u2572"]
|
|
867
|
+
];
|
|
868
|
+
function randomArt(seed) {
|
|
869
|
+
const i = seed != null ? seed % ART_PACK.length : Math.floor(Math.random() * ART_PACK.length);
|
|
870
|
+
return ART_PACK[(i % ART_PACK.length + ART_PACK.length) % ART_PACK.length];
|
|
871
|
+
}
|
|
872
|
+
var vlen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
873
|
+
function fetchPanel(fields, art = randomArt()) {
|
|
874
|
+
const shown = fields.filter((f) => f.value);
|
|
875
|
+
const labelW = Math.max(0, ...shown.map((f) => f.label.length));
|
|
876
|
+
const artW = Math.max(0, ...art.map(vlen));
|
|
877
|
+
const rows = Math.max(art.length, shown.length);
|
|
878
|
+
const out = [];
|
|
879
|
+
out.push("");
|
|
880
|
+
for (let i = 0; i < rows; i++) {
|
|
881
|
+
const left = (art[i] ?? "").padEnd(artW + 2);
|
|
882
|
+
const f = shown[i];
|
|
883
|
+
const right = f ? c.dim(f.label.padStart(labelW) + " ") + c.bold(f.value) : "";
|
|
884
|
+
out.push(" " + c.cyan(left) + right);
|
|
885
|
+
}
|
|
886
|
+
out.push("");
|
|
887
|
+
for (const l of out) process.stdout.write(l + "\n");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/cli.ts
|
|
891
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
892
|
+
import { basename } from "path";
|
|
893
|
+
var VERSION = "0.5.0";
|
|
894
|
+
function gitBranch(dir) {
|
|
895
|
+
try {
|
|
896
|
+
const head = readFileSync4(join6(dir, ".git", "HEAD"), "utf8").trim();
|
|
897
|
+
const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
|
|
898
|
+
return m ? m[1] : head.slice(0, 7);
|
|
899
|
+
} catch {
|
|
900
|
+
return "";
|
|
901
|
+
}
|
|
902
|
+
}
|
|
270
903
|
function parse(raw) {
|
|
271
904
|
const a = {
|
|
272
905
|
zetaUrl: process.env.ZETA_API_URL ?? "https://wholestack.ai",
|
|
@@ -294,6 +927,8 @@ function parse(raw) {
|
|
|
294
927
|
const m = raw[++i];
|
|
295
928
|
if (isPermissionMode(m)) a.mode = m;
|
|
296
929
|
} else if (t === "--plan") a.plan = true;
|
|
930
|
+
else if (t === "--persona") a.persona = raw[++i];
|
|
931
|
+
else if (t === "--nzt48" || t === "--nzt-48") a.persona = "nzt-48";
|
|
297
932
|
else if (t === "--think") a.think = true;
|
|
298
933
|
else if (t === "--no-think") a.think = false;
|
|
299
934
|
else if (t === "--mcp") a.mcp = (raw[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -324,12 +959,15 @@ ${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmo
|
|
|
324
959
|
zeta-g prove <dir> <Contract> --property <k> prove one invariant
|
|
325
960
|
zeta-g verify <dir> <Contract> --cert c.json passthrough + signed certificate
|
|
326
961
|
${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
|
|
962
|
+
zeta-g --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
|
|
327
963
|
|
|
328
964
|
${c.bold("Session")}
|
|
329
|
-
-m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max
|
|
330
|
-
(lite = light \xB7 g1 = default \xB7 max = most capable)
|
|
965
|
+
-m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
|
|
966
|
+
(lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
|
|
331
967
|
--mode <m> default | acceptEdits | plan | yolo (permission mode)
|
|
332
968
|
--plan start in read-only plan mode
|
|
969
|
+
--nzt48 run as NZT-48, the web3 special-ops persona
|
|
970
|
+
--persona <id> layer a named persona on the base agent
|
|
333
971
|
--think/--no-think toggle extended thinking (when a tier supports it)
|
|
334
972
|
--continue resume the most recent session
|
|
335
973
|
--resume [id] resume a session (id, or the latest)
|
|
@@ -352,7 +990,14 @@ ${c.bold("Auth")} ${c.dim("(set once, then just run zeta-g)")}
|
|
|
352
990
|
zeta-g login --build <key> ZETA build-engine key (for /build)
|
|
353
991
|
|
|
354
992
|
${c.bold("In a session")} ${c.dim("(type /help for the full list)")}
|
|
355
|
-
/model /cost /tools /undo /redo /checkpoints /compact /resume /memory /mcp /mode /think /init /doctor /clear /exit
|
|
993
|
+
/model /cost /tools /undo /redo /checkpoints /compact /resume /memory /mcp /mode /think /init /doctor /clear /exit
|
|
994
|
+
/diff /commit /pr ${c.dim("git flow \u2014 review, commit (auto-written msg), open a PR")}
|
|
995
|
+
/tasks ${c.dim("background jobs \u2014 run_background runs builds/tests while you work")}
|
|
996
|
+
/export [file.md] ${c.dim("write the conversation transcript to markdown")}
|
|
997
|
+
${c.dim("@path \xB7 @dir/ \xB7 @glob attach files to the turn (token-budgeted, Tab-completes)")}
|
|
998
|
+
${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
|
|
999
|
+
${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
|
|
1000
|
+
${c.dim("spawn_agents tool fan a task out to parallel read-only sub-agents")}`;
|
|
356
1001
|
async function runSecuritySubcommand(raw) {
|
|
357
1002
|
const verb = raw[0];
|
|
358
1003
|
if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
|
|
@@ -375,6 +1020,19 @@ async function runSecuritySubcommand(raw) {
|
|
|
375
1020
|
line(r.ok ? c.green(" \u2713 verified") : c.red(" \u2717 NOT verified"));
|
|
376
1021
|
return r.code;
|
|
377
1022
|
}
|
|
1023
|
+
function maybeYoloNotice() {
|
|
1024
|
+
try {
|
|
1025
|
+
const dir = join6(homedir4(), ".zeta-g");
|
|
1026
|
+
const marker = join6(dir, "yolo-notice-seen");
|
|
1027
|
+
if (existsSync3(marker)) return;
|
|
1028
|
+
line(
|
|
1029
|
+
" " + 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"
|
|
1030
|
+
);
|
|
1031
|
+
mkdirSync4(dir, { recursive: true });
|
|
1032
|
+
writeFileSync5(marker, (/* @__PURE__ */ new Date()).toISOString());
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
378
1036
|
function webBaseUrl() {
|
|
379
1037
|
return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
|
|
380
1038
|
}
|
|
@@ -382,6 +1040,7 @@ async function runLogin(raw) {
|
|
|
382
1040
|
if (raw[0] !== "login" && raw[0] !== "logout") return null;
|
|
383
1041
|
const rest = raw.slice(1);
|
|
384
1042
|
if (raw[0] === "logout") {
|
|
1043
|
+
clearAccessCache();
|
|
385
1044
|
saveKey("ZETA_API_KEY", "");
|
|
386
1045
|
line(c.green(" \u2713 logged out") + c.dim(" (token cleared)"));
|
|
387
1046
|
return 0;
|
|
@@ -393,7 +1052,9 @@ async function runLogin(raw) {
|
|
|
393
1052
|
line(c.red(" usage: zeta login --token <vbfl_\u2026>"));
|
|
394
1053
|
return 1;
|
|
395
1054
|
}
|
|
396
|
-
|
|
1055
|
+
const ok = loginWithToken(tok);
|
|
1056
|
+
if (ok) clearAccessCache();
|
|
1057
|
+
return ok ? 0 : 1;
|
|
397
1058
|
}
|
|
398
1059
|
const pick = (flag) => {
|
|
399
1060
|
const i = rest.indexOf(flag);
|
|
@@ -408,6 +1069,7 @@ async function runLogin(raw) {
|
|
|
408
1069
|
if (!hit(memberFlags) && !hit(brainFlags) && !hit(buildFlags)) {
|
|
409
1070
|
const noBrowser = rest.includes("--no-browser");
|
|
410
1071
|
const token = await loginBrowser({ webBase: webBaseUrl(), noBrowser });
|
|
1072
|
+
if (token) clearAccessCache();
|
|
411
1073
|
return token ? 0 : 1;
|
|
412
1074
|
}
|
|
413
1075
|
if (hit(memberFlags)) {
|
|
@@ -439,6 +1101,7 @@ async function runLogin(raw) {
|
|
|
439
1101
|
}
|
|
440
1102
|
const what = name === "ZETA_API_KEY" ? "membership (keep + deploy your apps)" : name === "OPENROUTER_API_KEY" ? "brain (Zeta-G1.0 tiers)" : "ZETA build engine";
|
|
441
1103
|
const path = saveKey(name, value);
|
|
1104
|
+
if (name === "ZETA_API_KEY") clearAccessCache();
|
|
442
1105
|
line(c.green(` \u2713 saved key for the ${what}`) + c.dim(` \u2192 ${path} (chmod 600)`));
|
|
443
1106
|
line(c.dim(" now just run `zeta-g`."));
|
|
444
1107
|
return 0;
|
|
@@ -473,13 +1136,36 @@ async function main() {
|
|
|
473
1136
|
if (sub !== null) exit(sub);
|
|
474
1137
|
const args = parse(rawArgs);
|
|
475
1138
|
if (args.version) {
|
|
476
|
-
line(
|
|
1139
|
+
line(`wholestack ${VERSION}`);
|
|
477
1140
|
return;
|
|
478
1141
|
}
|
|
479
1142
|
if (args.help) {
|
|
480
1143
|
line(HELP);
|
|
481
1144
|
return;
|
|
482
1145
|
}
|
|
1146
|
+
{
|
|
1147
|
+
let token = process.env.ZETA_API_KEY;
|
|
1148
|
+
let access = await verifyAccess(webBaseUrl(), token);
|
|
1149
|
+
if (!access.active && stdin.isTTY) {
|
|
1150
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1151
|
+
const ans = (await rl.question(
|
|
1152
|
+
"\n " + (token ? c.dim("No active plan on this account. ") + "Log in to a different account? " : "Log in now? ") + c.dim("[Y/n] ")
|
|
1153
|
+
)).trim().toLowerCase();
|
|
1154
|
+
rl.close();
|
|
1155
|
+
if (ans === "" || ans === "y" || ans === "yes") {
|
|
1156
|
+
const minted = await loginBrowser({ webBase: webBaseUrl(), noBrowser: false });
|
|
1157
|
+
if (minted) {
|
|
1158
|
+
token = minted;
|
|
1159
|
+
process.env.ZETA_API_KEY = minted;
|
|
1160
|
+
access = await verifyAccess(webBaseUrl(), token, true);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (!access.active) {
|
|
1165
|
+
showPaywall(Boolean(token), webBaseUrl());
|
|
1166
|
+
exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
483
1169
|
let modelKey;
|
|
484
1170
|
try {
|
|
485
1171
|
modelKey = resolveModelKey(args.model);
|
|
@@ -497,7 +1183,11 @@ async function main() {
|
|
|
497
1183
|
const isTty = !!stdin.isTTY;
|
|
498
1184
|
const oneShot = args.prompt.length > 0;
|
|
499
1185
|
const workdir = cwd();
|
|
500
|
-
const
|
|
1186
|
+
const persistedMode = isPermissionMode(
|
|
1187
|
+
process.env.ZETA_PERMISSION_MODE
|
|
1188
|
+
) ? process.env.ZETA_PERMISSION_MODE : void 0;
|
|
1189
|
+
const yoloIsDefault = !args.plan && !args.mode && !args.yes && !persistedMode && isTty;
|
|
1190
|
+
const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
|
|
501
1191
|
const memory = await loadProjectMemory(workdir);
|
|
502
1192
|
const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
|
|
503
1193
|
const memoryText = [memory.text, plugins.systemText].filter(Boolean).join("\n\n") || void 0;
|
|
@@ -507,7 +1197,6 @@ async function main() {
|
|
|
507
1197
|
disabled: args.noMcp,
|
|
508
1198
|
extraServers: plugins.mcpServers
|
|
509
1199
|
});
|
|
510
|
-
const extraTools = { ...buildWebTools(), ...mcp.tools };
|
|
511
1200
|
const hooks = new HookRunner(mergeHookSets(loadHookFiles(workdir), plugins.hooks), workdir);
|
|
512
1201
|
const registry = new CommandRegistry();
|
|
513
1202
|
registry.loadCustom(workdir);
|
|
@@ -518,6 +1207,7 @@ async function main() {
|
|
|
518
1207
|
if (shuttingDown) return;
|
|
519
1208
|
shuttingDown = true;
|
|
520
1209
|
killRunningApps();
|
|
1210
|
+
tasks.killAll();
|
|
521
1211
|
await mcp.close().catch(() => {
|
|
522
1212
|
});
|
|
523
1213
|
ic?.close();
|
|
@@ -529,12 +1219,33 @@ async function main() {
|
|
|
529
1219
|
onExit: () => {
|
|
530
1220
|
line(c.dim(" bye."));
|
|
531
1221
|
void shutdown(0);
|
|
1222
|
+
},
|
|
1223
|
+
// Starship-style context segments above the prompt box.
|
|
1224
|
+
contextBar: () => {
|
|
1225
|
+
const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename(workdir))];
|
|
1226
|
+
const br = gitBranch(workdir);
|
|
1227
|
+
if (br) seg.push(c.dim("\u2387 " + br));
|
|
1228
|
+
seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
|
|
1229
|
+
return seg.join(c.dim(" \xB7 "));
|
|
532
1230
|
}
|
|
533
1231
|
});
|
|
534
1232
|
}
|
|
535
1233
|
const confirm = isTty && ic ? ic.confirm.bind(ic) : void 0;
|
|
536
|
-
const
|
|
1234
|
+
const persistMode = (m) => {
|
|
1235
|
+
try {
|
|
1236
|
+
saveKey("ZETA_PERMISSION_MODE", m);
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
const permissions = new Permissions(mode, confirm, persistMode);
|
|
537
1241
|
const checkpoints = new CheckpointStore();
|
|
1242
|
+
const toolCtx = { cwd: workdir, zetaApiUrl: args.zetaUrl, buildMode: args.buildMode, permissions, checkpoints };
|
|
1243
|
+
const extraTools = {
|
|
1244
|
+
...buildWebTools(),
|
|
1245
|
+
...mcp.tools,
|
|
1246
|
+
// Parallel read-only sub-agents (needs the model handle the base tools lack).
|
|
1247
|
+
...buildSubagentTool({ model, modelKey, ctx: toolCtx })
|
|
1248
|
+
};
|
|
538
1249
|
let session;
|
|
539
1250
|
let resumedMessages = null;
|
|
540
1251
|
if (args.resume !== void 0 || args.cont) {
|
|
@@ -554,19 +1265,27 @@ async function main() {
|
|
|
554
1265
|
const agent = new Agent({
|
|
555
1266
|
model,
|
|
556
1267
|
modelKey,
|
|
557
|
-
ctx:
|
|
1268
|
+
ctx: toolCtx,
|
|
558
1269
|
extraTools,
|
|
559
1270
|
hooks,
|
|
560
1271
|
memoryText,
|
|
1272
|
+
persona: args.persona,
|
|
561
1273
|
thinking,
|
|
1274
|
+
yolo: mode === "yolo",
|
|
562
1275
|
maxSteps: 16,
|
|
563
1276
|
contextWindow: modelContextWindow(modelKey),
|
|
564
1277
|
session
|
|
565
1278
|
});
|
|
566
1279
|
if (resumedMessages) agent.replaceHistory(resumedMessages);
|
|
567
1280
|
if (oneShot) {
|
|
568
|
-
|
|
569
|
-
|
|
1281
|
+
const m = await expandMentions(args.prompt, workdir);
|
|
1282
|
+
if (m.notice) line(c.dim(` @ ${m.notice}`) + "\n");
|
|
1283
|
+
const oneShotText = m.context ? `${args.prompt}
|
|
1284
|
+
|
|
1285
|
+
---
|
|
1286
|
+
${m.context}` : args.prompt;
|
|
1287
|
+
if (ic) await ic.runInterruptible((sig) => agent.send(oneShotText, sig));
|
|
1288
|
+
else await agent.send(oneShotText);
|
|
570
1289
|
await shutdown(0);
|
|
571
1290
|
return;
|
|
572
1291
|
}
|
|
@@ -575,10 +1294,28 @@ async function main() {
|
|
|
575
1294
|
await shutdown(1);
|
|
576
1295
|
return;
|
|
577
1296
|
}
|
|
578
|
-
banner();
|
|
1297
|
+
await banner();
|
|
1298
|
+
fetchPanel([
|
|
1299
|
+
{ label: "model", value: modelLabel(modelKey) },
|
|
1300
|
+
{ label: "mode", value: mode },
|
|
1301
|
+
{ label: "dir", value: basename(workdir) },
|
|
1302
|
+
{ label: "git", value: gitBranch(workdir) },
|
|
1303
|
+
{ label: "node", value: process.version },
|
|
1304
|
+
{ label: "memory", value: memory.sources.length ? `${memory.sources.length} file(s)` : "" },
|
|
1305
|
+
{ label: "mcp", value: mcp.mounted.length ? `${mcp.mounted.length} mounted` : "" },
|
|
1306
|
+
{ label: "zeta", value: VERSION }
|
|
1307
|
+
]);
|
|
579
1308
|
bootSummary(memory, plugins, mcp, mode, thinking);
|
|
580
1309
|
if (mode === "plan") announcePlanMode();
|
|
1310
|
+
else if (yoloIsDefault) maybeYoloNotice();
|
|
581
1311
|
if (resumedMessages) line(" " + c.dim(`resumed ${session.meta.id} \xB7 ${resumedMessages.length} messages`) + "\n");
|
|
1312
|
+
void checkForUpdate(VERSION).then((latest) => {
|
|
1313
|
+
if (latest) {
|
|
1314
|
+
line(
|
|
1315
|
+
" " + c.yellow(`\u2191 wholestack ${latest} available`) + c.dim(` (you have ${VERSION}) \xB7 `) + c.cyan("npm i -g wholestack") + "\n"
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
582
1319
|
const baseCtx = () => ({
|
|
583
1320
|
cwd: workdir,
|
|
584
1321
|
modelKey,
|
|
@@ -593,9 +1330,9 @@ async function main() {
|
|
|
593
1330
|
checkpoints,
|
|
594
1331
|
print: (s = "") => line(s)
|
|
595
1332
|
});
|
|
596
|
-
const runTurn = async (text) => {
|
|
1333
|
+
const runTurn = async (text, images) => {
|
|
597
1334
|
const t0 = Date.now();
|
|
598
|
-
await ic.runInterruptible((sig) => agent.send(text, sig));
|
|
1335
|
+
await ic.runInterruptible((sig) => agent.send(text, sig, images));
|
|
599
1336
|
statusLine({
|
|
600
1337
|
model: modelLabel(modelKey),
|
|
601
1338
|
tokens: agent.usage.totalTokens,
|
|
@@ -607,6 +1344,11 @@ async function main() {
|
|
|
607
1344
|
});
|
|
608
1345
|
};
|
|
609
1346
|
for (; ; ) {
|
|
1347
|
+
for (const done of tasks.drainCompleted()) {
|
|
1348
|
+
const mark = done.status === "exited" ? c.green("\u2713") : c.red("\u2717");
|
|
1349
|
+
const code = done.exitCode == null ? "" : ` (exit ${done.exitCode})`;
|
|
1350
|
+
line(` ${mark} ${c.dim("background")} ${c.cyan(done.id)} ${done.display}${c.dim(code)}`);
|
|
1351
|
+
}
|
|
610
1352
|
const input = await ic.readLine(c.cyan(" \u203A "));
|
|
611
1353
|
if (!input) continue;
|
|
612
1354
|
if (input.startsWith("/")) {
|
|
@@ -636,7 +1378,12 @@ async function main() {
|
|
|
636
1378
|
}
|
|
637
1379
|
} else if (res.type === "setMode") {
|
|
638
1380
|
permissions.setMode(res.mode);
|
|
639
|
-
|
|
1381
|
+
if (res.mode !== "plan") {
|
|
1382
|
+
persistMode(res.mode);
|
|
1383
|
+
line(" " + c.dim(`mode: ${res.mode} \xB7 remembered for next session`) + "\n");
|
|
1384
|
+
} else {
|
|
1385
|
+
line(" " + c.dim(`mode: ${res.mode}`) + "\n");
|
|
1386
|
+
}
|
|
640
1387
|
if (res.mode === "plan") announcePlanMode();
|
|
641
1388
|
} else if (res.type === "toggleThinking") {
|
|
642
1389
|
if (supportsThinking(modelKey)) {
|
|
@@ -655,13 +1402,54 @@ async function main() {
|
|
|
655
1402
|
} else {
|
|
656
1403
|
line(" " + c.red(`no session ${res.sessionId}`) + "\n");
|
|
657
1404
|
}
|
|
1405
|
+
} else if (res.type === "export") {
|
|
1406
|
+
const file = res.file ?? `zeta-transcript-${gitBranch(workdir) || "session"}.md`;
|
|
1407
|
+
try {
|
|
1408
|
+
const abs = writeTranscript(agent.snapshot(), { model: modelLabel(modelKey) }, file, workdir);
|
|
1409
|
+
line(" " + c.green(`\u2713 transcript \u2192 ${abs}`) + "\n");
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
line(" " + c.red(e.message) + "\n");
|
|
1412
|
+
}
|
|
658
1413
|
}
|
|
659
1414
|
continue;
|
|
660
1415
|
}
|
|
661
|
-
userBox(input);
|
|
662
|
-
await
|
|
1416
|
+
if (!process.stdin.isTTY || process.env.NO_COLOR) userBox(input);
|
|
1417
|
+
const mentioned = await expandMentions(input, workdir);
|
|
1418
|
+
if (mentioned.notice) line(c.dim(` @ ${mentioned.notice}`) + "\n");
|
|
1419
|
+
const web = await expandUrls(input);
|
|
1420
|
+
if (web.notice) line(c.dim(` \u2301 ${web.notice}`) + "\n");
|
|
1421
|
+
const extraContext = [mentioned.context, web.context].filter(Boolean).join("\n\n---\n\n");
|
|
1422
|
+
const imgs = await scanImages(input, workdir);
|
|
1423
|
+
let attachments;
|
|
1424
|
+
if (imgs.images.length > 0) {
|
|
1425
|
+
if (!visionCapable(modelKey) && process.env.OPENROUTER_API_KEY) {
|
|
1426
|
+
try {
|
|
1427
|
+
const vkey = resolveModelKey("vision");
|
|
1428
|
+
modelKey = vkey;
|
|
1429
|
+
agent.setModel(resolveModel(vkey), vkey, modelContextWindow(vkey));
|
|
1430
|
+
agent.setThinking(false);
|
|
1431
|
+
line(c.dim(` \u29C9 image attached \u2192 switched to ${modelLabel(vkey)} (/model zeta-g1 to switch back)`) + "\n");
|
|
1432
|
+
} catch (e) {
|
|
1433
|
+
line(c.red(` \u29C9 couldn't switch to the vision lane: ${e.message}`) + "\n");
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (visionCapable(modelKey)) {
|
|
1437
|
+
attachments = imgs.images.map((i) => ({ dataUrl: i.dataUrl }));
|
|
1438
|
+
line(c.dim(` \u29C9 attached ${imgs.images.length} image(s): ${imgs.images.map((i) => i.path).join(", ")}`) + "\n");
|
|
1439
|
+
} else {
|
|
1440
|
+
line(
|
|
1441
|
+
c.yellow(` \u29C9 ${imgs.images.length} image(s) ignored \u2014 ${modelLabel(modelKey)} is text-only.`) + c.dim(" set OPENROUTER_API_KEY (or /model vision) to enable Zeta-G1.0 Vision") + "\n"
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (imgs.skipped.length) line(c.dim(` \u29C9 skipped: ${imgs.skipped.join(", ")}`) + "\n");
|
|
1446
|
+
await runTurn(extraContext ? `${input}
|
|
1447
|
+
|
|
1448
|
+
---
|
|
1449
|
+
${extraContext}` : input, attachments);
|
|
663
1450
|
}
|
|
664
1451
|
killRunningApps();
|
|
1452
|
+
tasks.killAll();
|
|
665
1453
|
await mcp.close().catch(() => {
|
|
666
1454
|
});
|
|
667
1455
|
ic.close();
|