cubefarm 0.2.0 → 0.3.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/README.md +8 -7
- package/bin/cubefarm.js +16 -2
- package/dist/assets/{ToyWorld-CrlIe2hV.js → ToyWorld-CM2m1Ytr.js} +2 -2
- package/dist/assets/index-BCFM-9fU.js +4331 -0
- package/dist/assets/index-BU33ZUhQ.css +1 -0
- package/dist/assets/{rapier-B1_J71Lw.js → rapier-BLchqNuA.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +1619 -173
- package/dist-server/index.js.map +4 -4
- package/dist-server/ptyHost.js +234 -0
- package/dist-server/ptyHost.js.map +7 -0
- package/package.json +87 -78
- package/dist/assets/index-CazA79UU.css +0 -1
- package/dist/assets/index-DDIwWVME.js +0 -4316
package/dist-server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// server/index.ts
|
|
2
2
|
import http2 from "node:http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path13 from "node:path";
|
|
4
|
+
import fs11 from "node:fs";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
|
|
@@ -474,20 +474,38 @@ function removeDesk(fullName, agentSlug) {
|
|
|
474
474
|
});
|
|
475
475
|
}
|
|
476
476
|
var LAUNCHERS = ["node.exe", "cmd.exe", "bash.exe", "sh.exe", "conhost.exe", "python.exe", "npm.exe", "npx.exe", "bun.exe", "deno.exe"];
|
|
477
|
-
|
|
477
|
+
var pathForms = (p) => [.../* @__PURE__ */ new Set([p, p.replaceAll("\\", "/"), p.replaceAll("\\", "\\\\")])];
|
|
478
|
+
function leftoversInDesk(listing, desk, keep) {
|
|
479
|
+
const out = [];
|
|
480
|
+
for (const line of listing.split("\n")) {
|
|
481
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
482
|
+
if (!m) continue;
|
|
483
|
+
const [pid, args] = [Number(m[1]), m[2]];
|
|
484
|
+
if (keep.pids.includes(pid) || keep.markers.some((mark) => pathForms(mark).some((f) => args.includes(f)))) continue;
|
|
485
|
+
if (pathForms(desk).some((f) => args.includes(f))) out.push(pid);
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
async function releaseDesk(fullName, agentSlug, port, keep = { pids: [], markers: [] }) {
|
|
478
490
|
const desk = deskDir(fullName, agentSlug);
|
|
491
|
+
const spare = [process.pid, ...keep.pids];
|
|
479
492
|
if (process.platform === "win32") {
|
|
480
|
-
const
|
|
493
|
+
const psList = (paths) => paths.flatMap(pathForms).map((d) => `'${d.toLowerCase().replaceAll("'", "''")}'`).join(", ");
|
|
481
494
|
const script = `
|
|
482
495
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
496
|
+
$spare = @(${spare.join(", ")})
|
|
483
497
|
$seed = @()
|
|
484
498
|
Get-NetTCPConnection -LocalPort ${port} -State Listen | ForEach-Object { $seed += [int]$_.OwningProcess }
|
|
485
|
-
$desks = @(${
|
|
499
|
+
$desks = @(${psList([desk])})
|
|
500
|
+
$marks = @(${psList(keep.markers)})
|
|
486
501
|
$all = Get-CimInstance Win32_Process
|
|
487
502
|
$byId = @{}; foreach ($p in $all) { $byId[[int]$p.ProcessId] = $p }
|
|
488
503
|
foreach ($p in $all) {
|
|
489
504
|
if (-not $p.CommandLine) { continue }
|
|
490
505
|
$cmd = $p.CommandLine.ToLower()
|
|
506
|
+
$ours = $false
|
|
507
|
+
foreach ($m in $marks) { if ($cmd.Contains($m)) { $ours = $true } }
|
|
508
|
+
if ($ours) { continue }
|
|
491
509
|
foreach ($d in $desks) { if ($cmd.Contains($d)) { $seed += [int]$p.ProcessId } }
|
|
492
510
|
}
|
|
493
511
|
$launchers = @(${LAUNCHERS.map((n) => `'${n}'`).join(", ")})
|
|
@@ -495,7 +513,7 @@ $kill = New-Object 'System.Collections.Generic.HashSet[int]'
|
|
|
495
513
|
foreach ($id in $seed) {
|
|
496
514
|
$cur = $byId[$id]
|
|
497
515
|
$first = $true
|
|
498
|
-
while ($cur -and [int]$cur.ProcessId -
|
|
516
|
+
while ($cur -and -not ($spare -contains [int]$cur.ProcessId) -and ($first -or $launchers -contains $cur.Name.ToLower())) {
|
|
499
517
|
[void]$kill.Add([int]$cur.ProcessId)
|
|
500
518
|
$first = $false
|
|
501
519
|
$cur = $byId[[int]$cur.ParentProcessId]
|
|
@@ -506,8 +524,11 @@ Write-Output $kill.Count`;
|
|
|
506
524
|
await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { timeoutMs: 3e4 }).catch(() => void 0);
|
|
507
525
|
return;
|
|
508
526
|
}
|
|
509
|
-
const
|
|
510
|
-
|
|
527
|
+
const listening = await run("sh", ["-c", `lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`]).catch(() => "");
|
|
528
|
+
const listing = await run("ps", ["-A", "-ww", "-o", "pid=,args="]).catch(() => "");
|
|
529
|
+
const pids = /* @__PURE__ */ new Set([...listening.split(/\s+/).map(Number), ...leftoversInDesk(listing, desk, keep)]);
|
|
530
|
+
for (const pid of pids) {
|
|
531
|
+
if (!pid || spare.includes(pid)) continue;
|
|
511
532
|
try {
|
|
512
533
|
process.kill(pid, "SIGKILL");
|
|
513
534
|
} catch {
|
|
@@ -516,6 +537,7 @@ Write-Output $kill.Count`;
|
|
|
516
537
|
}
|
|
517
538
|
|
|
518
539
|
// server/agentRunner.ts
|
|
540
|
+
import fs3 from "node:fs";
|
|
519
541
|
import path3 from "node:path";
|
|
520
542
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
521
543
|
var InputQueue = class {
|
|
@@ -544,54 +566,43 @@ var InputQueue = class {
|
|
|
544
566
|
};
|
|
545
567
|
}
|
|
546
568
|
};
|
|
547
|
-
var
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
{
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
function isInside(child, parent) {
|
|
557
|
-
const rel2 = path3.relative(parent, child);
|
|
558
|
-
return rel2 === "" || !rel2.startsWith("..") && !path3.isAbsolute(rel2);
|
|
569
|
+
var allowAll = async (_toolName, input) => ({ behavior: "allow", updatedInput: input });
|
|
570
|
+
function screenshotFile(cwd, text) {
|
|
571
|
+
const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
|
|
572
|
+
if (!file) return null;
|
|
573
|
+
try {
|
|
574
|
+
return { data: fs3.readFileSync(path3.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
|
|
575
|
+
} catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
559
578
|
}
|
|
560
|
-
var
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
for (const rule of role === "qa" ? [...BLOCKED_COMMANDS, ...QA_BLOCKED] : BLOCKED_COMMANDS) {
|
|
578
|
-
if (rule.re.test(cmd)) return { behavior: "deny", message: `Blocked by cubefarm: ${rule.why}.` };
|
|
579
|
-
}
|
|
580
|
-
if (defaultBranchPush.test(cmd)) {
|
|
581
|
-
return { behavior: "deny", message: "Push your own branch and open a PR; pushing to the default branch is not allowed." };
|
|
582
|
-
}
|
|
583
|
-
if (blockedPath && /\b(rm|del|rmdir|mv|move|Remove-Item)\b/i.test(cmd) && !isInside(path3.resolve(blockedPath), cwd)) {
|
|
584
|
-
return { behavior: "deny", message: `That command touches ${blockedPath}, which is outside your worktree.` };
|
|
585
|
-
}
|
|
579
|
+
var IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg" };
|
|
580
|
+
function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
581
|
+
let names;
|
|
582
|
+
try {
|
|
583
|
+
names = fs3.readdirSync(dir);
|
|
584
|
+
} catch {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
const found2 = [];
|
|
588
|
+
for (const name of names.sort()) {
|
|
589
|
+
const mime = IMAGE_MIME[path3.extname(name).toLowerCase()];
|
|
590
|
+
const file = path3.join(dir, name);
|
|
591
|
+
if (!mime || seen.has(name)) continue;
|
|
592
|
+
try {
|
|
593
|
+
if (now2 - fs3.statSync(file).mtimeMs < 1e3) continue;
|
|
594
|
+
} catch {
|
|
595
|
+
continue;
|
|
586
596
|
}
|
|
587
|
-
|
|
588
|
-
}
|
|
597
|
+
found2.push({ name, file, mime });
|
|
598
|
+
}
|
|
599
|
+
return found2;
|
|
589
600
|
}
|
|
590
601
|
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
591
602
|
function rel(cwd, p) {
|
|
592
603
|
const s = String(p ?? "");
|
|
593
604
|
if (!s) return "";
|
|
594
|
-
const r = path3.relative(cwd, s);
|
|
605
|
+
const r = path3.relative(cwd, path3.resolve(cwd, s));
|
|
595
606
|
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s;
|
|
596
607
|
}
|
|
597
608
|
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -628,6 +639,12 @@ function describeTool(cwd, name, input) {
|
|
|
628
639
|
return `Subagent: ${input.description ?? ""}`;
|
|
629
640
|
case "TodoWrite":
|
|
630
641
|
return "Update todo list";
|
|
642
|
+
case "ToolSearch":
|
|
643
|
+
return `Load tools ${String(input.query ?? "").replace(/^select:/, "")}`;
|
|
644
|
+
case "apply_patch": {
|
|
645
|
+
const files = [...String(input.command ?? "").matchAll(/^\*\*\* (Add|Update|Delete) File: (.+)$/gm)].map((m) => `${m[1] === "Update" ? "" : `${m[1].toLowerCase()} `}${rel(cwd, m[2].trim())}`);
|
|
646
|
+
return `Edit ${clip(files.join(", ") || "files", 200)}`;
|
|
647
|
+
}
|
|
631
648
|
}
|
|
632
649
|
if (name.startsWith("mcp__playwright__")) {
|
|
633
650
|
const action = name.replace("mcp__playwright__browser_", "").replace("mcp__playwright__", "");
|
|
@@ -676,6 +693,7 @@ function resultText(content) {
|
|
|
676
693
|
var lineCount = (text) => text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "").length;
|
|
677
694
|
function summariseResult(cwd, tool2, text) {
|
|
678
695
|
if (tool2 === "Edit" || tool2 === "MultiEdit" || tool2 === "NotebookEdit") return [{ kind: "result", text: " \u23BF Updated" }];
|
|
696
|
+
if (tool2 === "apply_patch" && /Success\. Updated the following files/.test(text)) return [{ kind: "result", text: " \u23BF Updated" }];
|
|
679
697
|
if (tool2 === "Write") return [{ kind: "result", text: " \u23BF Saved" }];
|
|
680
698
|
if (tool2 === "Read") return [{ kind: "result", text: ` \u23BF Read ${lineCount(text)} lines` }];
|
|
681
699
|
if (tool2 === "Glob") return [{ kind: "result", text: ` \u23BF Found ${lineCount(text)} files` }];
|
|
@@ -685,7 +703,7 @@ function summariseResult(cwd, tool2, text) {
|
|
|
685
703
|
if (lines.length > 3) shown.push({ kind: "result", text: ` \u2026 +${lines.length - 3} lines` });
|
|
686
704
|
return shown;
|
|
687
705
|
}
|
|
688
|
-
function startSession(opts, callbacks
|
|
706
|
+
function startSession(opts, callbacks) {
|
|
689
707
|
let done = false;
|
|
690
708
|
const cb = {
|
|
691
709
|
...callbacks,
|
|
@@ -717,8 +735,6 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
717
735
|
if (opts.office) mcpServers.office = opts.office.server;
|
|
718
736
|
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"];
|
|
719
737
|
if (opts.role === "ceo") disallowedTools.push("Bash", "PowerShell", "NotebookEdit");
|
|
720
|
-
const escaped = defaultBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
721
|
-
const defaultBranchPush = new RegExp(`\\bgit\\s+push\\b[^\\n]*\\s(HEAD:)?(refs/heads/)?${escaped}(\\s|$)`);
|
|
722
738
|
const options = {
|
|
723
739
|
cwd: opts.cwd,
|
|
724
740
|
additionalDirectories: opts.additionalDirectories,
|
|
@@ -726,11 +742,8 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
726
742
|
env,
|
|
727
743
|
model: opts.model || void 0,
|
|
728
744
|
effort: opts.effort,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
// no user-level servers or plugins from the host machine.
|
|
732
|
-
strictMcpConfig: true,
|
|
733
|
-
settings: { disableClaudeAiConnectors: true },
|
|
745
|
+
// The manager's own setup (settings, skills, plugins, MCP servers, connectors), plus the office's servers below.
|
|
746
|
+
settingSources: ["user", "project", "local"],
|
|
734
747
|
systemPrompt: { type: "preset", preset: "claude_code", append: opts.systemAppend },
|
|
735
748
|
mcpServers,
|
|
736
749
|
disallowedTools,
|
|
@@ -739,7 +752,8 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
739
752
|
const line = data.trim();
|
|
740
753
|
if (line && /error|fail/i.test(line)) cb.log([{ kind: "error", text: clip(line, 200) }]);
|
|
741
754
|
},
|
|
742
|
-
|
|
755
|
+
permissionMode: "acceptEdits",
|
|
756
|
+
canUseTool: allowAll,
|
|
743
757
|
...opts.outputSchema ? { outputFormat: { type: "json_schema", schema: opts.outputSchema } } : {}
|
|
744
758
|
};
|
|
745
759
|
let emptyTurn;
|
|
@@ -771,7 +785,7 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
771
785
|
cb.sessionId(msg.session_id);
|
|
772
786
|
const mcp = msg.mcp_servers.map((s) => `${s.name}:${s.status}`).join(", ");
|
|
773
787
|
cb.log([
|
|
774
|
-
{ kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort
|
|
788
|
+
{ kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort` },
|
|
775
789
|
{ kind: "system", text: ` cwd ${msg.cwd}${mcp ? ` \xB7 mcp ${mcp}` : ""}` }
|
|
776
790
|
]);
|
|
777
791
|
} else if (msg.subtype === "api_retry") {
|
|
@@ -805,12 +819,12 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
805
819
|
} else if (block.type === "thinking") {
|
|
806
820
|
out.push({ kind: "thinking", text: "\u273B Thinking\u2026" });
|
|
807
821
|
} else if (block.type === "tool_use") {
|
|
808
|
-
const
|
|
822
|
+
const toolInput2 = block.input ?? {};
|
|
809
823
|
toolNames.set(block.id, block.name);
|
|
810
|
-
out.push({ kind: "tool", tool: block.name, text: `\u23FA ${describeTool(opts.cwd, block.name,
|
|
811
|
-
if (block.name === "TodoWrite") out.push(...todoLines(
|
|
824
|
+
out.push({ kind: "tool", tool: block.name, text: `\u23FA ${describeTool(opts.cwd, block.name, toolInput2)}` });
|
|
825
|
+
if (block.name === "TodoWrite") out.push(...todoLines(toolInput2));
|
|
812
826
|
cb.tool(block.name);
|
|
813
|
-
if (block.name === "mcp__playwright__browser_navigate" && typeof
|
|
827
|
+
if (block.name === "mcp__playwright__browser_navigate" && typeof toolInput2.url === "string") cb.browserUrl(toolInput2.url);
|
|
814
828
|
}
|
|
815
829
|
}
|
|
816
830
|
if (out.length) cb.log(out);
|
|
@@ -825,13 +839,17 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
825
839
|
if (block.type !== "tool_result") continue;
|
|
826
840
|
const name = toolNames.get(block.tool_use_id) ?? "";
|
|
827
841
|
const text = resultText(block.content);
|
|
842
|
+
let images = 0;
|
|
828
843
|
if (Array.isArray(block.content)) {
|
|
829
844
|
for (const part of block.content) {
|
|
830
845
|
if (part.type === "image" && part.source.type === "base64") {
|
|
846
|
+
images += 1;
|
|
831
847
|
cb.screenshot(Buffer.from(part.source.data, "base64"), part.source.media_type);
|
|
832
848
|
}
|
|
833
849
|
}
|
|
834
850
|
}
|
|
851
|
+
const saved = name === "mcp__playwright__browser_take_screenshot" && !images ? screenshotFile(opts.cwd, text) : null;
|
|
852
|
+
if (saved) cb.screenshot(saved.data, saved.mime);
|
|
835
853
|
if (name.startsWith("mcp__playwright__")) {
|
|
836
854
|
const url = text.match(/Page URL:\s*(\S+)/);
|
|
837
855
|
if (url) cb.browserUrl(url[1]);
|
|
@@ -883,12 +901,1029 @@ function startSession(opts, callbacks, defaultBranch) {
|
|
|
883
901
|
};
|
|
884
902
|
}
|
|
885
903
|
|
|
886
|
-
// server/
|
|
904
|
+
// server/cliRunner.ts
|
|
905
|
+
import crypto3 from "node:crypto";
|
|
906
|
+
import fs6 from "node:fs";
|
|
907
|
+
import path7 from "node:path";
|
|
908
|
+
import { pathToFileURL } from "node:url";
|
|
909
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
910
|
+
|
|
911
|
+
// server/clis.ts
|
|
912
|
+
import fs4 from "node:fs";
|
|
913
|
+
import { createRequire } from "node:module";
|
|
914
|
+
import path4 from "node:path";
|
|
915
|
+
var CLIS = [
|
|
916
|
+
{ id: "claude", label: "Claude Code", command: "claude", integrated: true },
|
|
917
|
+
{ id: "codex", label: "Codex", command: "codex", integrated: false },
|
|
918
|
+
{ id: "opencode", label: "OpenCode", command: "opencode", integrated: false }
|
|
919
|
+
];
|
|
920
|
+
var isCli = (v) => CLIS.some((c) => c.id === v);
|
|
921
|
+
var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
|
|
922
|
+
var WIN = process.platform === "win32";
|
|
923
|
+
function resolveCommand(name, env = process.env) {
|
|
924
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
925
|
+
const dirs = (env[key] ?? "").split(path4.delimiter).filter(Boolean);
|
|
926
|
+
const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
|
|
927
|
+
for (const dir of dirs) {
|
|
928
|
+
for (const ext of exts) {
|
|
929
|
+
const file = path4.join(dir, name + ext.toLowerCase());
|
|
930
|
+
try {
|
|
931
|
+
const st = fs4.statSync(file);
|
|
932
|
+
if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
function unwrapCmdShim(shimPath, text) {
|
|
940
|
+
const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
|
|
941
|
+
if (!line) return null;
|
|
942
|
+
const dir = path4.dirname(shimPath);
|
|
943
|
+
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path4.join(dir, m[1]));
|
|
944
|
+
const target = targets[targets.length - 1];
|
|
945
|
+
if (!target) return null;
|
|
946
|
+
if (/\.exe$/i.test(target)) return { file: target, args: [] };
|
|
947
|
+
if (!/\.[cm]?js$/i.test(target)) return null;
|
|
948
|
+
const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
|
|
949
|
+
const nodeFlags = line.includes('"%_prog%"') ? between.trim().split(/\s+/).filter((f) => f.startsWith("--")) : [];
|
|
950
|
+
const localNode = path4.join(dir, "node.exe");
|
|
951
|
+
return { file: fs4.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
|
|
952
|
+
}
|
|
953
|
+
var bundled;
|
|
954
|
+
function bundledClaude() {
|
|
955
|
+
if (bundled !== void 0) return bundled;
|
|
956
|
+
bundled = null;
|
|
957
|
+
let sdk;
|
|
958
|
+
try {
|
|
959
|
+
sdk = createRequire(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
const { platform, arch } = process;
|
|
964
|
+
const report = platform === "linux" ? process.report?.getReport() : void 0;
|
|
965
|
+
const musl = platform === "linux" && !report?.header?.glibcVersionRuntime;
|
|
966
|
+
const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
967
|
+
for (const target of targets) {
|
|
968
|
+
try {
|
|
969
|
+
bundled = createRequire(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
|
|
970
|
+
return bundled;
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
function commandFor(id) {
|
|
977
|
+
const bundled2 = id === "claude" ? bundledClaude() : null;
|
|
978
|
+
if (bundled2) return { file: bundled2, args: [] };
|
|
979
|
+
const def = CLIS.find((c) => c.id === id);
|
|
980
|
+
const found2 = def && resolveCommand(def.command);
|
|
981
|
+
if (!found2) return null;
|
|
982
|
+
if (WIN && /\.(cmd|bat)$/i.test(found2)) {
|
|
983
|
+
try {
|
|
984
|
+
return unwrapCmdShim(found2, fs4.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
|
|
985
|
+
} catch {
|
|
986
|
+
return { file: found2, args: [] };
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return { file: found2, args: [] };
|
|
990
|
+
}
|
|
991
|
+
async function detectClis() {
|
|
992
|
+
return Promise.all(
|
|
993
|
+
CLIS.map(async (c) => {
|
|
994
|
+
const cmd = commandFor(c.id);
|
|
995
|
+
const version = cmd ? await run(cmd.file, [...cmd.args, "--version"], { timeoutMs: 2e4 }).catch(() => null) : null;
|
|
996
|
+
return { id: c.id, label: c.label, installed: !!cmd, version: version?.split(/\r?\n/)[0].trim().slice(0, 60) || null, integrated: c.integrated };
|
|
997
|
+
})
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
|
|
1001
|
+
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
|
|
1002
|
+
function codexHookCommand(node, script, win = WIN) {
|
|
1003
|
+
return win ? `node "${script}"` : [node, script].map((s) => `'${s.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
1004
|
+
}
|
|
1005
|
+
function launchArgs(id, ctx) {
|
|
1006
|
+
switch (id) {
|
|
1007
|
+
case "claude": {
|
|
1008
|
+
const disallowed = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", ...ctx.role === "ceo" ? ["Bash", "PowerShell", "NotebookEdit"] : []];
|
|
1009
|
+
const args = [
|
|
1010
|
+
...ctx.resumeId ? ["--resume", ctx.resumeId] : ["--session-id", ctx.sessionId],
|
|
1011
|
+
...ctx.model ? ["--model", ctx.model] : [],
|
|
1012
|
+
...ctx.effort ? ["--effort", ctx.effort] : [],
|
|
1013
|
+
// The office's PreToolUse hook answers every permission question (allow, or deny with an office rule).
|
|
1014
|
+
"--permission-mode",
|
|
1015
|
+
"acceptEdits",
|
|
1016
|
+
// On top of the manager's own setup (settings, skills, plugins, MCP servers): the office's hooks and servers.
|
|
1017
|
+
"--settings",
|
|
1018
|
+
ctx.files.settings,
|
|
1019
|
+
...ctx.files.mcp ? ["--mcp-config", ctx.files.mcp] : [],
|
|
1020
|
+
"--append-system-prompt-file",
|
|
1021
|
+
ctx.files.system,
|
|
1022
|
+
"--name",
|
|
1023
|
+
ctx.name,
|
|
1024
|
+
...ctx.additionalDirectories.flatMap((d) => ["--add-dir", d]),
|
|
1025
|
+
"--disallowedTools",
|
|
1026
|
+
...disallowed,
|
|
1027
|
+
"--",
|
|
1028
|
+
ctx.prompt
|
|
1029
|
+
];
|
|
1030
|
+
return { args, env: { DISABLE_AUTOUPDATER: "1" } };
|
|
1031
|
+
}
|
|
1032
|
+
case "codex": {
|
|
1033
|
+
const toml = (s) => JSON.stringify(s);
|
|
1034
|
+
const args = [
|
|
1035
|
+
"--no-alt-screen",
|
|
1036
|
+
"-c",
|
|
1037
|
+
`notify=[${[process.execPath, ctx.notify.script, ctx.notify.url].map(toml).join(",")}]`,
|
|
1038
|
+
"-c",
|
|
1039
|
+
`developer_instructions=${toml(ctx.systemAppend)}`,
|
|
1040
|
+
...ctx.model ? ["-m", ctx.model] : [],
|
|
1041
|
+
...ctx.effort ? ["-c", `model_reasoning_effort=${toml(EFFORT_CODEX[ctx.effort])}`] : [],
|
|
1042
|
+
...ctx.browser ? ["-c", `mcp_servers.playwright={command=${toml(ctx.browser.command)},args=[${ctx.browser.args.map(toml).join(",")}]}`] : [],
|
|
1043
|
+
...CODEX_HOOK_EVENTS.flatMap((e) => ["-c", `hooks.${e}=[{hooks=[{type="command",command=${toml(codexHookCommand(process.execPath, ctx.codexHook))},timeout=10}]}]`]),
|
|
1044
|
+
// Like the manager's own Codex, but it can't stop to ask: no approvals and no sandbox (its sandbox can't reach
|
|
1045
|
+
// the credential store, so git and gh fail in it).
|
|
1046
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
1047
|
+
];
|
|
1048
|
+
return { args: ctx.resumeId ? ["resume", ...args, ctx.resumeId, ctx.prompt] : [...args, "--", ctx.prompt], env: { CUBEFARM_HOOK_URL: ctx.notify.url } };
|
|
1049
|
+
}
|
|
1050
|
+
case "opencode": {
|
|
1051
|
+
const config = {
|
|
1052
|
+
plugin: [ctx.plugin],
|
|
1053
|
+
instructions: [ctx.files.system],
|
|
1054
|
+
autoupdate: false,
|
|
1055
|
+
// several agents starting at once must not each reinstall it
|
|
1056
|
+
permission: { edit: "allow", bash: "allow", webfetch: "allow" },
|
|
1057
|
+
// it can't stop to ask either
|
|
1058
|
+
...ctx.browser ? { mcp: { playwright: { type: "local", command: [ctx.browser.command, ...ctx.browser.args], enabled: true } } } : {}
|
|
1059
|
+
};
|
|
1060
|
+
const args = [
|
|
1061
|
+
"--auto",
|
|
1062
|
+
...ctx.model ? ["--model", ctx.model] : [],
|
|
1063
|
+
...ctx.resumeId ? ["--session", ctx.resumeId] : [],
|
|
1064
|
+
"--prompt",
|
|
1065
|
+
ctx.prompt
|
|
1066
|
+
];
|
|
1067
|
+
return { args, env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(config), CUBEFARM_NOTIFY_URL: ctx.notify.url } };
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
var TRUST_PROMPT = /Quick safety check|Do you trust the (files|contents) (in|of) this|trust this folder|allow Codex to work in this folder/i;
|
|
1072
|
+
function trustKey(screen) {
|
|
1073
|
+
if (!TRUST_PROMPT.test(screen)) return null;
|
|
1074
|
+
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
1075
|
+
if (/^(yes|trust|continue|proceed)\b/i.test(selected.trim())) return "enter";
|
|
1076
|
+
if (/^(no|exit|quit|cancel)\b/i.test(selected.trim())) return "down";
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
function hookReviewKey(screen) {
|
|
1080
|
+
if (!/Hooks need review/i.test(screen)) return null;
|
|
1081
|
+
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
1082
|
+
return /^continue without trusting/i.test(selected.trim()) ? "enter" : "down";
|
|
1083
|
+
}
|
|
1084
|
+
function interruptions(screen) {
|
|
1085
|
+
return screen.split("\n").filter((l) => /\binterrupted\b/i.test(l)).length;
|
|
1086
|
+
}
|
|
1087
|
+
var STATUSLINE_SOURCE = String.raw`// cubefarm: Claude Code's status line. Forwards the session's status to the office and prints the office's line.
|
|
1088
|
+
const url = process.argv[2];
|
|
1089
|
+
let body = '';
|
|
1090
|
+
process.stdin.on('data', (c) => (body += c));
|
|
1091
|
+
process.stdin.on('end', async () => {
|
|
1092
|
+
try {
|
|
1093
|
+
const status = JSON.parse(body);
|
|
1094
|
+
const res = await fetch(url, {
|
|
1095
|
+
method: 'POST',
|
|
1096
|
+
headers: { 'content-type': 'application/json' },
|
|
1097
|
+
body: JSON.stringify({ hook_event_name: 'StatusLine', session_id: status.session_id, cost: status.cost, rate_limits: status.rate_limits }),
|
|
1098
|
+
signal: AbortSignal.timeout(2000),
|
|
1099
|
+
});
|
|
1100
|
+
const out = await res.json();
|
|
1101
|
+
process.stdout.write(String(out.statusLine ?? ''));
|
|
1102
|
+
} catch {
|
|
1103
|
+
process.stdout.write('cubefarm');
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
`;
|
|
1107
|
+
var NOTIFY_SOURCE = String.raw`// cubefarm: Codex's notify program. Tells the office a turn is complete.
|
|
1108
|
+
const [url, payload] = process.argv.slice(2);
|
|
1109
|
+
let event = {};
|
|
1110
|
+
try {
|
|
1111
|
+
event = JSON.parse(payload ?? '{}');
|
|
1112
|
+
} catch {}
|
|
1113
|
+
if (event.type === 'agent-turn-complete') {
|
|
1114
|
+
const input = String((event['input-messages'] ?? [])[0] ?? '').slice(0, 300);
|
|
1115
|
+
fetch(url, {
|
|
1116
|
+
method: 'POST',
|
|
1117
|
+
headers: { 'content-type': 'application/json' },
|
|
1118
|
+
body: JSON.stringify({ hook_event_name: 'TurnComplete', session_id: event['thread-id'] ?? null, last_assistant_message: event['last-assistant-message'] ?? '', input }),
|
|
1119
|
+
signal: AbortSignal.timeout(3000),
|
|
1120
|
+
}).catch(() => {});
|
|
1121
|
+
}
|
|
1122
|
+
`;
|
|
1123
|
+
var CODEX_HOOK_SOURCE = String.raw`// cubefarm: Codex's hooks. Tells the office what Codex is doing; never changes what it does.
|
|
1124
|
+
const url = process.env.CUBEFARM_HOOK_URL;
|
|
1125
|
+
const chunks = [];
|
|
1126
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
1127
|
+
process.stdin.on('end', async () => {
|
|
1128
|
+
try {
|
|
1129
|
+
if (url) await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: Buffer.concat(chunks), signal: AbortSignal.timeout(5000) });
|
|
1130
|
+
} catch {}
|
|
1131
|
+
process.stdout.write('{}');
|
|
1132
|
+
});
|
|
1133
|
+
`;
|
|
1134
|
+
var OPENCODE_PLUGIN_SOURCE = String.raw`// cubefarm: tells the office when OpenCode's session goes idle.
|
|
1135
|
+
export default async function CubefarmPlugin({ client } = {}) {
|
|
1136
|
+
const url = process.env.CUBEFARM_NOTIFY_URL;
|
|
1137
|
+
if (!url) return {};
|
|
1138
|
+
let root;
|
|
1139
|
+
const post = (body) =>
|
|
1140
|
+
fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(3000) }).catch(() => {});
|
|
1141
|
+
const lastReply = async (id) => {
|
|
1142
|
+
try {
|
|
1143
|
+
const res = await client.session.messages({ path: { id } });
|
|
1144
|
+
const reply = (res?.data ?? []).filter((m) => m?.info?.role === 'assistant').pop();
|
|
1145
|
+
return (reply?.parts ?? []).filter((p) => p?.type === 'text').map((p) => p.text).join('\n').trim();
|
|
1146
|
+
} catch {
|
|
1147
|
+
return '';
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
return {
|
|
1151
|
+
event: async ({ event }) => {
|
|
1152
|
+
const props = event?.properties ?? {};
|
|
1153
|
+
if (event?.type === 'session.created' && !props.info?.parentID && !root) root = props.info?.id;
|
|
1154
|
+
const sessionID = props.sessionID ?? props.info?.id;
|
|
1155
|
+
if (event?.type === 'session.idle' && (!root || sessionID === root)) {
|
|
1156
|
+
post({ hook_event_name: 'TurnComplete', session_id: sessionID ?? null, last_assistant_message: sessionID ? await lastReply(sessionID) : '' });
|
|
1157
|
+
}
|
|
1158
|
+
if (event?.type === 'session.error') post({ hook_event_name: 'TurnError', session_id: sessionID ?? null, error: String(props.error?.data?.message ?? props.error?.name ?? 'error') });
|
|
1159
|
+
},
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
`;
|
|
1163
|
+
|
|
1164
|
+
// server/ptyClient.ts
|
|
1165
|
+
import crypto2 from "node:crypto";
|
|
1166
|
+
import fs5 from "node:fs";
|
|
1167
|
+
import net from "node:net";
|
|
1168
|
+
import path6 from "node:path";
|
|
887
1169
|
import { spawn } from "node:child_process";
|
|
1170
|
+
|
|
1171
|
+
// server/ptyProtocol.ts
|
|
888
1172
|
import crypto from "node:crypto";
|
|
889
|
-
import
|
|
890
|
-
|
|
891
|
-
|
|
1173
|
+
import path5 from "node:path";
|
|
1174
|
+
var PTY_PROTOCOL = 1;
|
|
1175
|
+
function hostSocket(home, platform = process.platform) {
|
|
1176
|
+
if (platform !== "win32") return path5.join(home, "pty.sock");
|
|
1177
|
+
const tag = crypto.createHash("sha1").update(path5.resolve(home).toLowerCase()).digest("hex").slice(0, 12);
|
|
1178
|
+
return `\\\\.\\pipe\\cubefarm-pty-${tag}`;
|
|
1179
|
+
}
|
|
1180
|
+
var encode = (m) => `${JSON.stringify(m)}
|
|
1181
|
+
`;
|
|
1182
|
+
function readMessages(sock, onMessage2) {
|
|
1183
|
+
let buf = "";
|
|
1184
|
+
sock.setEncoding("utf8");
|
|
1185
|
+
sock.on("data", (chunk) => {
|
|
1186
|
+
buf += chunk;
|
|
1187
|
+
let nl;
|
|
1188
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
1189
|
+
const line = buf.slice(0, nl);
|
|
1190
|
+
buf = buf.slice(nl + 1);
|
|
1191
|
+
if (!line) continue;
|
|
1192
|
+
let m;
|
|
1193
|
+
try {
|
|
1194
|
+
m = JSON.parse(line);
|
|
1195
|
+
} catch {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
onMessage2(m);
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// server/ptyClient.ts
|
|
1204
|
+
var nodePty = await import("@lydell/node-pty").catch((err) => {
|
|
1205
|
+
console.warn(` terminals unavailable: ${err.message}`);
|
|
1206
|
+
return null;
|
|
1207
|
+
});
|
|
1208
|
+
var terminalsAvailable = nodePty !== null;
|
|
1209
|
+
var conn = null;
|
|
1210
|
+
var leaving = false;
|
|
1211
|
+
var hookPort = 0;
|
|
1212
|
+
var hostPid = 0;
|
|
1213
|
+
var found = [];
|
|
1214
|
+
var remote = /* @__PURE__ */ new Map();
|
|
1215
|
+
var hookHandler = () => ({});
|
|
1216
|
+
var send = (m) => {
|
|
1217
|
+
if (conn && !conn.destroyed) conn.write(encode(m));
|
|
1218
|
+
};
|
|
1219
|
+
var RemotePty = class {
|
|
1220
|
+
constructor(id) {
|
|
1221
|
+
this.id = id;
|
|
1222
|
+
remote.set(id, this);
|
|
1223
|
+
}
|
|
1224
|
+
id;
|
|
1225
|
+
pid = 0;
|
|
1226
|
+
dataFns = [];
|
|
1227
|
+
exitFns = [];
|
|
1228
|
+
done = false;
|
|
1229
|
+
write(data) {
|
|
1230
|
+
send({ op: "write", id: this.id, data });
|
|
1231
|
+
}
|
|
1232
|
+
resize(cols, rows) {
|
|
1233
|
+
send({ op: "resize", id: this.id, cols, rows });
|
|
1234
|
+
}
|
|
1235
|
+
kill() {
|
|
1236
|
+
send({ op: "kill", id: this.id });
|
|
1237
|
+
}
|
|
1238
|
+
setMeta(meta) {
|
|
1239
|
+
send({ op: "meta", id: this.id, meta });
|
|
1240
|
+
}
|
|
1241
|
+
onData(cb) {
|
|
1242
|
+
this.dataFns.push(cb);
|
|
1243
|
+
}
|
|
1244
|
+
onExit(cb) {
|
|
1245
|
+
this.exitFns.push(cb);
|
|
1246
|
+
}
|
|
1247
|
+
data(data) {
|
|
1248
|
+
for (const fn of this.dataFns) fn(data);
|
|
1249
|
+
}
|
|
1250
|
+
exit(code) {
|
|
1251
|
+
if (this.done) return;
|
|
1252
|
+
this.done = true;
|
|
1253
|
+
remote.delete(this.id);
|
|
1254
|
+
for (const fn of this.exitFns) fn(code);
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
function onMessage(m) {
|
|
1258
|
+
switch (m.op) {
|
|
1259
|
+
case "spawned": {
|
|
1260
|
+
const p = remote.get(m.id);
|
|
1261
|
+
if (p) p.pid = m.pid;
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
case "failed":
|
|
1265
|
+
console.warn(` could not start a terminal: ${m.error}`);
|
|
1266
|
+
return remote.get(m.id)?.exit(-1);
|
|
1267
|
+
case "data":
|
|
1268
|
+
return remote.get(m.id)?.data(m.data);
|
|
1269
|
+
case "exit":
|
|
1270
|
+
return remote.get(m.id)?.exit(m.code);
|
|
1271
|
+
case "hook":
|
|
1272
|
+
void Promise.resolve().then(() => hookHandler(m.token, m.body)).catch(() => ({})).then((answer) => send({ op: "hookReply", rid: m.rid, answer }));
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function connect(socket, secret) {
|
|
1277
|
+
return new Promise((resolve) => {
|
|
1278
|
+
const sock = net.connect(socket);
|
|
1279
|
+
let ready = false;
|
|
1280
|
+
const fail = () => {
|
|
1281
|
+
clearTimeout(timer);
|
|
1282
|
+
sock.destroy();
|
|
1283
|
+
resolve(null);
|
|
1284
|
+
};
|
|
1285
|
+
const timer = setTimeout(fail, 2e3);
|
|
1286
|
+
sock.once("error", fail);
|
|
1287
|
+
sock.once("connect", () => sock.write(encode({ op: "hello", secret, version: PTY_PROTOCOL })));
|
|
1288
|
+
readMessages(sock, (m) => {
|
|
1289
|
+
if (ready) return onMessage(m);
|
|
1290
|
+
if (m.op !== "ready") return;
|
|
1291
|
+
ready = true;
|
|
1292
|
+
clearTimeout(timer);
|
|
1293
|
+
sock.removeListener("error", fail);
|
|
1294
|
+
sock.on("error", () => void 0);
|
|
1295
|
+
resolve({ ready: m, sock });
|
|
1296
|
+
});
|
|
1297
|
+
sock.on("close", () => {
|
|
1298
|
+
if (conn !== sock) return;
|
|
1299
|
+
conn = null;
|
|
1300
|
+
if (leaving) return;
|
|
1301
|
+
console.warn(" the terminal keeper stopped; running agents lost their terminals");
|
|
1302
|
+
for (const p of [...remote.values()]) p.exit(-1);
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
var runtimeFlags = () => process.execArgv.filter((a, i, all) => !/^--(inspect|watch|debug)/.test(a) && !/^--(inspect|watch|debug)/.test(all[i - 1] ?? ""));
|
|
1307
|
+
function launch(socket, secretFile) {
|
|
1308
|
+
const entry = path6.join(import.meta.dirname, `ptyHost${path6.extname(import.meta.filename)}`);
|
|
1309
|
+
spawn(process.execPath, [...runtimeFlags(), entry, "launch", socket, secretFile], { detached: true, stdio: "ignore", windowsHide: true }).once("error", () => void 0).unref();
|
|
1310
|
+
}
|
|
1311
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1312
|
+
async function startKeeper(home, onHook) {
|
|
1313
|
+
hookHandler = onHook;
|
|
1314
|
+
if (!nodePty) return [];
|
|
1315
|
+
const socket = hostSocket(home);
|
|
1316
|
+
const secretFile = path6.join(home, "pty.secret");
|
|
1317
|
+
try {
|
|
1318
|
+
if (!fs5.existsSync(secretFile)) {
|
|
1319
|
+
fs5.mkdirSync(home, { recursive: true });
|
|
1320
|
+
fs5.writeFileSync(secretFile, crypto2.randomBytes(24).toString("hex"), { mode: 384 });
|
|
1321
|
+
}
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
console.warn(` terminal keeper unavailable: ${err.message}`);
|
|
1324
|
+
return [];
|
|
1325
|
+
}
|
|
1326
|
+
const secret = fs5.readFileSync(secretFile, "utf8").trim();
|
|
1327
|
+
let hello = await connect(socket, secret);
|
|
1328
|
+
if (hello && hello.ready.version !== PTY_PROTOCOL) {
|
|
1329
|
+
console.log(` replacing the terminal keeper (v${hello.ready.version}); the ${hello.ready.ptys.length} terminal(s) it held are stopped`);
|
|
1330
|
+
hello.sock.end(encode({ op: "shutdown" }));
|
|
1331
|
+
hello = null;
|
|
1332
|
+
await sleep(3500);
|
|
1333
|
+
}
|
|
1334
|
+
if (!hello) {
|
|
1335
|
+
launch(socket, secretFile);
|
|
1336
|
+
for (let i = 0; i < 30 && !hello; i++) {
|
|
1337
|
+
await sleep(200);
|
|
1338
|
+
hello = await connect(socket, secret);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (!hello) {
|
|
1342
|
+
console.warn(" terminal keeper unavailable: agents' terminals run in the office and stop when it restarts");
|
|
1343
|
+
return [];
|
|
1344
|
+
}
|
|
1345
|
+
conn = hello.sock;
|
|
1346
|
+
leaving = false;
|
|
1347
|
+
hookPort = hello.ready.hookPort;
|
|
1348
|
+
hostPid = hello.ready.pid ?? 0;
|
|
1349
|
+
found = hello.ready.ptys;
|
|
1350
|
+
return found;
|
|
1351
|
+
}
|
|
1352
|
+
var keeperPid = () => conn ? hostPid : 0;
|
|
1353
|
+
function keeperHookUrl() {
|
|
1354
|
+
return conn ? `http://127.0.0.1:${hookPort}` : null;
|
|
1355
|
+
}
|
|
1356
|
+
function spawnPty(file, args, o, meta) {
|
|
1357
|
+
if (conn) {
|
|
1358
|
+
const p2 = new RemotePty(crypto2.randomUUID());
|
|
1359
|
+
send({ op: "spawn", id: p2.id, file, args, cwd: o.cwd, env: o.env, cols: o.cols, rows: o.rows, meta });
|
|
1360
|
+
return p2;
|
|
1361
|
+
}
|
|
1362
|
+
if (!nodePty) throw new Error("the terminal module could not be loaded on this machine");
|
|
1363
|
+
const p = nodePty.spawn(file, args, { name: "xterm-256color", cols: o.cols, rows: o.rows, cwd: o.cwd, env: o.env, useConptyDll: true });
|
|
1364
|
+
return {
|
|
1365
|
+
get pid() {
|
|
1366
|
+
return p.pid;
|
|
1367
|
+
},
|
|
1368
|
+
write: (data) => p.write(data),
|
|
1369
|
+
resize: (cols, rows) => p.resize(cols, rows),
|
|
1370
|
+
kill: () => killTree(p),
|
|
1371
|
+
setMeta: () => void 0,
|
|
1372
|
+
onData: (cb) => void p.onData(cb),
|
|
1373
|
+
onExit: (cb) => void p.onExit(({ exitCode }) => cb(exitCode))
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function adoptPty(held) {
|
|
1377
|
+
const p = new RemotePty(held.id);
|
|
1378
|
+
p.pid = held.pid;
|
|
1379
|
+
queueMicrotask(() => send({ op: "attach", id: held.id }));
|
|
1380
|
+
return p;
|
|
1381
|
+
}
|
|
1382
|
+
function discardPty(held) {
|
|
1383
|
+
send({ op: "kill", id: held.id });
|
|
1384
|
+
}
|
|
1385
|
+
function hooksReady() {
|
|
1386
|
+
send({ op: "hooksReady" });
|
|
1387
|
+
}
|
|
1388
|
+
function leaveKeeper() {
|
|
1389
|
+
leaving = true;
|
|
1390
|
+
conn?.end();
|
|
1391
|
+
}
|
|
1392
|
+
function killTree(proc) {
|
|
1393
|
+
if (process.platform === "win32") {
|
|
1394
|
+
const tk = spawn("taskkill", ["/PID", String(proc.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
1395
|
+
tk.once("error", () => void 0);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
1400
|
+
} catch {
|
|
1401
|
+
proc.kill();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// server/cliRunner.ts
|
|
1406
|
+
var SESSIONS_DIR = path7.join(HOME_DIR, "sessions");
|
|
1407
|
+
var BIN_DIR = path7.join(HOME_DIR, "bin");
|
|
1408
|
+
var FINISH_GRACE_MS = 3e3;
|
|
1409
|
+
var BOOT_MS = 6e4;
|
|
1410
|
+
var USAGE_WARN_PCT = 90;
|
|
1411
|
+
var INLINE_PROMPT_MAX = 8e3;
|
|
1412
|
+
var officeUrl = "";
|
|
1413
|
+
function setOfficeUrl(url) {
|
|
1414
|
+
officeUrl = url;
|
|
1415
|
+
}
|
|
1416
|
+
var routes = /* @__PURE__ */ new Map();
|
|
1417
|
+
function handleHook(token, body) {
|
|
1418
|
+
const route2 = routes.get(token);
|
|
1419
|
+
if (!route2 || !body || typeof body !== "object" || Array.isArray(body)) return {};
|
|
1420
|
+
try {
|
|
1421
|
+
return route2.hook(body);
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
console.warn("hook failed", err);
|
|
1424
|
+
return {};
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
async function handleMcp(token, req, res) {
|
|
1428
|
+
const office = routes.get(token)?.office;
|
|
1429
|
+
if (!office) {
|
|
1430
|
+
res.status(404).json({ jsonrpc: "2.0", error: { code: -32001, message: "This session has ended." }, id: null });
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const server2 = office.serve();
|
|
1434
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
|
|
1435
|
+
res.on("close", () => {
|
|
1436
|
+
void transport.close();
|
|
1437
|
+
void server2.close();
|
|
1438
|
+
});
|
|
1439
|
+
await server2.connect(transport);
|
|
1440
|
+
await transport.handleRequest(req, res, req.body);
|
|
1441
|
+
}
|
|
1442
|
+
var helpersWritten = false;
|
|
1443
|
+
function writeHelpers() {
|
|
1444
|
+
if (helpersWritten) return;
|
|
1445
|
+
fs6.mkdirSync(BIN_DIR, { recursive: true });
|
|
1446
|
+
fs6.writeFileSync(path7.join(BIN_DIR, "statusline.cjs"), STATUSLINE_SOURCE);
|
|
1447
|
+
fs6.writeFileSync(path7.join(BIN_DIR, "notify.cjs"), NOTIFY_SOURCE);
|
|
1448
|
+
fs6.writeFileSync(path7.join(BIN_DIR, "codex-hook.cjs"), CODEX_HOOK_SOURCE);
|
|
1449
|
+
fs6.writeFileSync(path7.join(BIN_DIR, "opencode-plugin.mjs"), OPENCODE_PLUGIN_SOURCE);
|
|
1450
|
+
helpersWritten = true;
|
|
1451
|
+
}
|
|
1452
|
+
var shellArg = (p) => `"${p.replaceAll("\\", "/")}"`;
|
|
1453
|
+
var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop", "StopFailure", "Notification"];
|
|
1454
|
+
function playwrightServer(outputDir) {
|
|
1455
|
+
const args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--output-dir", outputDir, "--init-script", path7.join(import.meta.dirname, "browser-init.js")];
|
|
1456
|
+
return process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
|
|
1457
|
+
}
|
|
1458
|
+
function toolOutput(response) {
|
|
1459
|
+
const images = [];
|
|
1460
|
+
const blocks = Array.isArray(response) ? response : Array.isArray(response?.content) ? response.content : null;
|
|
1461
|
+
if (blocks) {
|
|
1462
|
+
for (const b of blocks) {
|
|
1463
|
+
const source = b?.source;
|
|
1464
|
+
if (b?.type === "image" && source?.type === "base64" && source.data) images.push({ data: source.data, mime: source.media_type ?? "image/png" });
|
|
1465
|
+
else if (b?.type === "image" && typeof b.data === "string") images.push({ data: b.data, mime: String(b.mimeType ?? "image/png") });
|
|
1466
|
+
}
|
|
1467
|
+
return { text: resultText(blocks), images };
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof response === "string") return { text: response, images };
|
|
1470
|
+
const r = response;
|
|
1471
|
+
if (r && typeof r === "object") {
|
|
1472
|
+
if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((s) => typeof s === "string" && s).join("\n"), images };
|
|
1473
|
+
if (typeof r.file?.content === "string") return { text: r.file.content, images };
|
|
1474
|
+
if (Array.isArray(r.filenames)) return { text: r.filenames.join("\n"), images };
|
|
1475
|
+
}
|
|
1476
|
+
return { text: "", images };
|
|
1477
|
+
}
|
|
1478
|
+
function quietly(fn) {
|
|
1479
|
+
try {
|
|
1480
|
+
fn();
|
|
1481
|
+
} catch {
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
var lives = /* @__PURE__ */ new Map();
|
|
1485
|
+
var hooksHinted = false;
|
|
1486
|
+
var IDLE_MS = 30 * 6e4;
|
|
1487
|
+
var ALLOW = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", permissionDecisionReason: "Approved by cubefarm" } };
|
|
1488
|
+
var toolInput = (b) => b.tool_input && typeof b.tool_input === "object" ? b.tool_input : {};
|
|
1489
|
+
function endLive(live) {
|
|
1490
|
+
if (lives.get(live.term) !== live) return;
|
|
1491
|
+
lives.delete(live.term);
|
|
1492
|
+
clearTimeout(live.idleTimer);
|
|
1493
|
+
routes.delete(live.token);
|
|
1494
|
+
live.term.releaseIdle = null;
|
|
1495
|
+
live.term.bind(null);
|
|
1496
|
+
live.proc?.kill();
|
|
1497
|
+
live.proc = null;
|
|
1498
|
+
live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
|
|
1499
|
+
fs6.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
|
|
1500
|
+
}
|
|
1501
|
+
var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
|
|
1502
|
+
var remember = (l) => l.proc?.setMeta(metaOf(l));
|
|
1503
|
+
function wire(l, office) {
|
|
1504
|
+
const p = l.proc;
|
|
1505
|
+
lives.set(l.term, l);
|
|
1506
|
+
routes.set(l.token, { hook: (b) => l.session ? l.session.hook(b) : idleHook(l, b), office });
|
|
1507
|
+
l.term.bind({ write: (d) => quietly(() => p.write(d)), resize: (c, r) => quietly(() => p.resize(c, r)) });
|
|
1508
|
+
p.onData((data) => {
|
|
1509
|
+
if (lives.get(l.term) === l) l.term.write(data);
|
|
1510
|
+
});
|
|
1511
|
+
p.onExit((code) => {
|
|
1512
|
+
l.proc = null;
|
|
1513
|
+
if (lives.get(l.term) !== l) return;
|
|
1514
|
+
if (l.session) l.session.exited(code);
|
|
1515
|
+
else endLive(l);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
function waitAtPrompt(l) {
|
|
1519
|
+
clearTimeout(l.idleTimer);
|
|
1520
|
+
l.idleTimer = setTimeout(() => endLive(l), IDLE_MS);
|
|
1521
|
+
l.term.releaseIdle = () => endLive(l);
|
|
1522
|
+
remember(l);
|
|
1523
|
+
}
|
|
1524
|
+
async function reconnectClis(terminalFor) {
|
|
1525
|
+
const held = await startKeeper(HOME_DIR, handleHook);
|
|
1526
|
+
const back = [];
|
|
1527
|
+
for (const h of held) {
|
|
1528
|
+
const meta = h.meta ?? {};
|
|
1529
|
+
const term = meta.agentId ? terminalFor(meta.agentId) : null;
|
|
1530
|
+
if (h.exit !== null || !term || lives.has(term) || !isCli(meta.cli) || !meta.token || !meta.dir || !meta.agentId) {
|
|
1531
|
+
discardPty(h);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const l = { agentId: meta.agentId, cli: meta.cli, term, proc: adoptPty(h), token: meta.token, dir: meta.dir, resumeId: meta.resumeId ?? null, statusLine: "\u{1F3E2} cubefarm", shots: /* @__PURE__ */ new Set(), session: null };
|
|
1535
|
+
wire(l);
|
|
1536
|
+
waitAtPrompt(l);
|
|
1537
|
+
setTimeout(() => quietly(() => (l.proc?.resize(term.cols - 1, term.rows), l.proc?.resize(term.cols, term.rows))), 500);
|
|
1538
|
+
back.push({ agentId: l.agentId, cli: l.cli, resumeId: l.resumeId, busy: !!meta.busy });
|
|
1539
|
+
}
|
|
1540
|
+
return back;
|
|
1541
|
+
}
|
|
1542
|
+
function officeProcesses() {
|
|
1543
|
+
const pids = [keeperPid(), ...[...lives.values()].map((l) => l.proc?.pid ?? 0)].filter((p) => p > 0);
|
|
1544
|
+
return { pids, markers: [SESSIONS_DIR, BIN_DIR] };
|
|
1545
|
+
}
|
|
1546
|
+
function releaseClis(restart) {
|
|
1547
|
+
if (!restart) for (const l of [...lives.values()]) endLive(l);
|
|
1548
|
+
leaveKeeper();
|
|
1549
|
+
}
|
|
1550
|
+
function idleHook(live, b) {
|
|
1551
|
+
switch (String(b.hook_event_name ?? "")) {
|
|
1552
|
+
case "PreToolUse":
|
|
1553
|
+
return ALLOW;
|
|
1554
|
+
case "UserPromptSubmit": {
|
|
1555
|
+
const text = String(b.prompt ?? "").trim();
|
|
1556
|
+
if (!text || live.term.onIdlePrompt?.(text)) return {};
|
|
1557
|
+
return { decision: "block", reason: "The office couldn't take that on as a follow-up right now (see the agent's panel). Nothing was sent." };
|
|
1558
|
+
}
|
|
1559
|
+
case "StatusLine":
|
|
1560
|
+
return { statusLine: live.statusLine };
|
|
1561
|
+
}
|
|
1562
|
+
return {};
|
|
1563
|
+
}
|
|
1564
|
+
function startCliSession(opts, callbacks) {
|
|
1565
|
+
const term = opts.terminal;
|
|
1566
|
+
const prev = lives.get(term);
|
|
1567
|
+
const waiting = !!prev?.proc && !prev.session;
|
|
1568
|
+
const adopt = waiting && (!!opts.reattach || prev.cli === (opts.cli ?? "claude") && !!opts.resumeSessionId && prev.resumeId === opts.resumeSessionId);
|
|
1569
|
+
if (prev && !adopt) endLive(prev);
|
|
1570
|
+
let live = adopt ? prev : null;
|
|
1571
|
+
const cli = live?.cli ?? opts.cli ?? "claude";
|
|
1572
|
+
const label = cliLabel(cli);
|
|
1573
|
+
const token = crypto3.randomUUID();
|
|
1574
|
+
const sessionId = opts.resumeSessionId ?? crypto3.randomUUID();
|
|
1575
|
+
const dir = path7.join(SESSIONS_DIR, token);
|
|
1576
|
+
const started = Date.now();
|
|
1577
|
+
const statusLine = `\u{1F3E2} cubefarm${opts.label ? ` \xB7 ${opts.label}` : ""}`;
|
|
1578
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
1579
|
+
const officePrompts = [];
|
|
1580
|
+
const launched = [];
|
|
1581
|
+
let mainThread = opts.resumeSessionId ?? null;
|
|
1582
|
+
const usageResets = /* @__PURE__ */ new Map();
|
|
1583
|
+
const warned = /* @__PURE__ */ new Set();
|
|
1584
|
+
let done = false;
|
|
1585
|
+
let costUsd = 0;
|
|
1586
|
+
let turns = 0;
|
|
1587
|
+
let lastText = "";
|
|
1588
|
+
let turnError = null;
|
|
1589
|
+
let begun = false;
|
|
1590
|
+
let finishTimer;
|
|
1591
|
+
let bootTimer;
|
|
1592
|
+
let screenTimer;
|
|
1593
|
+
let shotTimer;
|
|
1594
|
+
const nudged = /* @__PURE__ */ new Set();
|
|
1595
|
+
let trustKeys = 0;
|
|
1596
|
+
let trustedAt = 0;
|
|
1597
|
+
const cb = { ...callbacks };
|
|
1598
|
+
const log = (entries) => entries.length && cb.log(entries);
|
|
1599
|
+
const collectShots = (all = false) => {
|
|
1600
|
+
const l2 = live;
|
|
1601
|
+
if (!l2 || cli === "claude" || !opts.browserTesting) return;
|
|
1602
|
+
for (const shot of newScreenshots(path7.join(l2.dir, "browser"), l2.shots, all ? Infinity : Date.now())) {
|
|
1603
|
+
l2.shots.add(shot.name);
|
|
1604
|
+
try {
|
|
1605
|
+
cb.screenshot(fs6.readFileSync(shot.file), shot.mime);
|
|
1606
|
+
} catch {
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
const finish = (r, keep = false) => {
|
|
1611
|
+
if (done) return;
|
|
1612
|
+
done = true;
|
|
1613
|
+
for (const t of [finishTimer, bootTimer]) clearTimeout(t);
|
|
1614
|
+
clearInterval(screenTimer);
|
|
1615
|
+
clearInterval(shotTimer);
|
|
1616
|
+
collectShots(true);
|
|
1617
|
+
if (term.onInput === watchKeys) term.onInput = null;
|
|
1618
|
+
const l2 = live;
|
|
1619
|
+
if (l2 && lives.get(term) === l2) {
|
|
1620
|
+
l2.session = null;
|
|
1621
|
+
if (keep && opts.keepAlive && l2.proc) waitAtPrompt(l2);
|
|
1622
|
+
else endLive(l2);
|
|
1623
|
+
}
|
|
1624
|
+
cb.tool(null);
|
|
1625
|
+
cb.finished({ ...r, costUsd, turns });
|
|
1626
|
+
};
|
|
1627
|
+
const turnEnded = (text) => {
|
|
1628
|
+
lastText = text;
|
|
1629
|
+
cb.tool(null);
|
|
1630
|
+
clearTimeout(finishTimer);
|
|
1631
|
+
finishTimer = setTimeout(() => finish({ ok: !turnError, text: lastText, errors: turnError ? [turnError] : [] }, true), FINISH_GRACE_MS);
|
|
1632
|
+
};
|
|
1633
|
+
const busy = () => {
|
|
1634
|
+
begun = true;
|
|
1635
|
+
clearTimeout(finishTimer);
|
|
1636
|
+
};
|
|
1637
|
+
const interrupted = () => finish({ ok: false, text: lastText, errors: ["Interrupted in the terminal"], interrupted: true }, true);
|
|
1638
|
+
const watchKeys = (data) => {
|
|
1639
|
+
if (data !== "\x1B" || done || !begun) return;
|
|
1640
|
+
const before = interruptions(term.screen());
|
|
1641
|
+
let looks = 0;
|
|
1642
|
+
const look = () => {
|
|
1643
|
+
if (done) return;
|
|
1644
|
+
if (interruptions(term.screen()) > before) interrupted();
|
|
1645
|
+
else if (++looks < 4) setTimeout(look, 1e3);
|
|
1646
|
+
};
|
|
1647
|
+
setTimeout(look, 800);
|
|
1648
|
+
};
|
|
1649
|
+
term.onInput = watchKeys;
|
|
1650
|
+
const assistantLines = (text) => {
|
|
1651
|
+
const lines = text.split("\n");
|
|
1652
|
+
const out = lines.slice(0, 30).map((l2, i) => ({ kind: "text", text: `${i === 0 ? "\u25CF " : " "}${l2}` }));
|
|
1653
|
+
if (lines.length > 30) out.push({ kind: "text", text: ` \u2026 (${lines.length - 30} more lines)` });
|
|
1654
|
+
return out;
|
|
1655
|
+
};
|
|
1656
|
+
const onStatus = (b) => {
|
|
1657
|
+
const cost = Number(b.cost?.total_cost_usd);
|
|
1658
|
+
if (Number.isFinite(cost)) costUsd = cost;
|
|
1659
|
+
const limits = b.rate_limits ?? {};
|
|
1660
|
+
for (const [type, l2] of Object.entries(limits)) {
|
|
1661
|
+
const used = Number(l2?.used_percentage);
|
|
1662
|
+
if (!Number.isFinite(used)) continue;
|
|
1663
|
+
const resetsAt = Number(l2?.resets_at) > 0 ? Number(l2?.resets_at) * 1e3 : null;
|
|
1664
|
+
usageResets.set(type, used >= 100 ? resetsAt : null);
|
|
1665
|
+
if (used >= USAGE_WARN_PCT && used < 100 && !warned.has(type)) {
|
|
1666
|
+
warned.add(type);
|
|
1667
|
+
cb.usageWarning?.({ resetsAt, rateLimitType: type, utilization: used / 100 });
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return { statusLine };
|
|
1671
|
+
};
|
|
1672
|
+
const hook = (b) => {
|
|
1673
|
+
const event = String(b.hook_event_name ?? "");
|
|
1674
|
+
const sub = typeof b.agent_id === "string" && b.agent_id !== "";
|
|
1675
|
+
switch (event) {
|
|
1676
|
+
case "PreToolUse": {
|
|
1677
|
+
const name = String(b.tool_name ?? "");
|
|
1678
|
+
const input = toolInput(b);
|
|
1679
|
+
if (!sub) {
|
|
1680
|
+
busy();
|
|
1681
|
+
turns += 1;
|
|
1682
|
+
toolNames.set(String(b.tool_use_id ?? ""), name);
|
|
1683
|
+
log([{ kind: "tool", tool: name, text: `\u23FA ${describeTool(opts.cwd, name, input)}` }, ...name === "TodoWrite" ? todoLines(input) : []]);
|
|
1684
|
+
cb.tool(name);
|
|
1685
|
+
if (name === "mcp__playwright__browser_navigate" && typeof input.url === "string") cb.browserUrl(input.url);
|
|
1686
|
+
}
|
|
1687
|
+
return ALLOW;
|
|
1688
|
+
}
|
|
1689
|
+
case "PostToolUse":
|
|
1690
|
+
case "PostToolUseFailure": {
|
|
1691
|
+
if (sub) return {};
|
|
1692
|
+
const name = toolNames.get(String(b.tool_use_id ?? "")) ?? String(b.tool_name ?? "");
|
|
1693
|
+
const out = toolOutput(b.tool_response);
|
|
1694
|
+
if (cli === "claude") for (const img of out.images) cb.screenshot(Buffer.from(img.data, "base64"), img.mime);
|
|
1695
|
+
const saved = name === "mcp__playwright__browser_take_screenshot" && !out.images.length ? screenshotFile(opts.cwd, out.text) : null;
|
|
1696
|
+
if (saved) cb.screenshot(saved.data, saved.mime);
|
|
1697
|
+
if (name.startsWith("mcp__playwright__")) {
|
|
1698
|
+
const url = out.text.match(/Page URL:\s*(\S+)/);
|
|
1699
|
+
if (url) cb.browserUrl(url[1]);
|
|
1700
|
+
}
|
|
1701
|
+
if (event === "PostToolUseFailure") log([{ kind: "error", text: ` \u23BF ${clip(tidyPaths(opts.cwd, String(b.error ?? out.text ?? "Error")).split("\n")[0] || "Error", 200)}` }]);
|
|
1702
|
+
else if (name !== "TodoWrite") log(summariseResult(opts.cwd, name, out.text));
|
|
1703
|
+
cb.tool(null);
|
|
1704
|
+
return {};
|
|
1705
|
+
}
|
|
1706
|
+
case "UserPromptSubmit": {
|
|
1707
|
+
if (sub) return {};
|
|
1708
|
+
busy();
|
|
1709
|
+
const prompt2 = String(b.prompt ?? "").trim();
|
|
1710
|
+
const i = officePrompts.findIndex((p2) => p2.slice(0, 60) === prompt2.slice(0, 60));
|
|
1711
|
+
if (i >= 0) officePrompts.splice(i, 1);
|
|
1712
|
+
else if (prompt2) log([{ kind: "manager", text: `\u25B6 Typed in the terminal: ${clip(prompt2.split("\n")[0], 200)}` }]);
|
|
1713
|
+
return {};
|
|
1714
|
+
}
|
|
1715
|
+
case "Stop": {
|
|
1716
|
+
const text = String(b.last_assistant_message ?? "").trim();
|
|
1717
|
+
turns += 1;
|
|
1718
|
+
if (text) {
|
|
1719
|
+
log(assistantLines(text));
|
|
1720
|
+
cb.turn?.(text);
|
|
1721
|
+
}
|
|
1722
|
+
turnEnded(text);
|
|
1723
|
+
return {};
|
|
1724
|
+
}
|
|
1725
|
+
case "StopFailure": {
|
|
1726
|
+
const error = String(b.error ?? "unknown");
|
|
1727
|
+
const details = String(b.error_details ?? "").trim();
|
|
1728
|
+
if (error === "rate_limit") cb.limited?.([...usageResets.values()].find((t) => t !== null) ?? null);
|
|
1729
|
+
finish({ ok: false, text: lastText, errors: [`${label} stopped: ${error.replace(/_/g, " ")}${details ? ` (${clip(details, 160)})` : ""}`] }, true);
|
|
1730
|
+
return {};
|
|
1731
|
+
}
|
|
1732
|
+
case "Notification": {
|
|
1733
|
+
if (b.notification_type === "permission_prompt") log([{ kind: "error", text: `\u26A0 ${label} is waiting for a permission answer: open the terminal to answer it.` }]);
|
|
1734
|
+
return {};
|
|
1735
|
+
}
|
|
1736
|
+
case "StatusLine":
|
|
1737
|
+
return onStatus(b);
|
|
1738
|
+
case "Interrupt":
|
|
1739
|
+
if (!sub) interrupted();
|
|
1740
|
+
return {};
|
|
1741
|
+
// Codex's notify program and the OpenCode plugin
|
|
1742
|
+
case "TurnComplete": {
|
|
1743
|
+
const thread = typeof b.session_id === "string" ? b.session_id : "";
|
|
1744
|
+
if (cli === "codex" && thread !== mainThread) {
|
|
1745
|
+
const input = String(b.input ?? "").trim();
|
|
1746
|
+
if (mainThread || !launched.some((p2) => input.startsWith(p2.slice(0, 40)))) return {};
|
|
1747
|
+
mainThread = thread;
|
|
1748
|
+
}
|
|
1749
|
+
if (thread) {
|
|
1750
|
+
cb.sessionId(thread);
|
|
1751
|
+
if (live && live.resumeId !== thread) {
|
|
1752
|
+
live.resumeId = thread;
|
|
1753
|
+
remember(live);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
const text = String(b.last_assistant_message ?? "").trim();
|
|
1757
|
+
turns += 1;
|
|
1758
|
+
if (text) {
|
|
1759
|
+
log(assistantLines(text));
|
|
1760
|
+
cb.turn?.(text);
|
|
1761
|
+
}
|
|
1762
|
+
turnEnded(text);
|
|
1763
|
+
return {};
|
|
1764
|
+
}
|
|
1765
|
+
case "TurnError":
|
|
1766
|
+
turnError = `${label}: ${clip(String(b.error ?? "error"), 200)}`;
|
|
1767
|
+
log([{ kind: "error", text: `\u2717 ${turnError}` }]);
|
|
1768
|
+
return {};
|
|
1769
|
+
}
|
|
1770
|
+
return {};
|
|
1771
|
+
};
|
|
1772
|
+
const exited = (exitCode) => {
|
|
1773
|
+
const ok = exitCode === 0 && (lastText !== "" || cli !== "claude" && Date.now() - started > 2e4);
|
|
1774
|
+
finish({ ok, text: lastText, errors: ok ? [] : [`${label} exited${exitCode ? ` with code ${exitCode}` : ""} before finishing.`] });
|
|
1775
|
+
};
|
|
1776
|
+
const handle = {
|
|
1777
|
+
send(text) {
|
|
1778
|
+
const proc = live?.proc;
|
|
1779
|
+
if (done || !proc) return;
|
|
1780
|
+
busy();
|
|
1781
|
+
turnError = null;
|
|
1782
|
+
officePrompts.push(text.trim());
|
|
1783
|
+
const clean = text.replace(/\x1b\[20[01]~/g, "").replace(/\r\n?/g, "\n");
|
|
1784
|
+
launched.push(clean.trim());
|
|
1785
|
+
quietly(() => proc.write(`\x1B[200~${clean}\x1B[201~`));
|
|
1786
|
+
setTimeout(() => quietly(() => live?.proc?.write("\r")), 150);
|
|
1787
|
+
},
|
|
1788
|
+
stop() {
|
|
1789
|
+
finish({ ok: false, text: "", errors: ["Stopped by manager"] });
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
if (adopt && live) {
|
|
1793
|
+
clearTimeout(live.idleTimer);
|
|
1794
|
+
term.releaseIdle = null;
|
|
1795
|
+
live.session = { hook, exited };
|
|
1796
|
+
Object.assign(live, { statusLine });
|
|
1797
|
+
remember(live);
|
|
1798
|
+
if (live.resumeId) cb.sessionId(live.resumeId);
|
|
1799
|
+
if (opts.typed || opts.reattach) busy();
|
|
1800
|
+
else handle.send(opts.prompt);
|
|
1801
|
+
shotTimer = setInterval(collectShots, 2e3);
|
|
1802
|
+
return handle;
|
|
1803
|
+
}
|
|
1804
|
+
if (opts.reattach) {
|
|
1805
|
+
setTimeout(() => finish({ ok: false, text: "", errors: [`${label} stopped while the office restarted.`] }), 0);
|
|
1806
|
+
return handle;
|
|
1807
|
+
}
|
|
1808
|
+
const cmd = commandFor(cli);
|
|
1809
|
+
if (!terminalsAvailable || !cmd) {
|
|
1810
|
+
const why = !terminalsAvailable ? "the terminal module could not be loaded on this machine" : `${label} isn't installed (no "${cli}" on PATH)`;
|
|
1811
|
+
setTimeout(() => finish({ ok: false, text: "", errors: [`Could not start ${label}: ${why}.`] }), 0);
|
|
1812
|
+
return handle;
|
|
1813
|
+
}
|
|
1814
|
+
const keeper = keeperHookUrl();
|
|
1815
|
+
const hookUrl = `${keeper ?? officeUrl}/api/hooks/${token}`;
|
|
1816
|
+
const browser = opts.browserTesting ? playwrightServer(path7.join(dir, "browser")) : null;
|
|
1817
|
+
let prompt = opts.prompt;
|
|
1818
|
+
if (opts.outputSchema) {
|
|
1819
|
+
prompt += `
|
|
1820
|
+
|
|
1821
|
+
When you are done, end your final message with your report as one JSON object in a \`\`\`json block, matching this JSON schema:
|
|
1822
|
+
${JSON.stringify(opts.outputSchema)}`;
|
|
1823
|
+
}
|
|
1824
|
+
let files;
|
|
1825
|
+
try {
|
|
1826
|
+
writeHelpers();
|
|
1827
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
1828
|
+
const settings = {
|
|
1829
|
+
hooks: Object.fromEntries(HOOK_EVENTS.map((e) => [e, [{ hooks: [{ type: "http", url: hookUrl, timeout: keeper ? 120 : 30 }] }]])),
|
|
1830
|
+
statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(path7.join(BIN_DIR, "statusline.cjs"))} ${shellArg(hookUrl)}`, padding: 0 }
|
|
1831
|
+
};
|
|
1832
|
+
const servers = {};
|
|
1833
|
+
if (browser) servers.playwright = browser;
|
|
1834
|
+
if (opts.office) servers.office = { type: "http", url: `${officeUrl}/api/mcp/${token}` };
|
|
1835
|
+
files = { settings: path7.join(dir, "settings.json"), mcp: Object.keys(servers).length ? path7.join(dir, "mcp.json") : null, system: path7.join(dir, "instructions.md") };
|
|
1836
|
+
fs6.writeFileSync(files.settings, JSON.stringify(settings, null, 2));
|
|
1837
|
+
if (files.mcp) fs6.writeFileSync(files.mcp, JSON.stringify({ mcpServers: servers }, null, 2));
|
|
1838
|
+
fs6.writeFileSync(files.system, opts.systemAppend);
|
|
1839
|
+
if (prompt.length > INLINE_PROMPT_MAX) {
|
|
1840
|
+
const file = path7.join(dir, "task.md");
|
|
1841
|
+
fs6.writeFileSync(file, prompt);
|
|
1842
|
+
prompt = `Your task is in ${file}. Read all of it first, then do what it says.`;
|
|
1843
|
+
}
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
setTimeout(() => finish({ ok: false, text: "", errors: [`Could not prepare ${label}: ${err.message}`] }), 0);
|
|
1846
|
+
return handle;
|
|
1847
|
+
}
|
|
1848
|
+
officePrompts.push(prompt.trim());
|
|
1849
|
+
launched.push(prompt.trim());
|
|
1850
|
+
const launch2 = launchArgs(cli, {
|
|
1851
|
+
prompt,
|
|
1852
|
+
systemAppend: opts.systemAppend,
|
|
1853
|
+
model: opts.model,
|
|
1854
|
+
effort: opts.effort,
|
|
1855
|
+
resumeId: opts.resumeSessionId,
|
|
1856
|
+
sessionId,
|
|
1857
|
+
name: opts.label ?? "cubefarm",
|
|
1858
|
+
role: opts.role,
|
|
1859
|
+
additionalDirectories: opts.additionalDirectories,
|
|
1860
|
+
files,
|
|
1861
|
+
notify: { script: path7.join(BIN_DIR, "notify.cjs"), url: hookUrl },
|
|
1862
|
+
codexHook: path7.join(BIN_DIR, "codex-hook.cjs"),
|
|
1863
|
+
plugin: pathToFileURL(path7.join(BIN_DIR, "opencode-plugin.mjs")).href,
|
|
1864
|
+
browser
|
|
1865
|
+
});
|
|
1866
|
+
const env = {};
|
|
1867
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1868
|
+
if (v === void 0) continue;
|
|
1869
|
+
if (/^(ANTHROPIC_|CLAUDE)/i.test(k) && k !== "CLAUDE_CONFIG_DIR") continue;
|
|
1870
|
+
if (/^(NO_COLOR|FORCE_COLOR|TERM_PROGRAM|TERM_PROGRAM_VERSION|CODEX_THREAD_ID|OPENCODE_CONFIG_CONTENT)$/i.test(k)) continue;
|
|
1871
|
+
env[k] = v;
|
|
1872
|
+
}
|
|
1873
|
+
Object.assign(env, { TERM: "xterm-256color", COLORTERM: "truecolor" }, launch2.env);
|
|
1874
|
+
if (cli === "claude") cb.sessionId(sessionId);
|
|
1875
|
+
term.note(`\u2500\u2500 ${label}${opts.label ? ` \xB7 ${opts.label}` : ""} \u2500\u2500`);
|
|
1876
|
+
const resumeId = cli === "claude" ? sessionId : opts.resumeSessionId ?? null;
|
|
1877
|
+
const l = { agentId: opts.agentId ?? "", cli, term, proc: null, token, dir, resumeId, statusLine, shots: /* @__PURE__ */ new Set(), session: { hook, exited } };
|
|
1878
|
+
let p;
|
|
1879
|
+
try {
|
|
1880
|
+
p = spawnPty(cmd.file, [...cmd.args, ...launch2.args], { cols: term.cols, rows: term.rows, cwd: opts.cwd, env }, metaOf(l));
|
|
1881
|
+
} catch (err) {
|
|
1882
|
+
setTimeout(() => finish({ ok: false, text: "", errors: [`Could not start ${label}: ${err.message}`] }), 0);
|
|
1883
|
+
return handle;
|
|
1884
|
+
}
|
|
1885
|
+
l.proc = p;
|
|
1886
|
+
live = l;
|
|
1887
|
+
wire(l, opts.office);
|
|
1888
|
+
shotTimer = setInterval(collectShots, 2e3);
|
|
1889
|
+
screenTimer = setInterval(() => {
|
|
1890
|
+
if (done || Date.now() - started > BOOT_MS * 2) return clearInterval(screenTimer);
|
|
1891
|
+
const screen = term.screen();
|
|
1892
|
+
const review = hookReviewKey(screen);
|
|
1893
|
+
const key = trustKeys < 12 && Date.now() - trustedAt > 3e3 ? trustKey(screen) ?? review : null;
|
|
1894
|
+
if (key === "down") {
|
|
1895
|
+
trustKeys += 1;
|
|
1896
|
+
quietly(() => p.write(term.appCursor ? "\x1BOB" : "\x1B[B"));
|
|
1897
|
+
} else if (key === "enter") {
|
|
1898
|
+
trustKeys += 1;
|
|
1899
|
+
trustedAt = Date.now();
|
|
1900
|
+
if (!review) log([{ kind: "system", text: `\u2713 Trusted the worktree for ${label}.` }]);
|
|
1901
|
+
else if (!hooksHinted) {
|
|
1902
|
+
hooksHinted = true;
|
|
1903
|
+
log([{ kind: "system", text: `\u2139 To see ${label}'s steps here, trust the office's hooks once: in this terminal, type /hooks and press t. Until then the office shows its task.` }]);
|
|
1904
|
+
}
|
|
1905
|
+
quietly(() => p.write("\r"));
|
|
1906
|
+
}
|
|
1907
|
+
if (!nudged.has("login") && /Not logged in|run \/login|Select login method|Sign in with ChatGPT|Please login|Invalid API key/i.test(screen)) {
|
|
1908
|
+
nudged.add("login");
|
|
1909
|
+
log([{ kind: "error", text: `\u26A0 ${label} needs you to sign in: open the terminal and sign in there, or run "${cli}" in a terminal of your own.` }]);
|
|
1910
|
+
} else if (!nudged.has("setup") && !trustKey(screen) && Date.now() - trustedAt > 5e3 && /Choose the text style|Press Enter to continue|Bypass Permissions mode|Settings Error/i.test(screen)) {
|
|
1911
|
+
nudged.add("setup");
|
|
1912
|
+
log([{ kind: "error", text: `\u26A0 ${label} is waiting on a setup screen: open the terminal to answer it.` }]);
|
|
1913
|
+
}
|
|
1914
|
+
}, 1e3);
|
|
1915
|
+
bootTimer = setTimeout(() => {
|
|
1916
|
+
if (!done && !begun && cli === "claude") log([{ kind: "error", text: `\u26A0 ${label} hasn't started on the task yet. Open the terminal to see what it's waiting for.` }]);
|
|
1917
|
+
}, BOOT_MS);
|
|
1918
|
+
return handle;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// server/previewRunner.ts
|
|
1922
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1923
|
+
import crypto4 from "node:crypto";
|
|
1924
|
+
import fs7 from "node:fs/promises";
|
|
1925
|
+
import net2 from "node:net";
|
|
1926
|
+
import path8 from "node:path";
|
|
892
1927
|
var PREVIEW_SLUG = "preview";
|
|
893
1928
|
var PREVIEW_BRANCH = "swarm-preview";
|
|
894
1929
|
var PREVIEW_TMP = ".preview-tmp";
|
|
@@ -909,7 +1944,7 @@ function previewEnv(extra, port, tmp) {
|
|
|
909
1944
|
}
|
|
910
1945
|
async function portOpen(port) {
|
|
911
1946
|
const probe = (host) => new Promise((resolve) => {
|
|
912
|
-
const sock =
|
|
1947
|
+
const sock = net2.connect({ port, host });
|
|
913
1948
|
const done = (ok) => {
|
|
914
1949
|
sock.destroy();
|
|
915
1950
|
resolve(ok);
|
|
@@ -921,10 +1956,10 @@ async function portOpen(port) {
|
|
|
921
1956
|
const [v4, v6] = await Promise.all([probe("127.0.0.1"), probe("::1")]);
|
|
922
1957
|
return v4 || v6;
|
|
923
1958
|
}
|
|
924
|
-
var
|
|
1959
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
925
1960
|
async function readJson(file) {
|
|
926
1961
|
try {
|
|
927
|
-
return JSON.parse(await
|
|
1962
|
+
return JSON.parse(await fs7.readFile(file, "utf8"));
|
|
928
1963
|
} catch {
|
|
929
1964
|
return null;
|
|
930
1965
|
}
|
|
@@ -937,11 +1972,11 @@ function defaultCommand(pkg) {
|
|
|
937
1972
|
const vite = /^\s*vite(\s|$)/.test(script) && !/--port\b/.test(script);
|
|
938
1973
|
return `npm run ${name}${vite ? " -- --port {port} --strictPort" : ""}`;
|
|
939
1974
|
}
|
|
940
|
-
function
|
|
1975
|
+
function killTree2(child) {
|
|
941
1976
|
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
942
1977
|
if (process.platform === "win32") {
|
|
943
1978
|
return new Promise((resolve) => {
|
|
944
|
-
const tk =
|
|
1979
|
+
const tk = spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
945
1980
|
tk.once("error", () => resolve());
|
|
946
1981
|
tk.once("close", () => resolve());
|
|
947
1982
|
});
|
|
@@ -962,7 +1997,7 @@ var RealPreview = class {
|
|
|
962
1997
|
void this.run().catch((err) => {
|
|
963
1998
|
if (this.stopped || err instanceof Stopped) return;
|
|
964
1999
|
this.stopped = true;
|
|
965
|
-
if (this.child) void
|
|
2000
|
+
if (this.child) void killTree2(this.child);
|
|
966
2001
|
this.cb.failed(err.message);
|
|
967
2002
|
});
|
|
968
2003
|
}
|
|
@@ -973,7 +2008,7 @@ var RealPreview = class {
|
|
|
973
2008
|
partial = "";
|
|
974
2009
|
async stop() {
|
|
975
2010
|
this.stopped = true;
|
|
976
|
-
if (this.child) await
|
|
2011
|
+
if (this.child) await killTree2(this.child);
|
|
977
2012
|
}
|
|
978
2013
|
check() {
|
|
979
2014
|
if (this.stopped) throw new Stopped();
|
|
@@ -985,9 +2020,9 @@ var RealPreview = class {
|
|
|
985
2020
|
this.check();
|
|
986
2021
|
await ensureLocalExcludes(job.fullName).catch(() => void 0);
|
|
987
2022
|
cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
|
|
988
|
-
const tmp =
|
|
989
|
-
await
|
|
990
|
-
const pkg = await readJson(
|
|
2023
|
+
const tmp = path8.join(wt, PREVIEW_TMP);
|
|
2024
|
+
await fs7.mkdir(tmp, { recursive: true });
|
|
2025
|
+
const pkg = await readJson(path8.join(wt, "package.json"));
|
|
991
2026
|
const command = job.command ?? defaultCommand(pkg);
|
|
992
2027
|
if (!command) {
|
|
993
2028
|
this.stopped = true;
|
|
@@ -1014,42 +2049,42 @@ var RealPreview = class {
|
|
|
1014
2049
|
this.check();
|
|
1015
2050
|
if (exitCode !== void 0) throw new Error(`The app exited (code ${exitCode}) before it listened on port ${job.port}.`);
|
|
1016
2051
|
if (Date.now() > deadline) throw new Error(`The app did not listen on port ${job.port} within 3 minutes. Does the command use PORT or {port}?`);
|
|
1017
|
-
await
|
|
2052
|
+
await sleep2(700);
|
|
1018
2053
|
}
|
|
1019
2054
|
this.check();
|
|
1020
2055
|
cb.status("running");
|
|
1021
2056
|
const code = await exited;
|
|
1022
|
-
while (!this.stopped && await portOpen(job.port)) await
|
|
2057
|
+
while (!this.stopped && await portOpen(job.port)) await sleep2(3e3);
|
|
1023
2058
|
if (this.stopped) return;
|
|
1024
2059
|
this.stopped = true;
|
|
1025
2060
|
cb.failed(`The app stopped (exit code ${code}).`);
|
|
1026
2061
|
}
|
|
1027
2062
|
/** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
|
|
1028
2063
|
async install(wt, env) {
|
|
1029
|
-
const lockFile =
|
|
1030
|
-
const lock = await
|
|
1031
|
-
const hash =
|
|
1032
|
-
const marker =
|
|
1033
|
-
if (await
|
|
2064
|
+
const lockFile = path8.join(wt, "package-lock.json");
|
|
2065
|
+
const lock = await fs7.readFile(lockFile, "utf8").catch(() => null);
|
|
2066
|
+
const hash = crypto4.createHash("sha256").update(await fs7.readFile(path8.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
|
|
2067
|
+
const marker = path8.join(wt, "node_modules", INSTALL_MARKER);
|
|
2068
|
+
if (await fs7.readFile(marker, "utf8").catch(() => "") === hash) {
|
|
1034
2069
|
this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
1035
2070
|
return;
|
|
1036
2071
|
}
|
|
1037
2072
|
const cmd = lock !== null ? "npm ci" : "npm install";
|
|
1038
2073
|
this.cb.log([`$ ${cmd}`]);
|
|
1039
|
-
const code = await Promise.race([this.spawn(cmd, wt, env),
|
|
2074
|
+
const code = await Promise.race([this.spawn(cmd, wt, env), sleep2(INSTALL_TIMEOUT_MS).then(() => "timeout")]);
|
|
1040
2075
|
this.check();
|
|
1041
2076
|
if (code === "timeout") {
|
|
1042
|
-
if (this.child) await
|
|
2077
|
+
if (this.child) await killTree2(this.child);
|
|
1043
2078
|
throw new Error(`${cmd} took longer than ${INSTALL_TIMEOUT_MS / 6e4} minutes.`);
|
|
1044
2079
|
}
|
|
1045
2080
|
if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
|
|
1046
2081
|
this.child = null;
|
|
1047
|
-
await
|
|
2082
|
+
await fs7.writeFile(marker, hash).catch(() => void 0);
|
|
1048
2083
|
}
|
|
1049
2084
|
/** Run a command line through the platform shell; resolves with its exit code. */
|
|
1050
2085
|
spawn(cmd, cwd, env) {
|
|
1051
2086
|
this.check();
|
|
1052
|
-
const child =
|
|
2087
|
+
const child = spawn2(cmd, { cwd, env, shell: true, windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
1053
2088
|
this.child = child;
|
|
1054
2089
|
const onData = (buf) => {
|
|
1055
2090
|
const text = this.partial + buf.toString().replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
@@ -1075,12 +2110,12 @@ var RealPreview = class {
|
|
|
1075
2110
|
};
|
|
1076
2111
|
var realPreviews = {
|
|
1077
2112
|
start: (job, cb) => new RealPreview(job, cb),
|
|
1078
|
-
hasDefault: async (fullName) =>
|
|
2113
|
+
hasDefault: async (fullName) => fs7.access(path8.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
1079
2114
|
};
|
|
1080
2115
|
|
|
1081
2116
|
// server/officeUpdate.ts
|
|
1082
|
-
import
|
|
1083
|
-
import
|
|
2117
|
+
import fs8 from "node:fs/promises";
|
|
2118
|
+
import path9 from "node:path";
|
|
1084
2119
|
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
1085
2120
|
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
1086
2121
|
function underLauncher(proc = process) {
|
|
@@ -1117,10 +2152,10 @@ function parseLastUpdate(raw) {
|
|
|
1117
2152
|
}
|
|
1118
2153
|
}
|
|
1119
2154
|
async function takeLastUpdate(home) {
|
|
1120
|
-
const file =
|
|
1121
|
-
const raw = await
|
|
2155
|
+
const file = path9.join(home, "last-update.json");
|
|
2156
|
+
const raw = await fs8.readFile(file, "utf8").catch(() => null);
|
|
1122
2157
|
if (raw === null) return null;
|
|
1123
|
-
await
|
|
2158
|
+
await fs8.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
1124
2159
|
return parseLastUpdate(raw);
|
|
1125
2160
|
}
|
|
1126
2161
|
function lastUpdateMessage(u, commits) {
|
|
@@ -1128,8 +2163,8 @@ function lastUpdateMessage(u, commits) {
|
|
|
1128
2163
|
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
1129
2164
|
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
1130
2165
|
}
|
|
1131
|
-
var OFFICE_DIR =
|
|
1132
|
-
var isSamePath = (a, b) => process.platform === "win32" ?
|
|
2166
|
+
var OFFICE_DIR = path9.resolve(import.meta.dirname, "..");
|
|
2167
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path9.resolve(a).toLowerCase() === path9.resolve(b).toLowerCase() : path9.resolve(a) === path9.resolve(b);
|
|
1133
2168
|
var realOffice = {
|
|
1134
2169
|
launcher: underLauncher(),
|
|
1135
2170
|
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
@@ -1173,16 +2208,21 @@ var realBackend = {
|
|
|
1173
2208
|
deskDir,
|
|
1174
2209
|
prepareDesk,
|
|
1175
2210
|
removeDesk,
|
|
1176
|
-
releaseDesk,
|
|
1177
|
-
startSession,
|
|
2211
|
+
releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
|
|
2212
|
+
startSession: (opts, cb) => opts.terminal ? startCliSession(opts, cb) : startSession(opts, cb),
|
|
2213
|
+
terminals: terminalsAvailable,
|
|
2214
|
+
reconnectClis: (terminalFor) => terminalsAvailable ? reconnectClis(terminalFor) : Promise.resolve([]),
|
|
2215
|
+
hooksReady,
|
|
2216
|
+
releaseClis,
|
|
2217
|
+
detectClis,
|
|
1178
2218
|
previews: realPreviews,
|
|
1179
2219
|
office: realOffice
|
|
1180
2220
|
};
|
|
1181
2221
|
|
|
1182
2222
|
// server/demo.ts
|
|
1183
|
-
import
|
|
2223
|
+
import crypto5 from "node:crypto";
|
|
1184
2224
|
import http from "node:http";
|
|
1185
|
-
import
|
|
2225
|
+
import path10 from "node:path";
|
|
1186
2226
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1187
2227
|
function issue(n, title, body, fullName, labels = []) {
|
|
1188
2228
|
return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
@@ -1191,7 +2231,7 @@ var repos = /* @__PURE__ */ new Map();
|
|
|
1191
2231
|
var bareRepos = /* @__PURE__ */ new Set();
|
|
1192
2232
|
var mergedSinceSync = /* @__PURE__ */ new Map();
|
|
1193
2233
|
var closedIssues = /* @__PURE__ */ new Set();
|
|
1194
|
-
var fakeSha = () =>
|
|
2234
|
+
var fakeSha = () => crypto5.randomBytes(20).toString("hex");
|
|
1195
2235
|
function runChecks(pr, fail = Math.random() < 0.2) {
|
|
1196
2236
|
Object.assign(pr, { checks: "pending", pendingChecks: ["CI / build", "Vercel"], failedChecks: [] });
|
|
1197
2237
|
setTimeout(() => {
|
|
@@ -1235,7 +2275,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
1235
2275
|
</svg>`;
|
|
1236
2276
|
}
|
|
1237
2277
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
1238
|
-
const branch =
|
|
2278
|
+
const branch = path10.basename(opts.cwd);
|
|
1239
2279
|
const hue = issueNumber * 67 % 360;
|
|
1240
2280
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
1241
2281
|
const port = 5200 + issueNumber % 50;
|
|
@@ -1314,7 +2354,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
1314
2354
|
const title = (kind === "issue" ? issueMatch?.[2] : prMatch?.[2])?.trim() ?? "follow-up";
|
|
1315
2355
|
const round = Number(opts.prompt.match(/QA round (\d+)/)?.[1] ?? 1);
|
|
1316
2356
|
const header = [
|
|
1317
|
-
{ kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort
|
|
2357
|
+
{ kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort` },
|
|
1318
2358
|
{ kind: "system", text: ` cwd ${opts.cwd}` }
|
|
1319
2359
|
];
|
|
1320
2360
|
const body = kind === "qa" ? qaScript(cb, number, title, round) : kind === "fix" ? fixScript(number) : devScript(opts, cb, number, title);
|
|
@@ -1373,7 +2413,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
1373
2413
|
number: n,
|
|
1374
2414
|
title,
|
|
1375
2415
|
url,
|
|
1376
|
-
headRefName: `swarm/issue-${number}-${
|
|
2416
|
+
headRefName: `swarm/issue-${number}-${path10.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
1377
2417
|
state: "OPEN",
|
|
1378
2418
|
isDraft: false,
|
|
1379
2419
|
mergeable: "MERGEABLE",
|
|
@@ -1425,6 +2465,71 @@ function fakeSession(opts, cb, fullName) {
|
|
|
1425
2465
|
}
|
|
1426
2466
|
};
|
|
1427
2467
|
}
|
|
2468
|
+
var ANSI = {
|
|
2469
|
+
text: (t) => t.replace(/^● /, "\x1B[97m\u25CF\x1B[0m "),
|
|
2470
|
+
tool: (t) => `\x1B[32m\u25CF\x1B[0m \x1B[1m${t.replace(/^⏺ /, "")}\x1B[0m`,
|
|
2471
|
+
result: (t) => `\x1B[2m${t}\x1B[0m`,
|
|
2472
|
+
thinking: (t) => `\x1B[35m${t}\x1B[0m`,
|
|
2473
|
+
error: (t) => `\x1B[31m${t}\x1B[0m`,
|
|
2474
|
+
system: (t) => `\x1B[2m${t}\x1B[0m`,
|
|
2475
|
+
manager: (t) => `\x1B[36m${t.replace(/^▶ /, "\u276F ")}\x1B[0m`,
|
|
2476
|
+
done: (t) => `\x1B[32m${t}\x1B[0m`
|
|
2477
|
+
};
|
|
2478
|
+
function inTerminal(opts, cb, start) {
|
|
2479
|
+
const term = opts.terminal;
|
|
2480
|
+
if (!term) return start(cb);
|
|
2481
|
+
const name = CLIS.find((c) => c.id === (opts.cli ?? "claude"))?.label ?? "Claude Code";
|
|
2482
|
+
term.note(`\u2500\u2500 ${name}${opts.label ? ` \xB7 ${opts.label}` : ""} \u2500\u2500`);
|
|
2483
|
+
term.write(
|
|
2484
|
+
[
|
|
2485
|
+
"",
|
|
2486
|
+
` \x1B[38;5;209m\u2590\u259B\u2588\u2588\u2588\u259C\u258C\x1B[0m \x1B[1m${name}\x1B[0m (demo)`,
|
|
2487
|
+
`\x1B[38;5;209m\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u259B\u2598\x1B[0m ${opts.model || "default model"} \xB7 ${opts.effort} effort`,
|
|
2488
|
+
`\x1B[38;5;209m \u2598\u2598 \u259D\u259D\x1B[0m \x1B[2m${opts.cwd}\x1B[0m`,
|
|
2489
|
+
"",
|
|
2490
|
+
`\x1B[36m\u276F\x1B[0m ${opts.prompt.split("\n")[0].slice(0, 200)}`,
|
|
2491
|
+
""
|
|
2492
|
+
].join("\r\n")
|
|
2493
|
+
);
|
|
2494
|
+
let handle = null;
|
|
2495
|
+
let line = "";
|
|
2496
|
+
term.bind({
|
|
2497
|
+
write: (data) => {
|
|
2498
|
+
for (const ch of data.replace(/\x1b\[[0-9;?]*[A-Za-z~]|\x1b./g, "")) {
|
|
2499
|
+
if (ch === "\r") {
|
|
2500
|
+
const text = line.trim();
|
|
2501
|
+
line = "";
|
|
2502
|
+
term.write("\r\n");
|
|
2503
|
+
if (text && handle) {
|
|
2504
|
+
cb.log([{ kind: "manager", text: `\u25B6 Typed in the terminal: ${text}` }]);
|
|
2505
|
+
handle.send(text);
|
|
2506
|
+
}
|
|
2507
|
+
} else if (ch === "\x7F" || ch === "\b") {
|
|
2508
|
+
if (line) term.write("\b \b");
|
|
2509
|
+
line = line.slice(0, -1);
|
|
2510
|
+
} else if (ch >= " ") {
|
|
2511
|
+
line += ch;
|
|
2512
|
+
term.write(ch);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
},
|
|
2516
|
+
resize: () => void 0
|
|
2517
|
+
});
|
|
2518
|
+
handle = start({
|
|
2519
|
+
...cb,
|
|
2520
|
+
log: (entries) => {
|
|
2521
|
+
cb.log(entries);
|
|
2522
|
+
for (const e of entries) term.write(`${e.kind === "tool" || e.kind === "text" && e.text.startsWith("\u25CF") ? "\r\n" : ""}${ANSI[e.kind](e.text)}\r
|
|
2523
|
+
`);
|
|
2524
|
+
},
|
|
2525
|
+
finished: (r) => {
|
|
2526
|
+
term.bind(null);
|
|
2527
|
+
term.note(`\u2500\u2500 ${name} session ended \u2500\u2500`);
|
|
2528
|
+
cb.finished(r);
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
return handle;
|
|
2532
|
+
}
|
|
1428
2533
|
var USAGE_WARNING_AT = 4;
|
|
1429
2534
|
var USAGE_WARNING_MS = 5 * 6e4;
|
|
1430
2535
|
var sessionsStarted = 0;
|
|
@@ -1562,8 +2667,14 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
1562
2667
|
releaseDesk: async () => void 0,
|
|
1563
2668
|
startSession: (opts, cb) => {
|
|
1564
2669
|
if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
|
|
1565
|
-
return opts.role === "ceo" ? ceoSession(opts,
|
|
2670
|
+
return inTerminal(opts, cb, (c) => opts.role === "ceo" ? ceoSession(opts, c) : fakeSession(opts, c, deskRepo.get(opts.cwd) ?? [...repos.keys()][0]));
|
|
1566
2671
|
},
|
|
2672
|
+
terminals: true,
|
|
2673
|
+
reconnectClis: async () => [],
|
|
2674
|
+
// fake sessions end with the office
|
|
2675
|
+
hooksReady: () => void 0,
|
|
2676
|
+
releaseClis: () => void 0,
|
|
2677
|
+
detectClis: async () => CLIS.map((c) => ({ id: c.id, label: c.label, installed: true, version: "demo", integrated: c.integrated })),
|
|
1567
2678
|
previews: demoPreviews,
|
|
1568
2679
|
office: demoOffice
|
|
1569
2680
|
};
|
|
@@ -1940,9 +3051,9 @@ Acceptance criteria:
|
|
|
1940
3051
|
}
|
|
1941
3052
|
|
|
1942
3053
|
// server/swarm.ts
|
|
1943
|
-
import
|
|
1944
|
-
import
|
|
1945
|
-
import
|
|
3054
|
+
import fs10 from "node:fs/promises";
|
|
3055
|
+
import path12 from "node:path";
|
|
3056
|
+
import crypto6 from "node:crypto";
|
|
1946
3057
|
|
|
1947
3058
|
// server/ceo.ts
|
|
1948
3059
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -2109,6 +3220,7 @@ function createOfficeTools(h) {
|
|
|
2109
3220
|
const server2 = createSdkMcpServer({ name: "office", version: "1.0.0", tools: defs });
|
|
2110
3221
|
return {
|
|
2111
3222
|
server: server2,
|
|
3223
|
+
serve: () => createSdkMcpServer({ name: "office", version: "1.0.0", tools: defs }).instance,
|
|
2112
3224
|
async call(name, args) {
|
|
2113
3225
|
const def = defs.find((d) => d.name === name);
|
|
2114
3226
|
if (!def) throw new Error(`No office tool ${name}`);
|
|
@@ -2121,11 +3233,11 @@ function ceoSystemPrompt(o) {
|
|
|
2121
3233
|
const manager = o.manager ? `the manager, ${o.manager}` : "the human manager";
|
|
2122
3234
|
return [
|
|
2123
3235
|
`You are ${o.name}, the CEO of ${o.company || "an autonomous software company"}, run from an office building called cubefarm. You work from the corner office in the lobby.`,
|
|
2124
|
-
`Every floor of the building is one GitHub repository with its own team of
|
|
3236
|
+
`Every floor of the building is one GitHub repository with its own team of AI coding agents. Developers pick up GitHub issues, each in their own git worktree, and open pull requests. QA testers review and verify every pull request (code review, tests, build, and a real browser via Playwright); when every tester is busy, a free developer who didn't write the PR covers QA. On floors with auto-merge on, the office merges a PR by itself once QA passes and GitHub's checks are green, and sends failing checks or merge conflicts back to a developer; on the others, ${manager} merges. The manager is your board: they approve hires and let-gos.`,
|
|
2125
3237
|
"",
|
|
2126
3238
|
"Your job is to run the company, not to write code:",
|
|
2127
3239
|
"- Understand each project: what it is, its stack, how far along it is, and what kind of people it needs. Projects differ a lot. A static marketing site, a 3D browser game and a REST API need different specialists and different QA.",
|
|
2128
|
-
`- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean:
|
|
3240
|
+
`- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: agents on the same coding agent share one subscription's usage limits${o.sessionLimit ? ` and at most ${o.sessionLimit} sessions run at once` : ""}, so a floor rarely needs more than ${o.teamCap} people. Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
|
|
2129
3241
|
"- Plan the work: turn a floor's brief into small, well-specified GitHub issues, one agent-session each, with acceptance criteria. Route each to a specialty. The office hands issues out itself: a free specialist gets first pick of their specialty, and otherwise any free developer takes the next issue that can start, so a specialty is a preference, not a lock.",
|
|
2130
3242
|
"- Write each floor's QA brief: what QA testers must check for this kind of project (for a 3D game: the canvas renders, controls respond, frame rate is smooth; for a website: links, phone layout, accessibility; for an API: status codes, validation, error cases).",
|
|
2131
3243
|
"",
|
|
@@ -2558,6 +3670,197 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
2558
3670
|
return `\u{1F422} Claude's usage is getting high${what ? ` (${what})` : ""}. Until ${clock(until, now2)} the office finishes open work first and starts at most ${sessions} session${sessions === 1 ? "" : "s"} at a time.`;
|
|
2559
3671
|
}
|
|
2560
3672
|
|
|
3673
|
+
// server/terminal.ts
|
|
3674
|
+
import fs9 from "node:fs/promises";
|
|
3675
|
+
import path11 from "node:path";
|
|
3676
|
+
import xtermHeadless from "@xterm/headless";
|
|
3677
|
+
import xtermSerialize from "@xterm/addon-serialize";
|
|
3678
|
+
var { Terminal } = xtermHeadless;
|
|
3679
|
+
var { SerializeAddon } = xtermSerialize;
|
|
3680
|
+
var TERM_COLS = 120;
|
|
3681
|
+
var TERM_ROWS = 32;
|
|
3682
|
+
var SCROLLBACK = 5e3;
|
|
3683
|
+
var STALE_BYTES = 4 * 1024 * 1024;
|
|
3684
|
+
var MOUSE_ENCODINGS = [1006, 1016];
|
|
3685
|
+
var clamp = (n, lo, hi, dflt) => {
|
|
3686
|
+
const v = Math.round(Number(n));
|
|
3687
|
+
return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : dflt;
|
|
3688
|
+
};
|
|
3689
|
+
var AgentTerminal = class {
|
|
3690
|
+
cols = TERM_COLS;
|
|
3691
|
+
rows = TERM_ROWS;
|
|
3692
|
+
/** Output arrived since the last save. */
|
|
3693
|
+
dirty = false;
|
|
3694
|
+
/** Closes a CLI left waiting at its prompt after its task (set by the runtime that left it there). */
|
|
3695
|
+
releaseIdle = null;
|
|
3696
|
+
/** Something the manager typed at that waiting prompt: the office takes it on as a follow-up (true) or refuses. */
|
|
3697
|
+
onIdlePrompt = null;
|
|
3698
|
+
/** Keys a viewer typed, after they went to the CLI (the runtime watches for Esc interrupting a turn). */
|
|
3699
|
+
onInput = null;
|
|
3700
|
+
term = new Terminal({ cols: TERM_COLS, rows: TERM_ROWS, scrollback: SCROLLBACK, allowProposedApi: true, scrollOnEraseInDisplay: true });
|
|
3701
|
+
ser = new SerializeAddon();
|
|
3702
|
+
viewers = /* @__PURE__ */ new Map();
|
|
3703
|
+
sink = null;
|
|
3704
|
+
mouseEncoding = null;
|
|
3705
|
+
resync = null;
|
|
3706
|
+
constructor() {
|
|
3707
|
+
this.term.loadAddon(this.ser);
|
|
3708
|
+
const decset = (on) => (params) => {
|
|
3709
|
+
for (const p of params) if (typeof p === "number" && MOUSE_ENCODINGS.includes(p)) this.mouseEncoding = on ? p : null;
|
|
3710
|
+
return false;
|
|
3711
|
+
};
|
|
3712
|
+
this.term.parser.registerCsiHandler({ prefix: "?", final: "h" }, decset(true));
|
|
3713
|
+
this.term.parser.registerCsiHandler({ prefix: "?", final: "l" }, decset(false));
|
|
3714
|
+
}
|
|
3715
|
+
/** Output from the CLI (or a note from the office): into the mirror and out to everyone watching. */
|
|
3716
|
+
write(data) {
|
|
3717
|
+
if (!data) return;
|
|
3718
|
+
this.term.write(data);
|
|
3719
|
+
this.dirty = true;
|
|
3720
|
+
this.send({ t: "data", data });
|
|
3721
|
+
}
|
|
3722
|
+
/** A dim line from the office itself, e.g. between sessions. */
|
|
3723
|
+
note(text) {
|
|
3724
|
+
this.write(`\r
|
|
3725
|
+
\x1B[2m${text}\x1B[0m\r
|
|
3726
|
+
`);
|
|
3727
|
+
}
|
|
3728
|
+
get live() {
|
|
3729
|
+
return this.sink !== null;
|
|
3730
|
+
}
|
|
3731
|
+
/** The CLI asked for application cursor keys: arrows are sent as ESC O A… rather than ESC [ A… */
|
|
3732
|
+
get appCursor() {
|
|
3733
|
+
return this.term.modes.applicationCursorKeysMode;
|
|
3734
|
+
}
|
|
3735
|
+
/** The running CLI, or null when it exits. */
|
|
3736
|
+
bind(sink) {
|
|
3737
|
+
this.sink = sink;
|
|
3738
|
+
sink?.resize(this.cols, this.rows);
|
|
3739
|
+
this.send({ t: "live", live: sink !== null });
|
|
3740
|
+
}
|
|
3741
|
+
resize(cols, rows) {
|
|
3742
|
+
const c = clamp(cols, 20, 400, this.cols);
|
|
3743
|
+
const r = clamp(rows, 5, 200, this.rows);
|
|
3744
|
+
if (c === this.cols && r === this.rows) return;
|
|
3745
|
+
this.cols = c;
|
|
3746
|
+
this.rows = r;
|
|
3747
|
+
this.term.resize(c, r);
|
|
3748
|
+
this.sink?.resize(c, r);
|
|
3749
|
+
}
|
|
3750
|
+
/** Everything a fresh terminal needs to show the same screen: scrollback, colours, cursor and modes. */
|
|
3751
|
+
snapshot() {
|
|
3752
|
+
return this.ser.serialize({ scrollback: SCROLLBACK }) + (this.mouseEncoding ? `\x1B[?${this.mouseEncoding}h` : "");
|
|
3753
|
+
}
|
|
3754
|
+
/** The visible rows as plain text, for spotting prompts the CLI is waiting on. */
|
|
3755
|
+
screen() {
|
|
3756
|
+
const b = this.term.buffer.active;
|
|
3757
|
+
const rows = [];
|
|
3758
|
+
for (let i = b.viewportY; i < b.viewportY + this.rows; i++) rows.push(b.getLine(i)?.translateToString(true) ?? "");
|
|
3759
|
+
return rows.join("\n");
|
|
3760
|
+
}
|
|
3761
|
+
/** Written to disk so a restarted office still shows what each agent did. */
|
|
3762
|
+
/** Resolves once everything written so far is parsed (xterm parses asynchronously). */
|
|
3763
|
+
flush() {
|
|
3764
|
+
return new Promise((resolve) => this.term.write("", resolve));
|
|
3765
|
+
}
|
|
3766
|
+
async save(file) {
|
|
3767
|
+
this.dirty = false;
|
|
3768
|
+
await this.flush();
|
|
3769
|
+
await fs9.mkdir(path11.dirname(file), { recursive: true });
|
|
3770
|
+
await fs9.writeFile(`${file}.tmp`, this.snapshot());
|
|
3771
|
+
await fs9.rename(`${file}.tmp`, file);
|
|
3772
|
+
}
|
|
3773
|
+
async load(file) {
|
|
3774
|
+
const data = await fs9.readFile(file, "utf8").catch(() => "");
|
|
3775
|
+
if (!data) return;
|
|
3776
|
+
this.term.write(data);
|
|
3777
|
+
this.note("(the office restarted)");
|
|
3778
|
+
this.dirty = false;
|
|
3779
|
+
}
|
|
3780
|
+
// ---------- viewers ----------
|
|
3781
|
+
attach(ws) {
|
|
3782
|
+
const viewer = { stale: false, queued: [] };
|
|
3783
|
+
this.viewers.set(ws, viewer);
|
|
3784
|
+
this.term.write("", () => {
|
|
3785
|
+
const queued = viewer.queued ?? [];
|
|
3786
|
+
viewer.queued = null;
|
|
3787
|
+
this.sendTo(ws, { t: "snapshot", data: this.snapshot(), cols: this.cols, rows: this.rows, live: this.live });
|
|
3788
|
+
for (const json of queued) if (ws.readyState === ws.OPEN) ws.send(json);
|
|
3789
|
+
});
|
|
3790
|
+
ws.on("message", (raw) => {
|
|
3791
|
+
let msg;
|
|
3792
|
+
try {
|
|
3793
|
+
msg = JSON.parse(String(raw));
|
|
3794
|
+
} catch {
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
if (msg.t === "input" && typeof msg.data === "string") {
|
|
3798
|
+
const data = msg.data.slice(0, 64 * 1024);
|
|
3799
|
+
this.sink?.write(data);
|
|
3800
|
+
this.onInput?.(data);
|
|
3801
|
+
} else if (msg.t === "resize") {
|
|
3802
|
+
this.resize(msg.cols, msg.rows);
|
|
3803
|
+
for (const other of this.viewers.keys()) if (other !== ws) this.sendTo(other, { t: "size", cols: this.cols, rows: this.rows });
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
ws.on("close", () => this.viewers.delete(ws));
|
|
3807
|
+
}
|
|
3808
|
+
dispose() {
|
|
3809
|
+
for (const ws of this.viewers.keys()) ws.close();
|
|
3810
|
+
this.viewers.clear();
|
|
3811
|
+
if (this.resync) clearTimeout(this.resync);
|
|
3812
|
+
this.sink = null;
|
|
3813
|
+
this.term.dispose();
|
|
3814
|
+
}
|
|
3815
|
+
send(msg) {
|
|
3816
|
+
if (this.viewers.size === 0) return;
|
|
3817
|
+
const json = JSON.stringify(msg);
|
|
3818
|
+
for (const [ws, v] of this.viewers) {
|
|
3819
|
+
if (ws.readyState !== ws.OPEN || v.stale) continue;
|
|
3820
|
+
if (v.queued) {
|
|
3821
|
+
v.queued.push(json);
|
|
3822
|
+
continue;
|
|
3823
|
+
}
|
|
3824
|
+
if (ws.bufferedAmount > STALE_BYTES) {
|
|
3825
|
+
v.stale = true;
|
|
3826
|
+
this.scheduleResync();
|
|
3827
|
+
continue;
|
|
3828
|
+
}
|
|
3829
|
+
ws.send(json);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
sendTo(ws, msg) {
|
|
3833
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
|
|
3834
|
+
}
|
|
3835
|
+
/** Viewers that fell behind get a fresh snapshot once their socket has drained, instead of an ever-growing backlog. */
|
|
3836
|
+
scheduleResync() {
|
|
3837
|
+
if (this.resync) return;
|
|
3838
|
+
this.resync = setTimeout(() => {
|
|
3839
|
+
this.resync = null;
|
|
3840
|
+
let waiting = false;
|
|
3841
|
+
for (const [ws, v] of this.viewers) {
|
|
3842
|
+
if (!v.stale) continue;
|
|
3843
|
+
if (ws.bufferedAmount > STALE_BYTES / 4) {
|
|
3844
|
+
waiting = true;
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3847
|
+
v.stale = false;
|
|
3848
|
+
this.sendTo(ws, { t: "snapshot", data: this.snapshot(), cols: this.cols, rows: this.rows, live: this.live });
|
|
3849
|
+
}
|
|
3850
|
+
if (waiting) this.scheduleResync();
|
|
3851
|
+
}, 1e3);
|
|
3852
|
+
}
|
|
3853
|
+
};
|
|
3854
|
+
|
|
3855
|
+
// shared/models.ts
|
|
3856
|
+
var isClaudeModel = (model) => /^(claude|opus|sonnet|haiku|fable|default|best|opusplan)\b/i.test(model.trim());
|
|
3857
|
+
function effectiveModel(own, cli, office, claudeDefault) {
|
|
3858
|
+
const suits = (m) => m.trim() !== "" && cli === "claude" === isClaudeModel(m);
|
|
3859
|
+
if (suits(own)) return own.trim();
|
|
3860
|
+
if (cli === office.defaultCli && suits(office.defaultModel)) return office.defaultModel.trim();
|
|
3861
|
+
return cli === "claude" ? claudeDefault : "";
|
|
3862
|
+
}
|
|
3863
|
+
|
|
2561
3864
|
// shared/types.ts
|
|
2562
3865
|
var CEO_ID = "ceo";
|
|
2563
3866
|
|
|
@@ -2605,8 +3908,8 @@ var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
|
2605
3908
|
var CEO_MODEL = "claude-opus-5-5";
|
|
2606
3909
|
var CEO_EFFORT = "xhigh";
|
|
2607
3910
|
var CEO_NAME = "Morgan";
|
|
2608
|
-
var CEO_DIR =
|
|
2609
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
3911
|
+
var CEO_DIR = path12.join(HOME_DIR, "ceo");
|
|
3912
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path12.resolve(import.meta.dirname, ".."));
|
|
2610
3913
|
var MAX_PENDING_REQUESTS = 8;
|
|
2611
3914
|
var MAX_ISSUES_PER_JOB = 12;
|
|
2612
3915
|
var KEEP_MESSAGES = 200;
|
|
@@ -2619,14 +3922,14 @@ var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
|
2619
3922
|
var MAX_ISSUE_FAILURES = 2;
|
|
2620
3923
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
2621
3924
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
2622
|
-
var SCREENS_DIR =
|
|
3925
|
+
var SCREENS_DIR = path12.join(HOME_DIR, "screens");
|
|
2623
3926
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
2624
|
-
var screenFile = (agentId, mime) =>
|
|
3927
|
+
var screenFile = (agentId, mime) => path12.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
2625
3928
|
async function loadScreen(agentId) {
|
|
2626
3929
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
2627
|
-
const file =
|
|
3930
|
+
const file = path12.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
2628
3931
|
try {
|
|
2629
|
-
const [data, stat] = await Promise.all([
|
|
3932
|
+
const [data, stat] = await Promise.all([fs10.readFile(file), fs10.stat(file)]);
|
|
2630
3933
|
return { data, mime, at: stat.mtimeMs };
|
|
2631
3934
|
} catch {
|
|
2632
3935
|
}
|
|
@@ -2634,8 +3937,11 @@ async function loadScreen(agentId) {
|
|
|
2634
3937
|
return null;
|
|
2635
3938
|
}
|
|
2636
3939
|
async function removeScreens(agentId) {
|
|
2637
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
3940
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs10.rm(path12.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
2638
3941
|
}
|
|
3942
|
+
var TERMINALS_DIR = path12.join(HOME_DIR, "terminals");
|
|
3943
|
+
var terminalFile = (agentId) => path12.join(TERMINALS_DIR, `${agentId}.ansi`);
|
|
3944
|
+
var TERMINAL_SAVE_MS = 2e4;
|
|
2639
3945
|
var QA_SCHEMA = {
|
|
2640
3946
|
type: "object",
|
|
2641
3947
|
additionalProperties: false,
|
|
@@ -2678,10 +3984,11 @@ var QA_SCHEMA = {
|
|
|
2678
3984
|
function parseReport(result) {
|
|
2679
3985
|
let raw = result.structured;
|
|
2680
3986
|
if (!raw && result.text) {
|
|
2681
|
-
const
|
|
2682
|
-
|
|
3987
|
+
const fenced = [...result.text.matchAll(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/g)].map((m) => m[1]).reverse();
|
|
3988
|
+
for (const json of [...fenced, result.text.match(/\{[\s\S]*\}/)?.[0]].filter((j) => !!j)) {
|
|
2683
3989
|
try {
|
|
2684
|
-
raw = JSON.parse(json
|
|
3990
|
+
raw = JSON.parse(json);
|
|
3991
|
+
break;
|
|
2685
3992
|
} catch {
|
|
2686
3993
|
raw = null;
|
|
2687
3994
|
}
|
|
@@ -2702,6 +4009,7 @@ function parseReport(result) {
|
|
|
2702
4009
|
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
2703
4010
|
var ICON = { pass: "\u2705", fail: "\u274C", skip: "\u23ED\uFE0F" };
|
|
2704
4011
|
var Swarm = class {
|
|
4012
|
+
// coding-agent CLIs found on this machine (detected at startup)
|
|
2705
4013
|
constructor(backend) {
|
|
2706
4014
|
this.backend = backend;
|
|
2707
4015
|
this.previews = new Previews(backend, {
|
|
@@ -2718,7 +4026,8 @@ var Swarm = class {
|
|
|
2718
4026
|
sessionLimit: 0,
|
|
2719
4027
|
defaultModel: DEFAULT_MODEL,
|
|
2720
4028
|
defaultEffort: "medium",
|
|
2721
|
-
|
|
4029
|
+
runtime: "terminal",
|
|
4030
|
+
defaultCli: "claude",
|
|
2722
4031
|
hiring: "approve",
|
|
2723
4032
|
teamCap: 6,
|
|
2724
4033
|
ceoHeartbeatMin: 60,
|
|
@@ -2781,10 +4090,11 @@ var Swarm = class {
|
|
|
2781
4090
|
// sessions cut off by the hand-over are the restarted office's to recover
|
|
2782
4091
|
};
|
|
2783
4092
|
lastOfficeView = "";
|
|
4093
|
+
clis = [];
|
|
2784
4094
|
// ---------- lifecycle ----------
|
|
2785
4095
|
async init() {
|
|
2786
4096
|
try {
|
|
2787
|
-
const raw = await
|
|
4097
|
+
const raw = await fs10.readFile(STATE_FILE, "utf8");
|
|
2788
4098
|
const loaded = JSON.parse(raw);
|
|
2789
4099
|
this.state = {
|
|
2790
4100
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -2807,7 +4117,9 @@ var Swarm = class {
|
|
|
2807
4117
|
brief: a.brief ?? "",
|
|
2808
4118
|
hiredBy: a.hiredBy ?? "manager",
|
|
2809
4119
|
look: a.look ?? lookFor(a.name),
|
|
2810
|
-
task: a.task ?? (a.issueNumber ? "issue" : null)
|
|
4120
|
+
task: a.task ?? (a.issueNumber ? "issue" : null),
|
|
4121
|
+
cli: isCli(a.cli) ? a.cli : "",
|
|
4122
|
+
sessionCli: a.sessionCli ?? (a.sessionId ? "claude" : null)
|
|
2811
4123
|
})),
|
|
2812
4124
|
qa: (loaded.qa ?? []).map((q) => ({
|
|
2813
4125
|
...q,
|
|
@@ -2827,13 +4139,16 @@ var Swarm = class {
|
|
|
2827
4139
|
phoneReadAt: loaded.phoneReadAt ?? 0
|
|
2828
4140
|
};
|
|
2829
4141
|
for (const m of this.state.messages) this.messageSeq = Math.max(this.messageSeq, m.id + 1);
|
|
2830
|
-
if (!this.state.settings.defaultModel) this.state.settings.defaultModel = DEFAULT_MODEL;
|
|
2831
4142
|
if (!EFFORTS.includes(this.state.settings.defaultEffort)) this.state.settings.defaultEffort = "medium";
|
|
4143
|
+
if (this.state.settings.runtime !== "sdk") this.state.settings.runtime = "terminal";
|
|
4144
|
+
if (!isCli(this.state.settings.defaultCli)) this.state.settings.defaultCli = "claude";
|
|
4145
|
+
if (!this.state.settings.defaultModel && this.state.settings.defaultCli === "claude") this.state.settings.defaultModel = DEFAULT_MODEL;
|
|
2832
4146
|
const old = this.state.settings;
|
|
2833
4147
|
if (old.maxConcurrent !== void 0) {
|
|
2834
4148
|
if (loaded.settings?.sessionLimit === void 0) old.sessionLimit = old.maxConcurrent === 4 ? 0 : old.maxConcurrent;
|
|
2835
4149
|
delete old.maxConcurrent;
|
|
2836
4150
|
}
|
|
4151
|
+
delete old.permissionMode;
|
|
2837
4152
|
if (loaded.settings && loaded.settings.setupDone === void 0 && this.state.repos.length > 0) {
|
|
2838
4153
|
Object.assign(this.state.settings, { setupDone: true, tutorialStep: -1 });
|
|
2839
4154
|
}
|
|
@@ -2844,14 +4159,30 @@ var Swarm = class {
|
|
|
2844
4159
|
for (const a of this.state.agents) {
|
|
2845
4160
|
const tail = a.logTail ?? [];
|
|
2846
4161
|
for (const l of tail) this.logSeq = Math.max(this.logSeq, l.id + 1);
|
|
2847
|
-
this.agentRt.set(a.id, { log: tail, pending: [], session: null, currentTool: null, browserUrl: null, screenshot: await loadScreen(a.id), shots: [] });
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
4162
|
+
this.agentRt.set(a.id, { log: tail, pending: [], session: null, currentTool: null, browserUrl: null, screenshot: await loadScreen(a.id), shots: [], terminal: await this.loadTerminal(a.id) });
|
|
4163
|
+
}
|
|
4164
|
+
const back = await this.backend.reconnectClis((id) => {
|
|
4165
|
+
const a = this.state.agents.find((x) => x.id === id);
|
|
4166
|
+
return a && a.role !== "ceo" ? this.terminalFor(a) : null;
|
|
4167
|
+
}).catch((err) => {
|
|
4168
|
+
console.warn("could not reconnect to the terminal keeper", err);
|
|
4169
|
+
return [];
|
|
4170
|
+
});
|
|
4171
|
+
const carryOn = [];
|
|
4172
|
+
for (const a of this.state.agents) {
|
|
4173
|
+
if (!BUSY.includes(a.status)) continue;
|
|
4174
|
+
if (back.some((c) => c.agentId === a.id && c.busy)) {
|
|
4175
|
+
carryOn.push(a);
|
|
4176
|
+
continue;
|
|
2852
4177
|
}
|
|
4178
|
+
a.status = "stopped";
|
|
4179
|
+
a.lastError = "The swarm server restarted while this agent was working.";
|
|
4180
|
+
interrupted.push(a);
|
|
2853
4181
|
}
|
|
4182
|
+
for (const c of back) if (c.busy && !carryOn.some((a) => a.id === c.agentId)) this.agentRt.get(c.agentId)?.terminal?.releaseIdle?.();
|
|
2854
4183
|
for (const r of this.state.repos) this.repoRt.set(r.id, { issues: [], pulls: [], lastSync: null, syncing: false, cloneStatus: "pending", lastMergedAt: null, folderSync: null, merging: false });
|
|
4184
|
+
for (const a of carryOn) this.reattachSession(a);
|
|
4185
|
+
this.backend.hooksReady();
|
|
2855
4186
|
try {
|
|
2856
4187
|
this.user = await this.backend.user();
|
|
2857
4188
|
} catch (err) {
|
|
@@ -2873,7 +4204,8 @@ var Swarm = class {
|
|
|
2873
4204
|
await Promise.all([
|
|
2874
4205
|
...this.state.agents.map((a) => {
|
|
2875
4206
|
const repo = this.state.repos.find((r) => r.id === a.repoId);
|
|
2876
|
-
|
|
4207
|
+
if (!repo || back.some((c) => c.agentId === a.id)) return void 0;
|
|
4208
|
+
return this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0);
|
|
2877
4209
|
}),
|
|
2878
4210
|
this.previews.clearOrphans(this.state.repos)
|
|
2879
4211
|
]);
|
|
@@ -2884,6 +4216,11 @@ var Swarm = class {
|
|
|
2884
4216
|
if (updated) await this.reportUpdate(updated);
|
|
2885
4217
|
setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
|
|
2886
4218
|
setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
|
|
4219
|
+
setInterval(() => void this.saveTerminals(), TERMINAL_SAVE_MS);
|
|
4220
|
+
void this.backend.detectClis().then((clis) => {
|
|
4221
|
+
this.clis = clis;
|
|
4222
|
+
this.broadcast({ type: "clis", clis });
|
|
4223
|
+
}).catch((err) => console.warn("could not look for agent CLIs", err));
|
|
2887
4224
|
this.save();
|
|
2888
4225
|
setTimeout(() => this.schedule(), 1e3);
|
|
2889
4226
|
}
|
|
@@ -2955,6 +4292,8 @@ var Swarm = class {
|
|
|
2955
4292
|
skin: a.skin,
|
|
2956
4293
|
model: a.model,
|
|
2957
4294
|
effort: a.effort,
|
|
4295
|
+
cli: a.cli,
|
|
4296
|
+
terminal: !!rt.terminal,
|
|
2958
4297
|
status: a.status,
|
|
2959
4298
|
issueNumber: a.issueNumber,
|
|
2960
4299
|
issueTitle: a.issueTitle,
|
|
@@ -3004,6 +4343,7 @@ var Swarm = class {
|
|
|
3004
4343
|
messages: this.state.messages.slice(-100),
|
|
3005
4344
|
phoneReadAt: this.state.phoneReadAt,
|
|
3006
4345
|
usage: this.usageNow(),
|
|
4346
|
+
clis: this.clis,
|
|
3007
4347
|
officeCommit: this.officeHead?.slice(0, 7) ?? null,
|
|
3008
4348
|
officeUpdate: this.officeHead ? this.officeUpdateView() : void 0
|
|
3009
4349
|
};
|
|
@@ -3071,10 +4411,10 @@ var Swarm = class {
|
|
|
3071
4411
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
3072
4412
|
this.saveTimer = null;
|
|
3073
4413
|
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
3074
|
-
await
|
|
4414
|
+
await fs10.mkdir(path12.dirname(STATE_FILE), { recursive: true });
|
|
3075
4415
|
const tmp = `${STATE_FILE}.tmp`;
|
|
3076
|
-
await
|
|
3077
|
-
await
|
|
4416
|
+
await fs10.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
4417
|
+
await fs10.rename(tmp, STATE_FILE);
|
|
3078
4418
|
}
|
|
3079
4419
|
// ---------- lookups ----------
|
|
3080
4420
|
repo(id) {
|
|
@@ -3156,7 +4496,7 @@ var Swarm = class {
|
|
|
3156
4496
|
const name = fullName.split("/")[1];
|
|
3157
4497
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
3158
4498
|
if (taken) throw new HttpError(409, `${taken.path} already exists and isn't a clone of ${fullName}. Connect that folder instead, or rename it first.`);
|
|
3159
|
-
return
|
|
4499
|
+
return path12.join(root, name);
|
|
3160
4500
|
}
|
|
3161
4501
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
3162
4502
|
async createRepo(name, opts) {
|
|
@@ -3168,11 +4508,11 @@ var Swarm = class {
|
|
|
3168
4508
|
}
|
|
3169
4509
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
3170
4510
|
async listProjectFolders(dir) {
|
|
3171
|
-
const root = dir?.trim() ?
|
|
4511
|
+
const root = dir?.trim() ? path12.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
3172
4512
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
3173
4513
|
throw new HttpError(400, err.message);
|
|
3174
4514
|
});
|
|
3175
|
-
const same = (a, b) =>
|
|
4515
|
+
const same = (a, b) => path12.resolve(a).toLowerCase() === path12.resolve(b).toLowerCase();
|
|
3176
4516
|
const floorOf = (f) => this.state.repos.find((r) => r.localPath && same(r.localPath, f.path) || f.github && r.id.toLowerCase() === f.github.toLowerCase())?.floor ?? null;
|
|
3177
4517
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
3178
4518
|
}
|
|
@@ -3352,7 +4692,7 @@ var Swarm = class {
|
|
|
3352
4692
|
rt.folderSync = status;
|
|
3353
4693
|
this.emitRepo(repo);
|
|
3354
4694
|
if (status && status !== before) {
|
|
3355
|
-
const name = repo.localPath ?
|
|
4695
|
+
const name = repo.localPath ? path12.basename(repo.localPath) : repo.fullName;
|
|
3356
4696
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
3357
4697
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
3358
4698
|
}
|
|
@@ -3371,9 +4711,14 @@ var Swarm = class {
|
|
|
3371
4711
|
stopPreview(id) {
|
|
3372
4712
|
return this.previews.stop(this.repo(id));
|
|
3373
4713
|
}
|
|
3374
|
-
/**
|
|
3375
|
-
|
|
4714
|
+
/**
|
|
4715
|
+
* Server shutdown: stop every floor's app so nothing is left holding a preview port. Agents' CLIs keep working
|
|
4716
|
+
* through a restart (the terminal keeper holds them for the next start) and stop when the office quits.
|
|
4717
|
+
*/
|
|
4718
|
+
async shutdown(restart = false) {
|
|
3376
4719
|
await this.writeState().catch((err) => console.warn("could not save the state", err));
|
|
4720
|
+
this.backend.releaseClis(restart);
|
|
4721
|
+
await this.saveTerminals(true);
|
|
3377
4722
|
await this.previews.stopAll(this.state.repos);
|
|
3378
4723
|
}
|
|
3379
4724
|
async cloneRepo(id) {
|
|
@@ -3484,7 +4829,7 @@ var Swarm = class {
|
|
|
3484
4829
|
}
|
|
3485
4830
|
const name = opts.name?.trim() || this.freeName(role);
|
|
3486
4831
|
const agent = {
|
|
3487
|
-
id:
|
|
4832
|
+
id: crypto6.randomUUID(),
|
|
3488
4833
|
name,
|
|
3489
4834
|
repoId: repo.id,
|
|
3490
4835
|
role,
|
|
@@ -3500,6 +4845,7 @@ var Swarm = class {
|
|
|
3500
4845
|
skin: opts.appearance?.skin ?? pick(SKIN),
|
|
3501
4846
|
model: opts.model ?? "",
|
|
3502
4847
|
effort: EFFORTS.includes(opts.effort) ? opts.effort : "",
|
|
4848
|
+
cli: isCli(opts.cli) ? opts.cli : "",
|
|
3503
4849
|
status: "idle",
|
|
3504
4850
|
issueNumber: null,
|
|
3505
4851
|
issueTitle: null,
|
|
@@ -3511,11 +4857,12 @@ var Swarm = class {
|
|
|
3511
4857
|
costUsd: 0,
|
|
3512
4858
|
turns: 0,
|
|
3513
4859
|
sessionId: null,
|
|
4860
|
+
sessionCli: null,
|
|
3514
4861
|
lastError: null,
|
|
3515
4862
|
logTail: []
|
|
3516
4863
|
};
|
|
3517
4864
|
this.state.agents.push(agent);
|
|
3518
|
-
this.agentRt.set(agent.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [] });
|
|
4865
|
+
this.agentRt.set(agent.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [], terminal: null });
|
|
3519
4866
|
this.appendLog(agent, [
|
|
3520
4867
|
{ kind: "system", text: role === "qa" ? `\u{1F50D} ${name} joined the QA lab on floor ${repo.floor} (${repo.fullName}).` : `\u{1F44B} ${name} joined floor ${repo.floor} (${repo.fullName}).` },
|
|
3521
4868
|
...agent.title ? [{ kind: "system", text: `\u{1FAAA} ${agent.title}${agent.specialty ? ` \xB7 takes swarm:${agent.specialty} issues first` : ""}` }] : []
|
|
@@ -3538,6 +4885,7 @@ var Swarm = class {
|
|
|
3538
4885
|
}
|
|
3539
4886
|
updateAgent(id, patch) {
|
|
3540
4887
|
const a = this.agent(id);
|
|
4888
|
+
if (patch.cli !== void 0 && a.role !== "ceo") a.cli = isCli(patch.cli) ? patch.cli : "";
|
|
3541
4889
|
if (patch.name?.trim() && patch.name.trim() !== a.name) {
|
|
3542
4890
|
a.name = patch.name.trim().slice(0, 24);
|
|
3543
4891
|
a.look = lookFor(a.name);
|
|
@@ -3565,7 +4913,10 @@ var Swarm = class {
|
|
|
3565
4913
|
this.decide(r, { status: "approved", note: "They were let go directly.", decidedBy: "manager" });
|
|
3566
4914
|
}
|
|
3567
4915
|
this.agentRt.get(id)?.session?.stop();
|
|
4916
|
+
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
4917
|
+
this.agentRt.get(id)?.terminal?.dispose();
|
|
3568
4918
|
void removeScreens(id);
|
|
4919
|
+
void fs10.rm(terminalFile(id), { force: true }).catch(() => void 0);
|
|
3569
4920
|
for (const q of this.state.qa) {
|
|
3570
4921
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
3571
4922
|
if (q.devAgentId === id) q.devAgentId = null;
|
|
@@ -3591,6 +4942,20 @@ var Swarm = class {
|
|
|
3591
4942
|
this.emitAgent(a);
|
|
3592
4943
|
this.save();
|
|
3593
4944
|
}
|
|
4945
|
+
/** An agent whose CLI kept working through the office's restart: the office follows its session again. */
|
|
4946
|
+
reattachSession(a) {
|
|
4947
|
+
const repo = this.state.repos.find((r) => r.id === a.repoId);
|
|
4948
|
+
if (!repo) return;
|
|
4949
|
+
this.appendLog(a, [{ kind: "system", text: "\u21BB The office restarted; their CLI kept working and the office is following it again." }]);
|
|
4950
|
+
this.startAgentSession(a, repo, this.backend.deskDir(repo.fullName, this.agentSlug(a)), "", "", a.sessionId ?? void 0, void 0, "reattach");
|
|
4951
|
+
}
|
|
4952
|
+
/** The manager pressed Esc in the agent's terminal and the CLI stopped its turn: a Stop that leaves the CLI to them. */
|
|
4953
|
+
interrupted(a) {
|
|
4954
|
+
if (!BUSY.includes(a.status)) return;
|
|
4955
|
+
a.status = "stopped";
|
|
4956
|
+
a.lastError = "Interrupted in the terminal";
|
|
4957
|
+
this.appendLog(a, [{ kind: "manager", text: "\u25A0 Interrupted in the terminal. Type there to carry on." }]);
|
|
4958
|
+
}
|
|
3594
4959
|
resetAgent(id) {
|
|
3595
4960
|
const a = this.agent(id);
|
|
3596
4961
|
if (BUSY.includes(a.status)) throw new HttpError(409, `${a.name} is busy; stop them first`);
|
|
@@ -3598,6 +4963,7 @@ var Swarm = class {
|
|
|
3598
4963
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
3599
4964
|
if (q.devAgentId === id && q.status === "fixing") this.setQa(q, { status: "failed" });
|
|
3600
4965
|
}
|
|
4966
|
+
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
3601
4967
|
this.clearTask(a);
|
|
3602
4968
|
this.appendLog(a, [{ kind: "system", text: "\u21BA Cleared desk. Ready for new work." }]);
|
|
3603
4969
|
this.save();
|
|
@@ -3632,6 +4998,57 @@ var Swarm = class {
|
|
|
3632
4998
|
linkedRepos(repo) {
|
|
3633
4999
|
return repo.links.map((id) => this.state.repos.find((r) => r.id === id)).filter((r) => !!r);
|
|
3634
5000
|
}
|
|
5001
|
+
/**
|
|
5002
|
+
* How an agent's next session runs: the CLI in their terminal (the terminal runtime), or Claude Code through the
|
|
5003
|
+
* SDK. A session can only be resumed by the CLI that made it, so a follow-up stays with that CLI.
|
|
5004
|
+
*/
|
|
5005
|
+
sessionRuntime(a, resume) {
|
|
5006
|
+
const inTerminal2 = this.state.settings.runtime === "terminal" && this.backend.terminals;
|
|
5007
|
+
let cli = a.role === "ceo" ? "claude" : a.cli || this.state.settings.defaultCli;
|
|
5008
|
+
if (resume && a.sessionCli && a.sessionCli !== cli) {
|
|
5009
|
+
if (inTerminal2) cli = a.sessionCli;
|
|
5010
|
+
else if (a.sessionCli !== "claude") resume = void 0;
|
|
5011
|
+
}
|
|
5012
|
+
if (!inTerminal2) return { resumeSessionId: resume };
|
|
5013
|
+
const what = a.role === "ceo" ? a.issueTitle : a.task === "qa" ? `QA \xB7 PR #${a.prNumber}` : a.task === "fix" ? `fixing PR #${a.prNumber}` : a.issueNumber ? `#${a.issueNumber} ${a.issueTitle ?? ""}` : null;
|
|
5014
|
+
return { terminal: this.terminalFor(a), cli, label: `${a.name}${what ? ` \xB7 ${what}` : ""}`.slice(0, 80).trim(), resumeSessionId: resume };
|
|
5015
|
+
}
|
|
5016
|
+
/** The model to ask for (the Agent SDK runs Claude Code). '' lets another coding agent use its own default. */
|
|
5017
|
+
modelFor(a, cli) {
|
|
5018
|
+
return effectiveModel(a.model, cli ?? "claude", this.state.settings, DEFAULT_MODEL);
|
|
5019
|
+
}
|
|
5020
|
+
// ---------- terminals ----------
|
|
5021
|
+
async loadTerminal(agentId) {
|
|
5022
|
+
if (!await fs10.stat(terminalFile(agentId)).catch(() => null)) return null;
|
|
5023
|
+
const t = this.newTerminal(agentId);
|
|
5024
|
+
await t.load(terminalFile(agentId));
|
|
5025
|
+
return t;
|
|
5026
|
+
}
|
|
5027
|
+
/** The agent's terminal, made the first time they run in the terminal runtime. */
|
|
5028
|
+
terminalFor(a) {
|
|
5029
|
+
const rt = this.agentRt.get(a.id);
|
|
5030
|
+
rt.terminal ??= this.newTerminal(a.id);
|
|
5031
|
+
return rt.terminal;
|
|
5032
|
+
}
|
|
5033
|
+
newTerminal(agentId) {
|
|
5034
|
+
const t = new AgentTerminal();
|
|
5035
|
+
t.onIdlePrompt = (text) => {
|
|
5036
|
+
void this.message(agentId, text, true).catch(() => void 0);
|
|
5037
|
+
return !!this.agentRt.get(agentId)?.session;
|
|
5038
|
+
};
|
|
5039
|
+
return t;
|
|
5040
|
+
}
|
|
5041
|
+
async saveTerminals(all = false) {
|
|
5042
|
+
for (const [id, rt] of this.agentRt) {
|
|
5043
|
+
if (rt.terminal && (all || rt.terminal.dirty)) await rt.terminal.save(terminalFile(id)).catch((err) => console.warn("could not save a terminal", err));
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
/** A browser opened an agent's terminal (/ws/term?agent=<id>). */
|
|
5047
|
+
attachTerminal(agentId, ws) {
|
|
5048
|
+
const t = this.agentRt.get(agentId)?.terminal;
|
|
5049
|
+
if (!t) return ws.close(4404, "That agent has no terminal");
|
|
5050
|
+
t.attach(ws);
|
|
5051
|
+
}
|
|
3635
5052
|
buildSystemAppend(a, repo, cwd, branch, fixing) {
|
|
3636
5053
|
const linked = this.linkedRepos(repo).map((r) => `- ${r.fullName}: read-only reference clone at ${this.backend.mainDir(r.fullName)}`);
|
|
3637
5054
|
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
@@ -3723,24 +5140,29 @@ Note from the manager: ${note}` : ""
|
|
|
3723
5140
|
].filter(Boolean).join("\n");
|
|
3724
5141
|
this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch));
|
|
3725
5142
|
}
|
|
3726
|
-
startAgentSession(a, repo, cwd, prompt, systemAppend, resumeSessionId, outputSchema) {
|
|
5143
|
+
startAgentSession(a, repo, cwd, prompt, systemAppend, resumeSessionId, outputSchema, mode = null) {
|
|
3727
5144
|
const rt = this.agentRt.get(a.id);
|
|
3728
5145
|
a.status = "working";
|
|
5146
|
+
const how = this.sessionRuntime(a, resumeSessionId);
|
|
3729
5147
|
this.emitAgent(a);
|
|
3730
5148
|
rt.session = this.backend.startSession(
|
|
3731
5149
|
{
|
|
3732
5150
|
cwd,
|
|
3733
5151
|
prompt,
|
|
3734
5152
|
systemAppend,
|
|
3735
|
-
model: a
|
|
5153
|
+
model: this.modelFor(a, how.cli),
|
|
3736
5154
|
effort: a.effort || this.state.settings.defaultEffort,
|
|
3737
5155
|
browserTesting: repo.browserTesting,
|
|
3738
|
-
permissionMode: this.state.settings.permissionMode,
|
|
3739
5156
|
additionalDirectories: this.linkedRepos(repo).map((r) => this.backend.mainDir(r.fullName)),
|
|
3740
5157
|
role: a.task === "qa" ? "qa" : a.role,
|
|
3741
5158
|
// a developer covering QA works under QA's rules
|
|
3742
5159
|
outputSchema,
|
|
3743
|
-
|
|
5160
|
+
// A developer's CLI stays at its prompt afterwards, for the manager and for follow-ups; QA's closes.
|
|
5161
|
+
keepAlive: a.task !== "qa",
|
|
5162
|
+
typed: mode === "typed",
|
|
5163
|
+
reattach: mode === "reattach",
|
|
5164
|
+
agentId: a.id,
|
|
5165
|
+
...how
|
|
3744
5166
|
},
|
|
3745
5167
|
{
|
|
3746
5168
|
log: (entries) => this.appendLog(a, entries),
|
|
@@ -3751,6 +5173,7 @@ Note from the manager: ${note}` : ""
|
|
|
3751
5173
|
},
|
|
3752
5174
|
sessionId: (id) => {
|
|
3753
5175
|
a.sessionId = id;
|
|
5176
|
+
a.sessionCli = how.cli ?? "claude";
|
|
3754
5177
|
},
|
|
3755
5178
|
browserUrl: (url) => {
|
|
3756
5179
|
rt.browserUrl = url;
|
|
@@ -3762,13 +5185,12 @@ Note from the manager: ${note}` : ""
|
|
|
3762
5185
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
3763
5186
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
3764
5187
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
3765
|
-
void removeScreens(a.id).then(() =>
|
|
5188
|
+
void removeScreens(a.id).then(() => fs10.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs10.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
|
|
3766
5189
|
},
|
|
3767
5190
|
limited: (at) => this.pauseForLimit(at),
|
|
3768
5191
|
usageWarning: (info) => this.paceForWarning(info),
|
|
3769
5192
|
finished: (result) => void this.onFinished(a, repo, result)
|
|
3770
|
-
}
|
|
3771
|
-
repo.defaultBranch
|
|
5193
|
+
}
|
|
3772
5194
|
);
|
|
3773
5195
|
}
|
|
3774
5196
|
async onFinished(a, repo, result) {
|
|
@@ -3781,6 +5203,7 @@ Note from the manager: ${note}` : ""
|
|
|
3781
5203
|
a.costUsd += result.costUsd;
|
|
3782
5204
|
a.turns += result.turns;
|
|
3783
5205
|
void this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0);
|
|
5206
|
+
if (result.interrupted) this.interrupted(a);
|
|
3784
5207
|
if (a.task === "qa") await this.onQaFinished(a, repo, result);
|
|
3785
5208
|
else if (a.task === "fix") this.onFixFinished(a, repo, result);
|
|
3786
5209
|
else await this.onIssueFinished(a, repo, result);
|
|
@@ -3916,7 +5339,7 @@ ${repo.qaBrief}`] : [],
|
|
|
3916
5339
|
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
3917
5340
|
`2. Review the code as a careful reviewer would: git diff origin/${repo.defaultBranch}...HEAD. Look for bugs, unhandled errors and edge cases, security problems, leftover debug code, and new logic without tests.`,
|
|
3918
5341
|
"3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
3919
|
-
repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${this.port(a)} (reserved for you) and exercise the change in a real browser with the Playwright tools: navigate, click, type, resize to a phone size, try edge cases, and check the console for errors. Take a screenshot with browser_take_screenshot of every important state: the screenshots are attached to the PR as evidence. Stop the server afterwards.` : "4. Exercise the changed behaviour directly (run the program, call the API, write a quick script).",
|
|
5342
|
+
repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${this.port(a)} (reserved for you) and exercise the change in a real browser with the Playwright tools: navigate, click, type, resize to a phone size, try edge cases, and check the console for errors. Take a screenshot with browser_take_screenshot (no filename) of every important state: the screenshots are attached to the PR as evidence. Stop the server afterwards.` : "4. Exercise the changed behaviour directly (run the program, call the API, write a quick script).",
|
|
3920
5343
|
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
3921
5344
|
"",
|
|
3922
5345
|
"Rules: do not modify the code under test, do not commit, push, comment on, review or merge anything on GitHub. The office posts your report on the pull request. Finish with the structured QA report: verdict, summary, the checks you performed, the commands you ran and one caption per screenshot."
|
|
@@ -4153,7 +5576,8 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
4153
5576
|
this.toast("info", `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`);
|
|
4154
5577
|
}
|
|
4155
5578
|
}
|
|
4156
|
-
|
|
5579
|
+
/** A message for an agent: sent into their running session, or a follow-up that resumes it. typed: the manager typed it at their CLI's prompt, where it's already running. */
|
|
5580
|
+
async message(id, text, typed = false) {
|
|
4157
5581
|
const a = this.agent(id);
|
|
4158
5582
|
if (a.role === "ceo") return this.messageCeo(text);
|
|
4159
5583
|
const repo = this.repo(a.repoId);
|
|
@@ -4161,7 +5585,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
4161
5585
|
const rt = this.agentRt.get(id);
|
|
4162
5586
|
if (rt.session) {
|
|
4163
5587
|
this.appendLog(a, [{ kind: "manager", text: `\u25B6 Manager: ${text}` }]);
|
|
4164
|
-
rt.session.send(text);
|
|
5588
|
+
if (!typed) rt.session.send(text);
|
|
4165
5589
|
return;
|
|
4166
5590
|
}
|
|
4167
5591
|
if (a.role === "qa") throw new HttpError(409, `${a.name} isn't testing anything right now. Send a PR to QA from the Kanban board.`);
|
|
@@ -4174,20 +5598,24 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
4174
5598
|
a.startedAt = Date.now();
|
|
4175
5599
|
if (a.task === null) a.task = "issue";
|
|
4176
5600
|
const fixing = a.task === "fix" && a.prNumber ? { pr: a.prNumber, headRef: a.branch } : void 0;
|
|
4177
|
-
this.startAgentSession(a, repo, cwd, text, this.buildSystemAppend(a, repo, cwd, a.branch, fixing), a.sessionId);
|
|
5601
|
+
this.startAgentSession(a, repo, cwd, text, this.buildSystemAppend(a, repo, cwd, a.branch, fixing), a.sessionId, void 0, typed ? "typed" : null);
|
|
4178
5602
|
}
|
|
4179
5603
|
updateSettings(patch) {
|
|
4180
5604
|
const s = this.state.settings;
|
|
4181
5605
|
if (patch.sessionLimit !== void 0) s.sessionLimit = Math.max(0, Math.round(Number(patch.sessionLimit)) || 0);
|
|
4182
|
-
if (patch.
|
|
5606
|
+
if (isCli(patch.defaultCli) && patch.defaultCli !== s.defaultCli) {
|
|
5607
|
+
s.defaultCli = patch.defaultCli;
|
|
5608
|
+
if (patch.defaultModel === void 0) s.defaultModel = s.defaultCli === "claude" ? DEFAULT_MODEL : "";
|
|
5609
|
+
}
|
|
5610
|
+
if (patch.defaultModel !== void 0) s.defaultModel = String(patch.defaultModel).trim() || (s.defaultCli === "claude" ? DEFAULT_MODEL : "");
|
|
4183
5611
|
if (patch.defaultEffort !== void 0 && EFFORTS.includes(patch.defaultEffort)) s.defaultEffort = patch.defaultEffort;
|
|
4184
|
-
if (patch.
|
|
5612
|
+
if (patch.runtime === "terminal" || patch.runtime === "sdk") s.runtime = patch.runtime;
|
|
4185
5613
|
if (patch.hiring === "approve" || patch.hiring === "auto") s.hiring = patch.hiring;
|
|
4186
5614
|
if (patch.teamCap !== void 0) s.teamCap = Math.max(1, Math.min(15, Math.round(Number(patch.teamCap)) || 1));
|
|
4187
5615
|
if (patch.ceoHeartbeatMin !== void 0) s.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
4188
5616
|
if (typeof patch.managerName === "string") s.managerName = patch.managerName.trim().slice(0, 40);
|
|
4189
5617
|
if (typeof patch.companyName === "string") s.companyName = patch.companyName.trim().slice(0, 60);
|
|
4190
|
-
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir =
|
|
5618
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir = path12.resolve(patch.projectsDir.trim());
|
|
4191
5619
|
if (typeof patch.setupDone === "boolean") s.setupDone = patch.setupDone;
|
|
4192
5620
|
if (patch.tutorialStep !== void 0) s.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
4193
5621
|
if (typeof patch.autoUpdate === "boolean") s.autoUpdate = patch.autoUpdate;
|
|
@@ -4526,6 +5954,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
4526
5954
|
skin: pick(SKIN),
|
|
4527
5955
|
model: CEO_MODEL,
|
|
4528
5956
|
effort: CEO_EFFORT,
|
|
5957
|
+
cli: "claude",
|
|
4529
5958
|
status: "idle",
|
|
4530
5959
|
issueNumber: null,
|
|
4531
5960
|
issueTitle: null,
|
|
@@ -4537,11 +5966,12 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
4537
5966
|
costUsd: 0,
|
|
4538
5967
|
turns: 0,
|
|
4539
5968
|
sessionId: null,
|
|
5969
|
+
sessionCli: null,
|
|
4540
5970
|
lastError: null,
|
|
4541
5971
|
logTail: []
|
|
4542
5972
|
};
|
|
4543
5973
|
this.state.agents.push(a);
|
|
4544
|
-
this.agentRt.set(a.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [] });
|
|
5974
|
+
this.agentRt.set(a.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [], terminal: null });
|
|
4545
5975
|
this.appendLog(a, [{ kind: "system", text: `\u{1F3DB}\uFE0F ${a.name} moved into the corner office. The CEO studies every floor, shapes its team and plans its work.` }]);
|
|
4546
5976
|
}
|
|
4547
5977
|
const i = interrupted.indexOf(a);
|
|
@@ -4631,13 +6061,14 @@ ${job.text}` } : job;
|
|
|
4631
6061
|
this.emitAgent(a);
|
|
4632
6062
|
this.emitCeo();
|
|
4633
6063
|
this.save();
|
|
4634
|
-
await
|
|
6064
|
+
await fs10.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
4635
6065
|
if (a.status !== "working") {
|
|
4636
6066
|
this.state.ceo.job = null;
|
|
4637
6067
|
this.emitCeo();
|
|
4638
6068
|
return;
|
|
4639
6069
|
}
|
|
4640
6070
|
const s = this.state.settings;
|
|
6071
|
+
const how = this.sessionRuntime(a, job.kind === "chat" ? a.sessionId ?? void 0 : void 0);
|
|
4641
6072
|
rt.session = this.backend.startSession(
|
|
4642
6073
|
{
|
|
4643
6074
|
cwd: CEO_DIR,
|
|
@@ -4646,7 +6077,7 @@ ${job.text}` } : job;
|
|
|
4646
6077
|
name: a.name,
|
|
4647
6078
|
company: s.companyName,
|
|
4648
6079
|
manager: s.managerName,
|
|
4649
|
-
notesFile:
|
|
6080
|
+
notesFile: path12.join(CEO_DIR, "NOTES.md"),
|
|
4650
6081
|
sessionLimit: s.sessionLimit,
|
|
4651
6082
|
teamCap: s.teamCap,
|
|
4652
6083
|
hiring: s.hiring
|
|
@@ -4654,12 +6085,10 @@ ${job.text}` } : job;
|
|
|
4654
6085
|
model: a.model || CEO_MODEL,
|
|
4655
6086
|
effort: a.effort || CEO_EFFORT,
|
|
4656
6087
|
browserTesting: false,
|
|
4657
|
-
permissionMode: s.permissionMode,
|
|
4658
6088
|
additionalDirectories: this.state.repos.filter((r) => this.repoRt.get(r.id)?.cloneStatus === "ready").map((r) => this.backend.mainDir(r.fullName)),
|
|
4659
6089
|
role: "ceo",
|
|
4660
6090
|
office: this.officeTools(),
|
|
4661
|
-
|
|
4662
|
-
resumeSessionId: job.kind === "chat" ? a.sessionId ?? void 0 : void 0
|
|
6091
|
+
...how
|
|
4663
6092
|
},
|
|
4664
6093
|
{
|
|
4665
6094
|
log: (entries) => this.appendLog(a, entries),
|
|
@@ -4670,6 +6099,7 @@ ${job.text}` } : job;
|
|
|
4670
6099
|
},
|
|
4671
6100
|
sessionId: (id) => {
|
|
4672
6101
|
a.sessionId = id;
|
|
6102
|
+
a.sessionCli = "claude";
|
|
4673
6103
|
},
|
|
4674
6104
|
browserUrl: () => void 0,
|
|
4675
6105
|
screenshot: () => void 0,
|
|
@@ -4677,8 +6107,7 @@ ${job.text}` } : job;
|
|
|
4677
6107
|
limited: (at) => this.pauseForLimit(at),
|
|
4678
6108
|
usageWarning: (info) => this.paceForWarning(info),
|
|
4679
6109
|
finished: (result) => this.onCeoFinished(a, result)
|
|
4680
|
-
}
|
|
4681
|
-
""
|
|
6110
|
+
}
|
|
4682
6111
|
);
|
|
4683
6112
|
}
|
|
4684
6113
|
onCeoFinished(a, result) {
|
|
@@ -4691,6 +6120,7 @@ ${job.text}` } : job;
|
|
|
4691
6120
|
a.turns += result.turns;
|
|
4692
6121
|
const job = this.state.ceo.job;
|
|
4693
6122
|
this.state.ceo.job = null;
|
|
6123
|
+
if (result.interrupted) this.interrupted(a);
|
|
4694
6124
|
if (a.status === "stopped") {
|
|
4695
6125
|
} else if (!result.ok) {
|
|
4696
6126
|
a.status = "error";
|
|
@@ -4754,7 +6184,7 @@ ${t}`);
|
|
|
4754
6184
|
qa: this.state.qa.map((q) => [q.repoId, q.prNumber, q.status]),
|
|
4755
6185
|
requests: this.state.requests.filter((r) => r.status === "pending").map((r) => r.id)
|
|
4756
6186
|
};
|
|
4757
|
-
return
|
|
6187
|
+
return crypto6.createHash("sha1").update(JSON.stringify(data)).digest("hex");
|
|
4758
6188
|
}
|
|
4759
6189
|
maybeHeartbeat() {
|
|
4760
6190
|
const min = this.state.settings.ceoHeartbeatMin;
|
|
@@ -4968,7 +6398,8 @@ ${t}`);
|
|
|
4968
6398
|
doing: this.agentDoing(a),
|
|
4969
6399
|
issue: a.issueNumber ? { number: a.issueNumber, title: a.issueTitle } : null,
|
|
4970
6400
|
pullRequest: a.prNumber ? { number: a.prNumber, url: a.prUrl } : null,
|
|
4971
|
-
|
|
6401
|
+
codingAgent: ceo ? "claude" : a.cli || this.state.settings.defaultCli,
|
|
6402
|
+
model: ceo ? a.model || CEO_MODEL : this.modelFor(a, a.cli || this.state.settings.defaultCli) || "the coding agent default",
|
|
4972
6403
|
effort: a.effort || (ceo ? CEO_EFFORT : this.state.settings.defaultEffort),
|
|
4973
6404
|
hiredBy: a.hiredBy,
|
|
4974
6405
|
jobDescription: a.brief || null
|
|
@@ -5011,7 +6442,7 @@ ${t}`);
|
|
|
5011
6442
|
if (seated + waiting >= MAX_DESKS[role]) throw new Error(role === "qa" ? `The QA lab on floor ${repo.floor} is full.` : `Floor ${repo.floor} has no free desks.`);
|
|
5012
6443
|
const name = this.freeName(role);
|
|
5013
6444
|
const req = {
|
|
5014
|
-
id:
|
|
6445
|
+
id: crypto6.randomUUID(),
|
|
5015
6446
|
kind: "hire",
|
|
5016
6447
|
repoId: repo.id,
|
|
5017
6448
|
role,
|
|
@@ -5053,7 +6484,7 @@ ${t}`);
|
|
|
5053
6484
|
if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
|
|
5054
6485
|
if (pending2.length >= MAX_PENDING_REQUESTS) throw new Error(`${pending2.length} proposals are already waiting for the manager. Wait for their decisions first.`);
|
|
5055
6486
|
const req = {
|
|
5056
|
-
id:
|
|
6487
|
+
id: crypto6.randomUUID(),
|
|
5057
6488
|
kind: "let-go",
|
|
5058
6489
|
repoId: repo.id,
|
|
5059
6490
|
role: a.role,
|
|
@@ -5127,8 +6558,12 @@ _Filed by ${this.ceo().name}, the cubefarm CEO._`;
|
|
|
5127
6558
|
|
|
5128
6559
|
// server/index.ts
|
|
5129
6560
|
var swarm = new Swarm(DEMO ? createDemoBackend() : realBackend);
|
|
6561
|
+
if (PORT) setOfficeUrl(`http://127.0.0.1:${PORT}`);
|
|
5130
6562
|
await swarm.init();
|
|
5131
6563
|
var app = express();
|
|
6564
|
+
app.post("/api/hooks/:token", express.json({ limit: "64mb" }), (req, res) => void res.json(handleHook(String(req.params.token), req.body)));
|
|
6565
|
+
app.post("/api/mcp/:token", express.json({ limit: "4mb" }), (req, res, next) => void handleMcp(String(req.params.token), req, res).catch(next));
|
|
6566
|
+
app.all("/api/mcp/:token", (_req, res) => void res.status(405).set("Allow", "POST").end());
|
|
5132
6567
|
app.use(express.json({ limit: "1mb" }));
|
|
5133
6568
|
var route = (fn) => async (req, res, next) => {
|
|
5134
6569
|
try {
|
|
@@ -5248,10 +6683,10 @@ app.post(
|
|
|
5248
6683
|
route((req) => swarm.approveRequest(String(req.params.id), { name: str(req.body?.name) || void 0, model: typeof req.body?.model === "string" ? req.body.model : void 0, effort: typeof req.body?.effort === "string" ? req.body.effort : void 0 }))
|
|
5249
6684
|
);
|
|
5250
6685
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
5251
|
-
var dist =
|
|
5252
|
-
if (
|
|
6686
|
+
var dist = path13.resolve(import.meta.dirname, "../dist");
|
|
6687
|
+
if (fs11.existsSync(dist)) {
|
|
5253
6688
|
app.use(express.static(dist));
|
|
5254
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
6689
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path13.join(dist, "index.html")));
|
|
5255
6690
|
}
|
|
5256
6691
|
app.use((err, _req, res, _next) => {
|
|
5257
6692
|
const parserStatus = err?.status;
|
|
@@ -5261,9 +6696,18 @@ app.use((err, _req, res, _next) => {
|
|
|
5261
6696
|
res.status(status).json({ error: message });
|
|
5262
6697
|
});
|
|
5263
6698
|
var server = http2.createServer(app);
|
|
5264
|
-
var wss = new WebSocketServer({
|
|
6699
|
+
var wss = new WebSocketServer({ noServer: true });
|
|
6700
|
+
var terms = new WebSocketServer({ noServer: true });
|
|
5265
6701
|
wss.on("connection", (ws) => swarm.addClient(ws));
|
|
6702
|
+
terms.on("connection", (ws, req) => swarm.attachTerminal(new URL(req.url ?? "", "http://localhost").searchParams.get("agent") ?? "", ws));
|
|
6703
|
+
server.on("upgrade", (req, socket, head) => {
|
|
6704
|
+
const { pathname } = new URL(req.url ?? "", "http://localhost");
|
|
6705
|
+
const target = pathname === "/ws" ? wss : pathname === "/ws/term" ? terms : null;
|
|
6706
|
+
if (!target) return void socket.destroy();
|
|
6707
|
+
target.handleUpgrade(req, socket, head, (ws) => target.emit("connection", ws, req));
|
|
6708
|
+
});
|
|
5266
6709
|
server.listen(PORT, "127.0.0.1", () => {
|
|
6710
|
+
setOfficeUrl(`http://127.0.0.1:${server.address().port}`);
|
|
5267
6711
|
console.log(`
|
|
5268
6712
|
\u{1F3E2} cubefarm on http://localhost:${PORT}${DEMO ? " (DEMO MODE: fake GitHub + fake agents)" : ""}`);
|
|
5269
6713
|
console.log(` state: ${STATE_FILE}`);
|
|
@@ -5277,16 +6721,18 @@ var shutdown = (signal) => {
|
|
|
5277
6721
|
console.log(`
|
|
5278
6722
|
${signal}: stopping floor previews\u2026`);
|
|
5279
6723
|
const force = setTimeout(() => process.exit(0), 15e3);
|
|
5280
|
-
void swarm.shutdown().catch((err) => console.error(err)).finally(() => {
|
|
6724
|
+
void swarm.shutdown(signal === "restart").catch((err) => console.error(err)).finally(() => {
|
|
5281
6725
|
clearTimeout(force);
|
|
5282
6726
|
process.exit(0);
|
|
5283
6727
|
});
|
|
5284
6728
|
};
|
|
5285
6729
|
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
5286
6730
|
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
|
6731
|
+
process.once("SIGHUP", () => shutdown("SIGHUP"));
|
|
5287
6732
|
if (underLauncher()) {
|
|
5288
6733
|
process.on("message", (msg) => {
|
|
5289
|
-
|
|
6734
|
+
const m = msg;
|
|
6735
|
+
if (m?.type === "office:shutdown") shutdown(m.restart === false ? "stop" : "restart");
|
|
5290
6736
|
});
|
|
5291
6737
|
}
|
|
5292
6738
|
//# sourceMappingURL=index.js.map
|