wholestack 0.4.0 → 0.5.0
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-PGKDYDAR.js} +1087 -47
- package/dist/cli.js +766 -20
- package/dist/index.d.ts +101 -5
- package/dist/index.js +1 -1
- package/package.json +1 -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-PGKDYDAR.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,7 +133,7 @@ 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");
|
|
@@ -266,7 +269,615 @@ var CheckpointStore = class {
|
|
|
266
269
|
}
|
|
267
270
|
};
|
|
268
271
|
|
|
272
|
+
// src/mentions.ts
|
|
273
|
+
import { readFile as readFile2, stat, readdir } from "fs/promises";
|
|
274
|
+
import { resolve, relative, join as join2, isAbsolute } from "path";
|
|
275
|
+
import fg from "fast-glob";
|
|
276
|
+
var IGNORE = [
|
|
277
|
+
"**/node_modules/**",
|
|
278
|
+
"**/.git/**",
|
|
279
|
+
"**/dist/**",
|
|
280
|
+
"**/.next/**",
|
|
281
|
+
"**/build/**",
|
|
282
|
+
"**/.turbo/**"
|
|
283
|
+
];
|
|
284
|
+
var CHARS_PER_TOKEN = 4;
|
|
285
|
+
var DEFAULT_TOKEN_BUDGET = 25e3;
|
|
286
|
+
var PER_FILE_TOKEN_CAP = 8e3;
|
|
287
|
+
var MAX_FILES_PER_MENTION = 60;
|
|
288
|
+
var MAX_TREE_ENTRIES = 200;
|
|
289
|
+
function extractMentions(line2) {
|
|
290
|
+
const out = [];
|
|
291
|
+
const re = /(^|\s)@([~A-Za-z0-9._\-/*]+)/g;
|
|
292
|
+
for (let m = re.exec(line2); m; m = re.exec(line2)) {
|
|
293
|
+
let p = m[2];
|
|
294
|
+
p = p.replace(/[.,;:)]+$/g, (tail) => tail.length ? "" : tail);
|
|
295
|
+
if (p) out.push(p);
|
|
296
|
+
}
|
|
297
|
+
return [...new Set(out)];
|
|
298
|
+
}
|
|
299
|
+
function estTokens(s) {
|
|
300
|
+
return Math.ceil(s.length / CHARS_PER_TOKEN);
|
|
301
|
+
}
|
|
302
|
+
function looksBinary(buf) {
|
|
303
|
+
if (buf.includes("\0")) return true;
|
|
304
|
+
let ctrl = 0;
|
|
305
|
+
const n = Math.min(buf.length, 1e3);
|
|
306
|
+
for (let i = 0; i < n; i++) {
|
|
307
|
+
const code = buf.charCodeAt(i);
|
|
308
|
+
if (code < 9 || code > 13 && code < 32) ctrl++;
|
|
309
|
+
}
|
|
310
|
+
return ctrl / Math.max(1, n) > 0.3;
|
|
311
|
+
}
|
|
312
|
+
async function renderTree(absDir, relDir) {
|
|
313
|
+
const lines = [];
|
|
314
|
+
let count = 0;
|
|
315
|
+
async function walk(dir, prefix, depth) {
|
|
316
|
+
if (count >= MAX_TREE_ENTRIES || depth > 3) return;
|
|
317
|
+
let entries;
|
|
318
|
+
try {
|
|
319
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
320
|
+
} catch {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
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));
|
|
324
|
+
for (const e of filtered) {
|
|
325
|
+
if (count >= MAX_TREE_ENTRIES) {
|
|
326
|
+
lines.push(`${prefix}\u2026 (truncated)`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
count++;
|
|
330
|
+
lines.push(`${prefix}${e.name}${e.isDirectory() ? "/" : ""}`);
|
|
331
|
+
if (e.isDirectory()) await walk(join2(dir, e.name), prefix + " ", depth + 1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
await walk(absDir, "", 0);
|
|
335
|
+
return `${relDir || "."}/ (directory tree)
|
|
336
|
+
${lines.join("\n")}`;
|
|
337
|
+
}
|
|
338
|
+
async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET) {
|
|
339
|
+
const mentions = extractMentions(message);
|
|
340
|
+
if (mentions.length === 0) return { context: "", notice: null };
|
|
341
|
+
const blocks = [];
|
|
342
|
+
const attached = [];
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
let spent = 0;
|
|
345
|
+
for (const mention of mentions) {
|
|
346
|
+
if (spent >= tokenBudget) break;
|
|
347
|
+
const raw = mention.startsWith("~/") ? join2(process.env.HOME ?? "", mention.slice(2)) : mention;
|
|
348
|
+
const direct = isAbsolute(raw) ? raw : resolve(cwd2, raw);
|
|
349
|
+
let isDir = false;
|
|
350
|
+
try {
|
|
351
|
+
isDir = (await stat(direct)).isDirectory();
|
|
352
|
+
} catch {
|
|
353
|
+
isDir = false;
|
|
354
|
+
}
|
|
355
|
+
if (isDir) {
|
|
356
|
+
const rel = relative(cwd2, direct);
|
|
357
|
+
const tree = await renderTree(direct, rel);
|
|
358
|
+
const cost = estTokens(tree);
|
|
359
|
+
if (spent + cost <= tokenBudget) {
|
|
360
|
+
blocks.push(tree);
|
|
361
|
+
spent += cost;
|
|
362
|
+
attached.push(`${rel || "."}/ (tree)`);
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
let files = [];
|
|
367
|
+
try {
|
|
368
|
+
files = await fg(raw, {
|
|
369
|
+
cwd: cwd2,
|
|
370
|
+
ignore: IGNORE,
|
|
371
|
+
onlyFiles: true,
|
|
372
|
+
dot: true,
|
|
373
|
+
suppressErrors: true,
|
|
374
|
+
absolute: false
|
|
375
|
+
});
|
|
376
|
+
} catch {
|
|
377
|
+
files = [];
|
|
378
|
+
}
|
|
379
|
+
if (files.length === 0) {
|
|
380
|
+
try {
|
|
381
|
+
if ((await stat(direct)).isFile()) files = [relative(cwd2, direct)];
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
files = files.sort().slice(0, MAX_FILES_PER_MENTION);
|
|
386
|
+
for (const rel of files) {
|
|
387
|
+
if (spent >= tokenBudget) break;
|
|
388
|
+
if (seen.has(rel)) continue;
|
|
389
|
+
seen.add(rel);
|
|
390
|
+
let buf;
|
|
391
|
+
try {
|
|
392
|
+
buf = await readFile2(resolve(cwd2, rel), "utf8");
|
|
393
|
+
} catch {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (looksBinary(buf)) continue;
|
|
397
|
+
const cap = Math.min(PER_FILE_TOKEN_CAP, tokenBudget - spent);
|
|
398
|
+
let body = buf;
|
|
399
|
+
let truncated = false;
|
|
400
|
+
if (estTokens(body) > cap) {
|
|
401
|
+
body = body.slice(0, cap * CHARS_PER_TOKEN);
|
|
402
|
+
truncated = true;
|
|
403
|
+
}
|
|
404
|
+
const fence = body.includes("```") ? "````" : "```";
|
|
405
|
+
const header = truncated ? `${rel} (truncated to ~${cap} tokens of ${estTokens(buf)})` : rel;
|
|
406
|
+
blocks.push(`${header}
|
|
407
|
+
${fence}
|
|
408
|
+
${body}
|
|
409
|
+
${fence}`);
|
|
410
|
+
spent += estTokens(body);
|
|
411
|
+
attached.push(truncated ? `${rel} (truncated)` : rel);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (blocks.length === 0) return { context: "", notice: null };
|
|
415
|
+
const context = "The user attached these files with @-mentions. Use them as primary context for this turn:\n\n" + blocks.join("\n\n");
|
|
416
|
+
const notice = `attached ${attached.length} item(s): ${attached.slice(0, 8).join(", ")}${attached.length > 8 ? `, +${attached.length - 8} more` : ""} (~${spent} tokens)`;
|
|
417
|
+
return { context, notice };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/subagent.ts
|
|
421
|
+
import { generateText, stepCountIs, tool } from "ai";
|
|
422
|
+
import { z } from "zod";
|
|
423
|
+
var READONLY_TOOLS = ["read_file", "glob", "grep", "list_dir"];
|
|
424
|
+
function readonlyToolset(ctx) {
|
|
425
|
+
const all = buildTools(ctx);
|
|
426
|
+
const out = {};
|
|
427
|
+
for (const k of READONLY_TOOLS) if (all[k]) out[k] = all[k];
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
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.";
|
|
431
|
+
function buildSubagentTool(deps) {
|
|
432
|
+
const maxSteps = deps.maxSteps ?? 12;
|
|
433
|
+
const maxAgents = deps.maxAgents ?? 6;
|
|
434
|
+
const subtools = readonlyToolset(deps.ctx);
|
|
435
|
+
return {
|
|
436
|
+
spawn_agents: tool({
|
|
437
|
+
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.",
|
|
438
|
+
inputSchema: z.object({
|
|
439
|
+
tasks: z.array(
|
|
440
|
+
z.object({
|
|
441
|
+
label: z.string().describe("Short label for this slice, e.g. 'auth' or 'billing routes'."),
|
|
442
|
+
task: z.string().describe("The self-contained instruction for this sub-agent.")
|
|
443
|
+
})
|
|
444
|
+
).min(1).max(maxAgents).describe(`1\u2013${maxAgents} independent tasks to run concurrently.`)
|
|
445
|
+
}),
|
|
446
|
+
execute: async ({ tasks: tasks2 }) => {
|
|
447
|
+
line(c.dim(` \u21C9 spawning ${tasks2.length} sub-agent(s) in parallel`));
|
|
448
|
+
const results = await Promise.all(
|
|
449
|
+
tasks2.map(async ({ label, task }) => {
|
|
450
|
+
try {
|
|
451
|
+
const r = await generateText({
|
|
452
|
+
model: deps.model,
|
|
453
|
+
system: SUBAGENT_SYSTEM,
|
|
454
|
+
prompt: task,
|
|
455
|
+
tools: subtools,
|
|
456
|
+
stopWhen: stepCountIs(maxSteps)
|
|
457
|
+
});
|
|
458
|
+
line(c.dim(` \u21B3 ${c.cyan(label)} done`));
|
|
459
|
+
return { label, ok: true, result: r.text.trim() };
|
|
460
|
+
} catch (e) {
|
|
461
|
+
line(c.dim(` \u21B3 ${c.cyan(label)} ${c.red("failed")}`));
|
|
462
|
+
return { label, ok: false, error: e.message };
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
return { ok: true, count: results.length, results };
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/images.ts
|
|
473
|
+
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
474
|
+
import { resolve as resolve2, extname, relative as relative2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
475
|
+
import { execFile } from "child_process";
|
|
476
|
+
import { tmpdir, platform as platform2 } from "os";
|
|
477
|
+
var EXT_TO_MEDIA = {
|
|
478
|
+
".png": "image/png",
|
|
479
|
+
".jpg": "image/jpeg",
|
|
480
|
+
".jpeg": "image/jpeg",
|
|
481
|
+
".gif": "image/gif",
|
|
482
|
+
".webp": "image/webp"
|
|
483
|
+
};
|
|
484
|
+
var PER_IMAGE_BYTES = 6 * 1024 * 1024;
|
|
485
|
+
var TOTAL_BYTES = 15 * 1024 * 1024;
|
|
486
|
+
function wantsClipboard(message) {
|
|
487
|
+
return /(^|\s)@(clip|clipboard|paste)\b/i.test(message);
|
|
488
|
+
}
|
|
489
|
+
function run(cmd, args) {
|
|
490
|
+
return new Promise((res) => {
|
|
491
|
+
execFile(cmd, args, (err) => res(!err));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
async function grabClipboardImage() {
|
|
495
|
+
const out = join3(tmpdir(), `zeta-clip-${process.pid}-${process.hrtime.bigint()}.png`);
|
|
496
|
+
const os = platform2();
|
|
497
|
+
if (os === "darwin") {
|
|
498
|
+
const script = `set thePath to "${out}"
|
|
499
|
+
try
|
|
500
|
+
set pngData to (the clipboard as \xABclass PNGf\xBB)
|
|
501
|
+
on error
|
|
502
|
+
return "NOIMAGE"
|
|
503
|
+
end try
|
|
504
|
+
set fp to open for access (POSIX file thePath) with write permission
|
|
505
|
+
write pngData to fp
|
|
506
|
+
close access fp
|
|
507
|
+
return "OK"`;
|
|
508
|
+
const ok = await run("osascript", ["-e", script]);
|
|
509
|
+
if (!ok) return null;
|
|
510
|
+
} else if (os === "linux") {
|
|
511
|
+
const ok = await run("bash", ["-c", `xclip -selection clipboard -t image/png -o > "${out}"`]);
|
|
512
|
+
if (!ok) return null;
|
|
513
|
+
} else {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
const st = await stat2(out);
|
|
518
|
+
return st.isFile() && st.size > 0 ? out : null;
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function imageTokens(message) {
|
|
524
|
+
const out = /* @__PURE__ */ new Set();
|
|
525
|
+
const re = /@?([~A-Za-z0-9._\-/]+\.(?:png|jpe?g|gif|webp))\b/gi;
|
|
526
|
+
for (let m = re.exec(message); m; m = re.exec(message)) out.add(m[1]);
|
|
527
|
+
return [...out];
|
|
528
|
+
}
|
|
529
|
+
async function scanImages(message, cwd2) {
|
|
530
|
+
const tokens = imageTokens(message);
|
|
531
|
+
const images = [];
|
|
532
|
+
const skipped = [];
|
|
533
|
+
let total = 0;
|
|
534
|
+
if (wantsClipboard(message)) {
|
|
535
|
+
const clip = await grabClipboardImage();
|
|
536
|
+
if (clip) {
|
|
537
|
+
try {
|
|
538
|
+
const buf = await readFile3(clip);
|
|
539
|
+
if (buf.length <= PER_IMAGE_BYTES) {
|
|
540
|
+
images.push({
|
|
541
|
+
path: "clipboard",
|
|
542
|
+
dataUrl: `data:image/png;base64,${buf.toString("base64")}`,
|
|
543
|
+
mediaType: "image/png",
|
|
544
|
+
bytes: buf.length
|
|
545
|
+
});
|
|
546
|
+
total += buf.length;
|
|
547
|
+
} else {
|
|
548
|
+
skipped.push("clipboard (over the size cap)");
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
skipped.push("clipboard (unreadable)");
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
skipped.push("clipboard (no image found)");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (const tok of tokens) {
|
|
558
|
+
const raw = tok.startsWith("~/") ? join3(process.env.HOME ?? "", tok.slice(2)) : tok;
|
|
559
|
+
const abs = isAbsolute2(raw) ? raw : resolve2(cwd2, raw);
|
|
560
|
+
const mediaType = EXT_TO_MEDIA[extname(abs).toLowerCase()];
|
|
561
|
+
if (!mediaType) continue;
|
|
562
|
+
let bytes;
|
|
563
|
+
try {
|
|
564
|
+
const st = await stat2(abs);
|
|
565
|
+
if (!st.isFile()) continue;
|
|
566
|
+
bytes = st.size;
|
|
567
|
+
} catch {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const label = isAbsolute2(raw) ? raw : relative2(cwd2, abs);
|
|
571
|
+
if (bytes > PER_IMAGE_BYTES || total + bytes > TOTAL_BYTES) {
|
|
572
|
+
skipped.push(`${label} (${Math.round(bytes / 1024)}KB \u2014 over the size cap)`);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
const buf = await readFile3(abs);
|
|
577
|
+
const dataUrl = `data:${mediaType};base64,${buf.toString("base64")}`;
|
|
578
|
+
images.push({ path: label, dataUrl, mediaType, bytes });
|
|
579
|
+
total += bytes;
|
|
580
|
+
} catch {
|
|
581
|
+
skipped.push(`${label} (unreadable)`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return { images, skipped };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/weburl.ts
|
|
588
|
+
var CHARS_PER_TOKEN2 = 4;
|
|
589
|
+
var DEFAULT_TOKEN_BUDGET2 = 12e3;
|
|
590
|
+
var PER_URL_TOKEN_CAP = 6e3;
|
|
591
|
+
var FETCH_TIMEOUT_MS = 12e3;
|
|
592
|
+
var MAX_BYTES = 4 * 1024 * 1024;
|
|
593
|
+
function estTokens2(s) {
|
|
594
|
+
return Math.ceil(s.length / CHARS_PER_TOKEN2);
|
|
595
|
+
}
|
|
596
|
+
function extractUrls(message) {
|
|
597
|
+
const out = /* @__PURE__ */ new Set();
|
|
598
|
+
const re = /@?(https?:\/\/[^\s<>")]+)/gi;
|
|
599
|
+
for (let m = re.exec(message); m; m = re.exec(message)) {
|
|
600
|
+
out.add(m[1].replace(/[.,;:)\]]+$/, ""));
|
|
601
|
+
}
|
|
602
|
+
return [...out];
|
|
603
|
+
}
|
|
604
|
+
function htmlToText(html) {
|
|
605
|
+
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();
|
|
606
|
+
}
|
|
607
|
+
async function fetchReadable(url) {
|
|
608
|
+
const ctrl = new AbortController();
|
|
609
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
610
|
+
try {
|
|
611
|
+
const res = await fetch(url, {
|
|
612
|
+
signal: ctrl.signal,
|
|
613
|
+
redirect: "follow",
|
|
614
|
+
headers: { "user-agent": "zeta-g-cli/0.4 (+web-context)" }
|
|
615
|
+
});
|
|
616
|
+
if (!res.ok) return null;
|
|
617
|
+
const ctype = res.headers.get("content-type") ?? "";
|
|
618
|
+
const raw = await res.text();
|
|
619
|
+
const body = raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) : raw;
|
|
620
|
+
const text = /html/i.test(ctype) ? htmlToText(body) : body.trim();
|
|
621
|
+
return { text, finalUrl: res.url || url };
|
|
622
|
+
} catch {
|
|
623
|
+
return null;
|
|
624
|
+
} finally {
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async function expandUrls(message, tokenBudget = DEFAULT_TOKEN_BUDGET2) {
|
|
629
|
+
const urls = extractUrls(message);
|
|
630
|
+
if (urls.length === 0) return { context: "", notice: null };
|
|
631
|
+
const blocks = [];
|
|
632
|
+
const fetched = [];
|
|
633
|
+
const failed = [];
|
|
634
|
+
let spent = 0;
|
|
635
|
+
for (const url of urls) {
|
|
636
|
+
if (spent >= tokenBudget) break;
|
|
637
|
+
const r = await fetchReadable(url);
|
|
638
|
+
if (!r || !r.text) {
|
|
639
|
+
failed.push(url);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const cap = Math.min(PER_URL_TOKEN_CAP, tokenBudget - spent);
|
|
643
|
+
let body = r.text;
|
|
644
|
+
let truncated = false;
|
|
645
|
+
if (estTokens2(body) > cap) {
|
|
646
|
+
body = body.slice(0, cap * CHARS_PER_TOKEN2);
|
|
647
|
+
truncated = true;
|
|
648
|
+
}
|
|
649
|
+
const header = truncated ? `${r.finalUrl} (truncated to ~${cap} tokens)` : r.finalUrl;
|
|
650
|
+
blocks.push(`${header}
|
|
651
|
+
${body}`);
|
|
652
|
+
spent += estTokens2(body);
|
|
653
|
+
fetched.push(r.finalUrl);
|
|
654
|
+
}
|
|
655
|
+
if (blocks.length === 0) {
|
|
656
|
+
return { context: "", notice: failed.length ? `couldn't fetch: ${failed.join(", ")}` : null };
|
|
657
|
+
}
|
|
658
|
+
const context = "The user linked these pages. Use them as context for this turn:\n\n" + blocks.join("\n\n---\n\n");
|
|
659
|
+
const noticeParts = [`fetched ${fetched.length} URL(s) (~${spent} tokens)`];
|
|
660
|
+
if (failed.length) noticeParts.push(`failed: ${failed.length}`);
|
|
661
|
+
return { context, notice: noticeParts.join(" \xB7 ") };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/export.ts
|
|
665
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
666
|
+
import { resolve as resolve3 } from "path";
|
|
667
|
+
function renderContent(content) {
|
|
668
|
+
if (typeof content === "string") return content.trim();
|
|
669
|
+
if (!Array.isArray(content)) return "";
|
|
670
|
+
const parts = [];
|
|
671
|
+
for (const p of content) {
|
|
672
|
+
const type = p.type;
|
|
673
|
+
if (type === "text" && typeof p.text === "string") parts.push(p.text.trim());
|
|
674
|
+
else if (type === "image") parts.push("_[image]_");
|
|
675
|
+
else if (type === "tool-call") parts.push(`\`\u2192 ${String(p.toolName ?? "tool")}\``);
|
|
676
|
+
else if (type === "tool-result") parts.push(`\`\u2713 ${String(p.toolName ?? "tool")}\``);
|
|
677
|
+
}
|
|
678
|
+
return parts.filter(Boolean).join("\n\n");
|
|
679
|
+
}
|
|
680
|
+
function renderTranscript(messages, meta) {
|
|
681
|
+
const head = [
|
|
682
|
+
`# zeta-g transcript`,
|
|
683
|
+
``,
|
|
684
|
+
`- model: ${meta.model}`,
|
|
685
|
+
meta.startedAt ? `- started: ${meta.startedAt}` : "",
|
|
686
|
+
`- messages: ${messages.length}`,
|
|
687
|
+
``,
|
|
688
|
+
`---`,
|
|
689
|
+
``
|
|
690
|
+
].filter((l) => l !== "").join("\n");
|
|
691
|
+
const body = messages.map((m) => {
|
|
692
|
+
const text = renderContent(m.content);
|
|
693
|
+
if (!text) return "";
|
|
694
|
+
const who = m.role === "user" ? "\u{1F9D1} User" : m.role === "assistant" ? "\u{1F916} Zeta-G" : m.role === "tool" ? "\u{1F527} Tool" : m.role;
|
|
695
|
+
return `### ${who}
|
|
696
|
+
|
|
697
|
+
${text}`;
|
|
698
|
+
}).filter(Boolean).join("\n\n");
|
|
699
|
+
return `${head}
|
|
700
|
+
|
|
701
|
+
${body}
|
|
702
|
+
`;
|
|
703
|
+
}
|
|
704
|
+
function writeTranscript(messages, meta, file, cwd2) {
|
|
705
|
+
const abs = resolve3(cwd2, file);
|
|
706
|
+
writeFileSync2(abs, renderTranscript(messages, meta), "utf8");
|
|
707
|
+
return abs;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/cli.ts
|
|
711
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
712
|
+
import { homedir as homedir4 } from "os";
|
|
713
|
+
import { join as join6 } from "path";
|
|
714
|
+
|
|
715
|
+
// src/update-check.ts
|
|
716
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
717
|
+
import { homedir as homedir2 } from "os";
|
|
718
|
+
import { join as join4 } from "path";
|
|
719
|
+
var PKG = "wholestack";
|
|
720
|
+
var CACHE = join4(homedir2(), ".zeta-g", "update-check.json");
|
|
721
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
722
|
+
function readCache() {
|
|
723
|
+
try {
|
|
724
|
+
return JSON.parse(readFileSync2(CACHE, "utf8"));
|
|
725
|
+
} catch {
|
|
726
|
+
return { lastCheck: 0, latest: null };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function writeCache(c2) {
|
|
730
|
+
try {
|
|
731
|
+
mkdirSync2(join4(homedir2(), ".zeta-g"), { recursive: true });
|
|
732
|
+
writeFileSync3(CACHE, JSON.stringify(c2));
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function isNewer(a, b) {
|
|
737
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
738
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
739
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
740
|
+
const x = pa[i] ?? 0;
|
|
741
|
+
const y = pb[i] ?? 0;
|
|
742
|
+
if (x !== y) return x > y;
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
async function checkForUpdate(current) {
|
|
747
|
+
if (process.env.ZETA_NO_UPDATE_CHECK || !existsSync2) return null;
|
|
748
|
+
const cache = readCache();
|
|
749
|
+
const fresh = Date.now() - cache.lastCheck < DAY_MS;
|
|
750
|
+
let latest = cache.latest;
|
|
751
|
+
if (!fresh) {
|
|
752
|
+
try {
|
|
753
|
+
const ctrl = new AbortController();
|
|
754
|
+
const t = setTimeout(() => ctrl.abort(), 1200);
|
|
755
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
|
|
756
|
+
signal: ctrl.signal,
|
|
757
|
+
headers: { accept: "application/json" }
|
|
758
|
+
});
|
|
759
|
+
clearTimeout(t);
|
|
760
|
+
if (res.ok) {
|
|
761
|
+
const json = await res.json();
|
|
762
|
+
latest = json.version ?? null;
|
|
763
|
+
}
|
|
764
|
+
} catch {
|
|
765
|
+
latest = cache.latest;
|
|
766
|
+
}
|
|
767
|
+
writeCache({ lastCheck: Date.now(), latest });
|
|
768
|
+
}
|
|
769
|
+
return latest && isNewer(latest, current) ? latest : null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/access.ts
|
|
773
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
774
|
+
import { homedir as homedir3 } from "os";
|
|
775
|
+
import { join as join5 } from "path";
|
|
776
|
+
var CACHE2 = join5(homedir3(), ".zeta-g", "access.json");
|
|
777
|
+
var TTL_MS = 60 * 60 * 1e3;
|
|
778
|
+
function readCache2() {
|
|
779
|
+
try {
|
|
780
|
+
return JSON.parse(readFileSync3(CACHE2, "utf8"));
|
|
781
|
+
} catch {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function writeCache2(c2) {
|
|
786
|
+
try {
|
|
787
|
+
mkdirSync3(join5(homedir3(), ".zeta-g"), { recursive: true });
|
|
788
|
+
writeFileSync4(CACHE2, JSON.stringify(c2));
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async function verifyAccess(webUrl, token) {
|
|
793
|
+
if (!token) return { active: false, tier: null, degraded: false };
|
|
794
|
+
const cached = readCache2();
|
|
795
|
+
if (cached && Date.now() - cached.checkedAt < TTL_MS) {
|
|
796
|
+
return { active: cached.active, tier: cached.tier, degraded: false };
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
const ctrl = new AbortController();
|
|
800
|
+
const t = setTimeout(() => ctrl.abort(), 2500);
|
|
801
|
+
const res = await fetch(`${webUrl.replace(/\/$/, "")}/api/cli/verify`, {
|
|
802
|
+
headers: { authorization: `Bearer ${token}` },
|
|
803
|
+
signal: ctrl.signal
|
|
804
|
+
});
|
|
805
|
+
clearTimeout(t);
|
|
806
|
+
if (res.status === 401) {
|
|
807
|
+
writeCache2({ checkedAt: Date.now(), active: false, tier: null });
|
|
808
|
+
return { active: false, tier: null, degraded: false };
|
|
809
|
+
}
|
|
810
|
+
const json = await res.json();
|
|
811
|
+
const active = Boolean(json.active);
|
|
812
|
+
writeCache2({ checkedAt: Date.now(), active, tier: json.tier ?? null });
|
|
813
|
+
return { active, tier: json.tier ?? null, degraded: false };
|
|
814
|
+
} catch {
|
|
815
|
+
return { active: true, tier: cached?.tier ?? null, degraded: true };
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function showPaywall(loggedIn, webUrl) {
|
|
819
|
+
const w = 52;
|
|
820
|
+
line();
|
|
821
|
+
line(" " + c.cyan("\u256D" + "\u2500".repeat(w) + "\u256E"));
|
|
822
|
+
const row = (s) => line(" " + c.cyan("\u2502") + " " + s.padEnd(w - 2) + " " + c.cyan("\u2502"));
|
|
823
|
+
row(c.bold("zeta needs a subscription"));
|
|
824
|
+
row("");
|
|
825
|
+
if (!loggedIn) {
|
|
826
|
+
row(c.dim("You're not signed in."));
|
|
827
|
+
row("Run " + c.cyan("zeta login") + c.dim(" then subscribe."));
|
|
828
|
+
} else {
|
|
829
|
+
row(c.dim("No active paid plan on this account."));
|
|
830
|
+
row("Subscribe to use zeta in the terminal.");
|
|
831
|
+
}
|
|
832
|
+
row("");
|
|
833
|
+
row(c.dim("Plans: ") + c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
|
|
834
|
+
line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
|
|
835
|
+
line();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/art.ts
|
|
839
|
+
var ART_PACK = [
|
|
840
|
+
[" \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 "],
|
|
841
|
+
[" \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"],
|
|
842
|
+
[" \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 "],
|
|
843
|
+
[" \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"],
|
|
844
|
+
[" \u2571\u2572 ", " \u2571\u2500\u2500\u2572 ", " \u2571 \u2571\u2571 \u2572 ", "\u2571 \u2571\u2571 \u2572 ", "\u2572\u2571\u2571\u2500\u2500\u2500\u2500\u2500\u2572"]
|
|
845
|
+
];
|
|
846
|
+
function randomArt(seed) {
|
|
847
|
+
const i = seed != null ? seed % ART_PACK.length : Math.floor(Math.random() * ART_PACK.length);
|
|
848
|
+
return ART_PACK[(i % ART_PACK.length + ART_PACK.length) % ART_PACK.length];
|
|
849
|
+
}
|
|
850
|
+
var vlen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
851
|
+
function fetchPanel(fields, art = randomArt()) {
|
|
852
|
+
const shown = fields.filter((f) => f.value);
|
|
853
|
+
const labelW = Math.max(0, ...shown.map((f) => f.label.length));
|
|
854
|
+
const artW = Math.max(0, ...art.map(vlen));
|
|
855
|
+
const rows = Math.max(art.length, shown.length);
|
|
856
|
+
const out = [];
|
|
857
|
+
out.push("");
|
|
858
|
+
for (let i = 0; i < rows; i++) {
|
|
859
|
+
const left = (art[i] ?? "").padEnd(artW + 2);
|
|
860
|
+
const f = shown[i];
|
|
861
|
+
const right = f ? c.dim(f.label.padStart(labelW) + " ") + c.bold(f.value) : "";
|
|
862
|
+
out.push(" " + c.cyan(left) + right);
|
|
863
|
+
}
|
|
864
|
+
out.push("");
|
|
865
|
+
for (const l of out) process.stdout.write(l + "\n");
|
|
866
|
+
}
|
|
867
|
+
|
|
269
868
|
// src/cli.ts
|
|
869
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
870
|
+
import { basename } from "path";
|
|
871
|
+
var VERSION = "0.5.0";
|
|
872
|
+
function gitBranch(dir) {
|
|
873
|
+
try {
|
|
874
|
+
const head = readFileSync4(join6(dir, ".git", "HEAD"), "utf8").trim();
|
|
875
|
+
const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
|
|
876
|
+
return m ? m[1] : head.slice(0, 7);
|
|
877
|
+
} catch {
|
|
878
|
+
return "";
|
|
879
|
+
}
|
|
880
|
+
}
|
|
270
881
|
function parse(raw) {
|
|
271
882
|
const a = {
|
|
272
883
|
zetaUrl: process.env.ZETA_API_URL ?? "https://wholestack.ai",
|
|
@@ -294,6 +905,8 @@ function parse(raw) {
|
|
|
294
905
|
const m = raw[++i];
|
|
295
906
|
if (isPermissionMode(m)) a.mode = m;
|
|
296
907
|
} else if (t === "--plan") a.plan = true;
|
|
908
|
+
else if (t === "--persona") a.persona = raw[++i];
|
|
909
|
+
else if (t === "--nzt48" || t === "--nzt-48") a.persona = "nzt-48";
|
|
297
910
|
else if (t === "--think") a.think = true;
|
|
298
911
|
else if (t === "--no-think") a.think = false;
|
|
299
912
|
else if (t === "--mcp") a.mcp = (raw[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -324,12 +937,15 @@ ${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmo
|
|
|
324
937
|
zeta-g prove <dir> <Contract> --property <k> prove one invariant
|
|
325
938
|
zeta-g verify <dir> <Contract> --cert c.json passthrough + signed certificate
|
|
326
939
|
${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
|
|
940
|
+
zeta-g --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
|
|
327
941
|
|
|
328
942
|
${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)
|
|
943
|
+
-m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
|
|
944
|
+
(lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
|
|
331
945
|
--mode <m> default | acceptEdits | plan | yolo (permission mode)
|
|
332
946
|
--plan start in read-only plan mode
|
|
947
|
+
--nzt48 run as NZT-48, the web3 special-ops persona
|
|
948
|
+
--persona <id> layer a named persona on the base agent
|
|
333
949
|
--think/--no-think toggle extended thinking (when a tier supports it)
|
|
334
950
|
--continue resume the most recent session
|
|
335
951
|
--resume [id] resume a session (id, or the latest)
|
|
@@ -352,7 +968,14 @@ ${c.bold("Auth")} ${c.dim("(set once, then just run zeta-g)")}
|
|
|
352
968
|
zeta-g login --build <key> ZETA build-engine key (for /build)
|
|
353
969
|
|
|
354
970
|
${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
|
|
971
|
+
/model /cost /tools /undo /redo /checkpoints /compact /resume /memory /mcp /mode /think /init /doctor /clear /exit
|
|
972
|
+
/diff /commit /pr ${c.dim("git flow \u2014 review, commit (auto-written msg), open a PR")}
|
|
973
|
+
/tasks ${c.dim("background jobs \u2014 run_background runs builds/tests while you work")}
|
|
974
|
+
/export [file.md] ${c.dim("write the conversation transcript to markdown")}
|
|
975
|
+
${c.dim("@path \xB7 @dir/ \xB7 @glob attach files to the turn (token-budgeted, Tab-completes)")}
|
|
976
|
+
${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
|
|
977
|
+
${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
|
|
978
|
+
${c.dim("spawn_agents tool fan a task out to parallel read-only sub-agents")}`;
|
|
356
979
|
async function runSecuritySubcommand(raw) {
|
|
357
980
|
const verb = raw[0];
|
|
358
981
|
if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
|
|
@@ -375,6 +998,19 @@ async function runSecuritySubcommand(raw) {
|
|
|
375
998
|
line(r.ok ? c.green(" \u2713 verified") : c.red(" \u2717 NOT verified"));
|
|
376
999
|
return r.code;
|
|
377
1000
|
}
|
|
1001
|
+
function maybeYoloNotice() {
|
|
1002
|
+
try {
|
|
1003
|
+
const dir = join6(homedir4(), ".zeta-g");
|
|
1004
|
+
const marker = join6(dir, "yolo-notice-seen");
|
|
1005
|
+
if (existsSync3(marker)) return;
|
|
1006
|
+
line(
|
|
1007
|
+
" " + 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"
|
|
1008
|
+
);
|
|
1009
|
+
mkdirSync4(dir, { recursive: true });
|
|
1010
|
+
writeFileSync5(marker, (/* @__PURE__ */ new Date()).toISOString());
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
378
1014
|
function webBaseUrl() {
|
|
379
1015
|
return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
|
|
380
1016
|
}
|
|
@@ -473,13 +1109,21 @@ async function main() {
|
|
|
473
1109
|
if (sub !== null) exit(sub);
|
|
474
1110
|
const args = parse(rawArgs);
|
|
475
1111
|
if (args.version) {
|
|
476
|
-
line(
|
|
1112
|
+
line(`wholestack ${VERSION}`);
|
|
477
1113
|
return;
|
|
478
1114
|
}
|
|
479
1115
|
if (args.help) {
|
|
480
1116
|
line(HELP);
|
|
481
1117
|
return;
|
|
482
1118
|
}
|
|
1119
|
+
{
|
|
1120
|
+
const token = process.env.ZETA_API_KEY;
|
|
1121
|
+
const access = await verifyAccess(webBaseUrl(), token);
|
|
1122
|
+
if (!access.active) {
|
|
1123
|
+
showPaywall(Boolean(token), webBaseUrl());
|
|
1124
|
+
exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
483
1127
|
let modelKey;
|
|
484
1128
|
try {
|
|
485
1129
|
modelKey = resolveModelKey(args.model);
|
|
@@ -497,7 +1141,11 @@ async function main() {
|
|
|
497
1141
|
const isTty = !!stdin.isTTY;
|
|
498
1142
|
const oneShot = args.prompt.length > 0;
|
|
499
1143
|
const workdir = cwd();
|
|
500
|
-
const
|
|
1144
|
+
const persistedMode = isPermissionMode(
|
|
1145
|
+
process.env.ZETA_PERMISSION_MODE
|
|
1146
|
+
) ? process.env.ZETA_PERMISSION_MODE : void 0;
|
|
1147
|
+
const yoloIsDefault = !args.plan && !args.mode && !args.yes && !persistedMode && isTty;
|
|
1148
|
+
const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
|
|
501
1149
|
const memory = await loadProjectMemory(workdir);
|
|
502
1150
|
const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
|
|
503
1151
|
const memoryText = [memory.text, plugins.systemText].filter(Boolean).join("\n\n") || void 0;
|
|
@@ -507,7 +1155,6 @@ async function main() {
|
|
|
507
1155
|
disabled: args.noMcp,
|
|
508
1156
|
extraServers: plugins.mcpServers
|
|
509
1157
|
});
|
|
510
|
-
const extraTools = { ...buildWebTools(), ...mcp.tools };
|
|
511
1158
|
const hooks = new HookRunner(mergeHookSets(loadHookFiles(workdir), plugins.hooks), workdir);
|
|
512
1159
|
const registry = new CommandRegistry();
|
|
513
1160
|
registry.loadCustom(workdir);
|
|
@@ -518,6 +1165,7 @@ async function main() {
|
|
|
518
1165
|
if (shuttingDown) return;
|
|
519
1166
|
shuttingDown = true;
|
|
520
1167
|
killRunningApps();
|
|
1168
|
+
tasks.killAll();
|
|
521
1169
|
await mcp.close().catch(() => {
|
|
522
1170
|
});
|
|
523
1171
|
ic?.close();
|
|
@@ -529,12 +1177,33 @@ async function main() {
|
|
|
529
1177
|
onExit: () => {
|
|
530
1178
|
line(c.dim(" bye."));
|
|
531
1179
|
void shutdown(0);
|
|
1180
|
+
},
|
|
1181
|
+
// Starship-style context segments above the prompt box.
|
|
1182
|
+
contextBar: () => {
|
|
1183
|
+
const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename(workdir))];
|
|
1184
|
+
const br = gitBranch(workdir);
|
|
1185
|
+
if (br) seg.push(c.dim("\u2387 " + br));
|
|
1186
|
+
seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
|
|
1187
|
+
return seg.join(c.dim(" \xB7 "));
|
|
532
1188
|
}
|
|
533
1189
|
});
|
|
534
1190
|
}
|
|
535
1191
|
const confirm = isTty && ic ? ic.confirm.bind(ic) : void 0;
|
|
536
|
-
const
|
|
1192
|
+
const persistMode = (m) => {
|
|
1193
|
+
try {
|
|
1194
|
+
saveKey("ZETA_PERMISSION_MODE", m);
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const permissions = new Permissions(mode, confirm, persistMode);
|
|
537
1199
|
const checkpoints = new CheckpointStore();
|
|
1200
|
+
const toolCtx = { cwd: workdir, zetaApiUrl: args.zetaUrl, buildMode: args.buildMode, permissions, checkpoints };
|
|
1201
|
+
const extraTools = {
|
|
1202
|
+
...buildWebTools(),
|
|
1203
|
+
...mcp.tools,
|
|
1204
|
+
// Parallel read-only sub-agents (needs the model handle the base tools lack).
|
|
1205
|
+
...buildSubagentTool({ model, modelKey, ctx: toolCtx })
|
|
1206
|
+
};
|
|
538
1207
|
let session;
|
|
539
1208
|
let resumedMessages = null;
|
|
540
1209
|
if (args.resume !== void 0 || args.cont) {
|
|
@@ -554,19 +1223,27 @@ async function main() {
|
|
|
554
1223
|
const agent = new Agent({
|
|
555
1224
|
model,
|
|
556
1225
|
modelKey,
|
|
557
|
-
ctx:
|
|
1226
|
+
ctx: toolCtx,
|
|
558
1227
|
extraTools,
|
|
559
1228
|
hooks,
|
|
560
1229
|
memoryText,
|
|
1230
|
+
persona: args.persona,
|
|
561
1231
|
thinking,
|
|
1232
|
+
yolo: mode === "yolo",
|
|
562
1233
|
maxSteps: 16,
|
|
563
1234
|
contextWindow: modelContextWindow(modelKey),
|
|
564
1235
|
session
|
|
565
1236
|
});
|
|
566
1237
|
if (resumedMessages) agent.replaceHistory(resumedMessages);
|
|
567
1238
|
if (oneShot) {
|
|
568
|
-
|
|
569
|
-
|
|
1239
|
+
const m = await expandMentions(args.prompt, workdir);
|
|
1240
|
+
if (m.notice) line(c.dim(` @ ${m.notice}`) + "\n");
|
|
1241
|
+
const oneShotText = m.context ? `${args.prompt}
|
|
1242
|
+
|
|
1243
|
+
---
|
|
1244
|
+
${m.context}` : args.prompt;
|
|
1245
|
+
if (ic) await ic.runInterruptible((sig) => agent.send(oneShotText, sig));
|
|
1246
|
+
else await agent.send(oneShotText);
|
|
570
1247
|
await shutdown(0);
|
|
571
1248
|
return;
|
|
572
1249
|
}
|
|
@@ -575,10 +1252,28 @@ async function main() {
|
|
|
575
1252
|
await shutdown(1);
|
|
576
1253
|
return;
|
|
577
1254
|
}
|
|
578
|
-
banner();
|
|
1255
|
+
await banner();
|
|
1256
|
+
fetchPanel([
|
|
1257
|
+
{ label: "model", value: modelLabel(modelKey) },
|
|
1258
|
+
{ label: "mode", value: mode },
|
|
1259
|
+
{ label: "dir", value: basename(workdir) },
|
|
1260
|
+
{ label: "git", value: gitBranch(workdir) },
|
|
1261
|
+
{ label: "node", value: process.version },
|
|
1262
|
+
{ label: "memory", value: memory.sources.length ? `${memory.sources.length} file(s)` : "" },
|
|
1263
|
+
{ label: "mcp", value: mcp.mounted.length ? `${mcp.mounted.length} mounted` : "" },
|
|
1264
|
+
{ label: "zeta", value: VERSION }
|
|
1265
|
+
]);
|
|
579
1266
|
bootSummary(memory, plugins, mcp, mode, thinking);
|
|
580
1267
|
if (mode === "plan") announcePlanMode();
|
|
1268
|
+
else if (yoloIsDefault) maybeYoloNotice();
|
|
581
1269
|
if (resumedMessages) line(" " + c.dim(`resumed ${session.meta.id} \xB7 ${resumedMessages.length} messages`) + "\n");
|
|
1270
|
+
void checkForUpdate(VERSION).then((latest) => {
|
|
1271
|
+
if (latest) {
|
|
1272
|
+
line(
|
|
1273
|
+
" " + c.yellow(`\u2191 wholestack ${latest} available`) + c.dim(` (you have ${VERSION}) \xB7 `) + c.cyan("npm i -g wholestack") + "\n"
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
582
1277
|
const baseCtx = () => ({
|
|
583
1278
|
cwd: workdir,
|
|
584
1279
|
modelKey,
|
|
@@ -593,9 +1288,9 @@ async function main() {
|
|
|
593
1288
|
checkpoints,
|
|
594
1289
|
print: (s = "") => line(s)
|
|
595
1290
|
});
|
|
596
|
-
const runTurn = async (text) => {
|
|
1291
|
+
const runTurn = async (text, images) => {
|
|
597
1292
|
const t0 = Date.now();
|
|
598
|
-
await ic.runInterruptible((sig) => agent.send(text, sig));
|
|
1293
|
+
await ic.runInterruptible((sig) => agent.send(text, sig, images));
|
|
599
1294
|
statusLine({
|
|
600
1295
|
model: modelLabel(modelKey),
|
|
601
1296
|
tokens: agent.usage.totalTokens,
|
|
@@ -607,6 +1302,11 @@ async function main() {
|
|
|
607
1302
|
});
|
|
608
1303
|
};
|
|
609
1304
|
for (; ; ) {
|
|
1305
|
+
for (const done of tasks.drainCompleted()) {
|
|
1306
|
+
const mark = done.status === "exited" ? c.green("\u2713") : c.red("\u2717");
|
|
1307
|
+
const code = done.exitCode == null ? "" : ` (exit ${done.exitCode})`;
|
|
1308
|
+
line(` ${mark} ${c.dim("background")} ${c.cyan(done.id)} ${done.display}${c.dim(code)}`);
|
|
1309
|
+
}
|
|
610
1310
|
const input = await ic.readLine(c.cyan(" \u203A "));
|
|
611
1311
|
if (!input) continue;
|
|
612
1312
|
if (input.startsWith("/")) {
|
|
@@ -636,7 +1336,12 @@ async function main() {
|
|
|
636
1336
|
}
|
|
637
1337
|
} else if (res.type === "setMode") {
|
|
638
1338
|
permissions.setMode(res.mode);
|
|
639
|
-
|
|
1339
|
+
if (res.mode !== "plan") {
|
|
1340
|
+
persistMode(res.mode);
|
|
1341
|
+
line(" " + c.dim(`mode: ${res.mode} \xB7 remembered for next session`) + "\n");
|
|
1342
|
+
} else {
|
|
1343
|
+
line(" " + c.dim(`mode: ${res.mode}`) + "\n");
|
|
1344
|
+
}
|
|
640
1345
|
if (res.mode === "plan") announcePlanMode();
|
|
641
1346
|
} else if (res.type === "toggleThinking") {
|
|
642
1347
|
if (supportsThinking(modelKey)) {
|
|
@@ -655,13 +1360,54 @@ async function main() {
|
|
|
655
1360
|
} else {
|
|
656
1361
|
line(" " + c.red(`no session ${res.sessionId}`) + "\n");
|
|
657
1362
|
}
|
|
1363
|
+
} else if (res.type === "export") {
|
|
1364
|
+
const file = res.file ?? `zeta-transcript-${gitBranch(workdir) || "session"}.md`;
|
|
1365
|
+
try {
|
|
1366
|
+
const abs = writeTranscript(agent.snapshot(), { model: modelLabel(modelKey) }, file, workdir);
|
|
1367
|
+
line(" " + c.green(`\u2713 transcript \u2192 ${abs}`) + "\n");
|
|
1368
|
+
} catch (e) {
|
|
1369
|
+
line(" " + c.red(e.message) + "\n");
|
|
1370
|
+
}
|
|
658
1371
|
}
|
|
659
1372
|
continue;
|
|
660
1373
|
}
|
|
661
|
-
userBox(input);
|
|
662
|
-
await
|
|
1374
|
+
if (!process.stdin.isTTY || process.env.NO_COLOR) userBox(input);
|
|
1375
|
+
const mentioned = await expandMentions(input, workdir);
|
|
1376
|
+
if (mentioned.notice) line(c.dim(` @ ${mentioned.notice}`) + "\n");
|
|
1377
|
+
const web = await expandUrls(input);
|
|
1378
|
+
if (web.notice) line(c.dim(` \u2301 ${web.notice}`) + "\n");
|
|
1379
|
+
const extraContext = [mentioned.context, web.context].filter(Boolean).join("\n\n---\n\n");
|
|
1380
|
+
const imgs = await scanImages(input, workdir);
|
|
1381
|
+
let attachments;
|
|
1382
|
+
if (imgs.images.length > 0) {
|
|
1383
|
+
if (!visionCapable(modelKey) && process.env.OPENROUTER_API_KEY) {
|
|
1384
|
+
try {
|
|
1385
|
+
const vkey = resolveModelKey("vision");
|
|
1386
|
+
modelKey = vkey;
|
|
1387
|
+
agent.setModel(resolveModel(vkey), vkey, modelContextWindow(vkey));
|
|
1388
|
+
agent.setThinking(false);
|
|
1389
|
+
line(c.dim(` \u29C9 image attached \u2192 switched to ${modelLabel(vkey)} (/model zeta-g1 to switch back)`) + "\n");
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
line(c.red(` \u29C9 couldn't switch to the vision lane: ${e.message}`) + "\n");
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (visionCapable(modelKey)) {
|
|
1395
|
+
attachments = imgs.images.map((i) => ({ dataUrl: i.dataUrl }));
|
|
1396
|
+
line(c.dim(` \u29C9 attached ${imgs.images.length} image(s): ${imgs.images.map((i) => i.path).join(", ")}`) + "\n");
|
|
1397
|
+
} else {
|
|
1398
|
+
line(
|
|
1399
|
+
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"
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (imgs.skipped.length) line(c.dim(` \u29C9 skipped: ${imgs.skipped.join(", ")}`) + "\n");
|
|
1404
|
+
await runTurn(extraContext ? `${input}
|
|
1405
|
+
|
|
1406
|
+
---
|
|
1407
|
+
${extraContext}` : input, attachments);
|
|
663
1408
|
}
|
|
664
1409
|
killRunningApps();
|
|
1410
|
+
tasks.killAll();
|
|
665
1411
|
await mcp.close().catch(() => {
|
|
666
1412
|
});
|
|
667
1413
|
ic.close();
|