cubefarm 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cubefarm.js +9 -1
- package/dist/assets/ToyWorld-DQTMvcEl.js +2 -0
- package/dist/assets/{index-CvwxmawV.js → index-D3lYbosx.js} +30 -30
- package/dist/assets/{index-BU33ZUhQ.css → index-hi9vqi9h.css} +1 -1
- package/dist/assets/{rapier-DS9bMu-p.js → rapier-DfUh5clU.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +182 -131
- package/dist-server/index.js.map +4 -4
- package/package.json +3 -1
- package/dist/assets/ToyWorld-C1nzIgVK.js +0 -2
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 path14 from "node:path";
|
|
4
|
+
import fs12 from "node:fs";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
|
|
@@ -537,9 +537,46 @@ Write-Output $kill.Count`;
|
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
// server/agentRunner.ts
|
|
540
|
+
import fs4 from "node:fs";
|
|
541
|
+
import path4 from "node:path";
|
|
542
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
543
|
+
|
|
544
|
+
// server/browser.ts
|
|
540
545
|
import fs3 from "node:fs";
|
|
546
|
+
import { createRequire } from "node:module";
|
|
547
|
+
import os2 from "node:os";
|
|
541
548
|
import path3 from "node:path";
|
|
542
|
-
|
|
549
|
+
function playwrightMcpCli() {
|
|
550
|
+
try {
|
|
551
|
+
const pkgFile = createRequire(import.meta.url).resolve("@playwright/mcp/package.json");
|
|
552
|
+
const bin = JSON.parse(fs3.readFileSync(pkgFile, "utf8")).bin?.["playwright-mcp"] ?? "cli.js";
|
|
553
|
+
return path3.join(path3.dirname(pkgFile), bin);
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function playwrightMcpCommand(cli, initScript, outputDir, node = process.execPath) {
|
|
559
|
+
return { command: node, args: [cli, "--headless", "--isolated", ...outputDir ? ["--output-dir", outputDir] : [], "--init-script", initScript] };
|
|
560
|
+
}
|
|
561
|
+
function playwrightMcp(outputDir) {
|
|
562
|
+
const cli = playwrightMcpCli();
|
|
563
|
+
return cli ? playwrightMcpCommand(cli, path3.join(import.meta.dirname, "browser-init.js"), outputDir) : null;
|
|
564
|
+
}
|
|
565
|
+
function chromePaths(platform = process.platform, env = process.env, home = os2.homedir()) {
|
|
566
|
+
if (platform === "win32") {
|
|
567
|
+
return [env.LOCALAPPDATA, env.PROGRAMFILES, env["PROGRAMFILES(X86)"]].filter((d) => !!d).map((d) => path3.win32.join(d, "Google", "Chrome", "Application", "chrome.exe"));
|
|
568
|
+
}
|
|
569
|
+
if (platform === "darwin") return ["/Applications/Google Chrome.app", path3.posix.join(home, "Applications", "Google Chrome.app")];
|
|
570
|
+
return ["/opt/google/chrome/chrome", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"];
|
|
571
|
+
}
|
|
572
|
+
var hasChrome = () => chromePaths().some((p) => fs3.existsSync(p));
|
|
573
|
+
function browserProblem(mcp, chrome = hasChrome()) {
|
|
574
|
+
if (!mcp) return "\u26A0 The browser tool (@playwright/mcp) is not installed with cubefarm: reinstall it, or run npx cubefarm doctor";
|
|
575
|
+
if (!chrome) return "\u26A0 Google Chrome not found, so this session cannot test in a browser: install it, or run npx cubefarm doctor";
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// server/agentRunner.ts
|
|
543
580
|
var InputQueue = class {
|
|
544
581
|
items = [];
|
|
545
582
|
waiters = [];
|
|
@@ -571,7 +608,7 @@ function screenshotFile(cwd, text) {
|
|
|
571
608
|
const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
|
|
572
609
|
if (!file) return null;
|
|
573
610
|
try {
|
|
574
|
-
return { data:
|
|
611
|
+
return { data: fs4.readFileSync(path4.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
|
|
575
612
|
} catch {
|
|
576
613
|
return null;
|
|
577
614
|
}
|
|
@@ -580,17 +617,17 @@ var IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jp
|
|
|
580
617
|
function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
581
618
|
let names;
|
|
582
619
|
try {
|
|
583
|
-
names =
|
|
620
|
+
names = fs4.readdirSync(dir);
|
|
584
621
|
} catch {
|
|
585
622
|
return [];
|
|
586
623
|
}
|
|
587
624
|
const found2 = [];
|
|
588
625
|
for (const name of names.sort()) {
|
|
589
|
-
const mime = IMAGE_MIME[
|
|
590
|
-
const file =
|
|
626
|
+
const mime = IMAGE_MIME[path4.extname(name).toLowerCase()];
|
|
627
|
+
const file = path4.join(dir, name);
|
|
591
628
|
if (!mime || seen.has(name)) continue;
|
|
592
629
|
try {
|
|
593
|
-
if (now2 -
|
|
630
|
+
if (now2 - fs4.statSync(file).mtimeMs < 1e3) continue;
|
|
594
631
|
} catch {
|
|
595
632
|
continue;
|
|
596
633
|
}
|
|
@@ -602,7 +639,7 @@ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
|
602
639
|
function rel(cwd, p) {
|
|
603
640
|
const s = String(p ?? "");
|
|
604
641
|
if (!s) return "";
|
|
605
|
-
const r =
|
|
642
|
+
const r = path4.relative(cwd, path4.resolve(cwd, s));
|
|
606
643
|
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s;
|
|
607
644
|
}
|
|
608
645
|
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -729,8 +766,10 @@ function startSession(opts, callbacks) {
|
|
|
729
766
|
env.CLAUDE_AGENT_SDK_CLIENT_APP = `cubefarm/${VERSION}`;
|
|
730
767
|
const mcpServers = {};
|
|
731
768
|
if (opts.browserTesting) {
|
|
732
|
-
const
|
|
733
|
-
mcpServers.playwright =
|
|
769
|
+
const browser = playwrightMcp();
|
|
770
|
+
if (browser) mcpServers.playwright = browser;
|
|
771
|
+
const problem = browserProblem(browser);
|
|
772
|
+
if (problem) cb.log([{ kind: "error", text: problem }]);
|
|
734
773
|
}
|
|
735
774
|
if (opts.office) mcpServers.office = opts.office.server;
|
|
736
775
|
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"];
|
|
@@ -903,15 +942,15 @@ function startSession(opts, callbacks) {
|
|
|
903
942
|
|
|
904
943
|
// server/cliRunner.ts
|
|
905
944
|
import crypto3 from "node:crypto";
|
|
906
|
-
import
|
|
907
|
-
import
|
|
945
|
+
import fs7 from "node:fs";
|
|
946
|
+
import path8 from "node:path";
|
|
908
947
|
import { pathToFileURL } from "node:url";
|
|
909
948
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
910
949
|
|
|
911
950
|
// server/clis.ts
|
|
912
|
-
import
|
|
913
|
-
import { createRequire } from "node:module";
|
|
914
|
-
import
|
|
951
|
+
import fs5 from "node:fs";
|
|
952
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
953
|
+
import path5 from "node:path";
|
|
915
954
|
var CLIS = [
|
|
916
955
|
{ id: "claude", label: "Claude Code", command: "claude", integrated: true },
|
|
917
956
|
{ id: "codex", label: "Codex", command: "codex", integrated: false },
|
|
@@ -922,13 +961,13 @@ var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
|
|
|
922
961
|
var WIN = process.platform === "win32";
|
|
923
962
|
function resolveCommand(name, env = process.env) {
|
|
924
963
|
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
925
|
-
const dirs = (env[key] ?? "").split(
|
|
964
|
+
const dirs = (env[key] ?? "").split(path5.delimiter).filter(Boolean);
|
|
926
965
|
const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
|
|
927
966
|
for (const dir of dirs) {
|
|
928
967
|
for (const ext of exts) {
|
|
929
|
-
const file =
|
|
968
|
+
const file = path5.join(dir, name + ext.toLowerCase());
|
|
930
969
|
try {
|
|
931
|
-
const st =
|
|
970
|
+
const st = fs5.statSync(file);
|
|
932
971
|
if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
|
|
933
972
|
} catch {
|
|
934
973
|
}
|
|
@@ -939,16 +978,16 @@ function resolveCommand(name, env = process.env) {
|
|
|
939
978
|
function unwrapCmdShim(shimPath, text) {
|
|
940
979
|
const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
|
|
941
980
|
if (!line) return null;
|
|
942
|
-
const dir =
|
|
943
|
-
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) =>
|
|
981
|
+
const dir = path5.dirname(shimPath);
|
|
982
|
+
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path5.join(dir, m[1]));
|
|
944
983
|
const target = targets[targets.length - 1];
|
|
945
984
|
if (!target) return null;
|
|
946
985
|
if (/\.exe$/i.test(target)) return { file: target, args: [] };
|
|
947
986
|
if (!/\.[cm]?js$/i.test(target)) return null;
|
|
948
987
|
const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
|
|
949
988
|
const nodeFlags = line.includes('"%_prog%"') ? between.trim().split(/\s+/).filter((f) => f.startsWith("--")) : [];
|
|
950
|
-
const localNode =
|
|
951
|
-
return { file:
|
|
989
|
+
const localNode = path5.join(dir, "node.exe");
|
|
990
|
+
return { file: fs5.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
|
|
952
991
|
}
|
|
953
992
|
var bundled;
|
|
954
993
|
function bundledClaude() {
|
|
@@ -956,7 +995,7 @@ function bundledClaude() {
|
|
|
956
995
|
bundled = null;
|
|
957
996
|
let sdk;
|
|
958
997
|
try {
|
|
959
|
-
sdk =
|
|
998
|
+
sdk = createRequire2(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
960
999
|
} catch {
|
|
961
1000
|
return null;
|
|
962
1001
|
}
|
|
@@ -966,7 +1005,7 @@ function bundledClaude() {
|
|
|
966
1005
|
const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
967
1006
|
for (const target of targets) {
|
|
968
1007
|
try {
|
|
969
|
-
bundled =
|
|
1008
|
+
bundled = createRequire2(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
|
|
970
1009
|
return bundled;
|
|
971
1010
|
} catch {
|
|
972
1011
|
}
|
|
@@ -981,7 +1020,7 @@ function commandFor(id) {
|
|
|
981
1020
|
if (!found2) return null;
|
|
982
1021
|
if (WIN && /\.(cmd|bat)$/i.test(found2)) {
|
|
983
1022
|
try {
|
|
984
|
-
return unwrapCmdShim(found2,
|
|
1023
|
+
return unwrapCmdShim(found2, fs5.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
|
|
985
1024
|
} catch {
|
|
986
1025
|
return { file: found2, args: [] };
|
|
987
1026
|
}
|
|
@@ -1183,18 +1222,18 @@ export default async function CubefarmPlugin({ client } = {}) {
|
|
|
1183
1222
|
|
|
1184
1223
|
// server/ptyClient.ts
|
|
1185
1224
|
import crypto2 from "node:crypto";
|
|
1186
|
-
import
|
|
1225
|
+
import fs6 from "node:fs";
|
|
1187
1226
|
import net from "node:net";
|
|
1188
|
-
import
|
|
1227
|
+
import path7 from "node:path";
|
|
1189
1228
|
import { spawn } from "node:child_process";
|
|
1190
1229
|
|
|
1191
1230
|
// server/ptyProtocol.ts
|
|
1192
1231
|
import crypto from "node:crypto";
|
|
1193
|
-
import
|
|
1232
|
+
import path6 from "node:path";
|
|
1194
1233
|
var PTY_PROTOCOL = 1;
|
|
1195
1234
|
function hostSocket(home, platform = process.platform) {
|
|
1196
|
-
if (platform !== "win32") return
|
|
1197
|
-
const tag = crypto.createHash("sha1").update(
|
|
1235
|
+
if (platform !== "win32") return path6.join(home, "pty.sock");
|
|
1236
|
+
const tag = crypto.createHash("sha1").update(path6.resolve(home).toLowerCase()).digest("hex").slice(0, 12);
|
|
1198
1237
|
return `\\\\.\\pipe\\cubefarm-pty-${tag}`;
|
|
1199
1238
|
}
|
|
1200
1239
|
var encode = (m) => `${JSON.stringify(m)}
|
|
@@ -1325,7 +1364,7 @@ function connect(socket, secret) {
|
|
|
1325
1364
|
}
|
|
1326
1365
|
var runtimeFlags = () => process.execArgv.filter((a, i, all) => !/^--(inspect|watch|debug)/.test(a) && !/^--(inspect|watch|debug)/.test(all[i - 1] ?? ""));
|
|
1327
1366
|
function launch(socket, secretFile) {
|
|
1328
|
-
const entry =
|
|
1367
|
+
const entry = path7.join(import.meta.dirname, `ptyHost${path7.extname(import.meta.filename)}`);
|
|
1329
1368
|
spawn(process.execPath, [...runtimeFlags(), entry, "launch", socket, secretFile], { detached: true, stdio: "ignore", windowsHide: true }).once("error", () => void 0).unref();
|
|
1330
1369
|
}
|
|
1331
1370
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -1333,17 +1372,17 @@ async function startKeeper(home, onHook) {
|
|
|
1333
1372
|
hookHandler = onHook;
|
|
1334
1373
|
if (!nodePty) return [];
|
|
1335
1374
|
const socket = hostSocket(home);
|
|
1336
|
-
const secretFile =
|
|
1375
|
+
const secretFile = path7.join(home, "pty.secret");
|
|
1337
1376
|
try {
|
|
1338
|
-
if (!
|
|
1339
|
-
|
|
1340
|
-
|
|
1377
|
+
if (!fs6.existsSync(secretFile)) {
|
|
1378
|
+
fs6.mkdirSync(home, { recursive: true });
|
|
1379
|
+
fs6.writeFileSync(secretFile, crypto2.randomBytes(24).toString("hex"), { mode: 384 });
|
|
1341
1380
|
}
|
|
1342
1381
|
} catch (err) {
|
|
1343
1382
|
console.warn(` terminal keeper unavailable: ${err.message}`);
|
|
1344
1383
|
return [];
|
|
1345
1384
|
}
|
|
1346
|
-
const secret =
|
|
1385
|
+
const secret = fs6.readFileSync(secretFile, "utf8").trim();
|
|
1347
1386
|
let hello = await connect(socket, secret);
|
|
1348
1387
|
if (hello && hello.ready.version !== PTY_PROTOCOL) {
|
|
1349
1388
|
console.log(` replacing the terminal keeper (v${hello.ready.version}); the ${hello.ready.ptys.length} terminal(s) it held are stopped`);
|
|
@@ -1423,8 +1462,8 @@ function killTree(proc) {
|
|
|
1423
1462
|
}
|
|
1424
1463
|
|
|
1425
1464
|
// server/cliRunner.ts
|
|
1426
|
-
var SESSIONS_DIR =
|
|
1427
|
-
var BIN_DIR =
|
|
1465
|
+
var SESSIONS_DIR = path8.join(HOME_DIR, "sessions");
|
|
1466
|
+
var BIN_DIR = path8.join(HOME_DIR, "bin");
|
|
1428
1467
|
var FINISH_GRACE_MS = 3e3;
|
|
1429
1468
|
var BOOT_MS = 6e4;
|
|
1430
1469
|
var USAGE_WARN_PCT = 90;
|
|
@@ -1462,19 +1501,15 @@ async function handleMcp(token, req, res) {
|
|
|
1462
1501
|
var helpersWritten = false;
|
|
1463
1502
|
function writeHelpers() {
|
|
1464
1503
|
if (helpersWritten) return;
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1504
|
+
fs7.mkdirSync(BIN_DIR, { recursive: true });
|
|
1505
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "statusline.cjs"), STATUSLINE_SOURCE);
|
|
1506
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "notify.cjs"), NOTIFY_SOURCE);
|
|
1507
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "codex-hook.cjs"), CODEX_HOOK_SOURCE);
|
|
1508
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "opencode-plugin.mjs"), OPENCODE_PLUGIN_SOURCE);
|
|
1470
1509
|
helpersWritten = true;
|
|
1471
1510
|
}
|
|
1472
1511
|
var shellArg = (p) => `"${p.replaceAll("\\", "/")}"`;
|
|
1473
1512
|
var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop", "StopFailure", "Notification"];
|
|
1474
|
-
function playwrightServer(outputDir) {
|
|
1475
|
-
const args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--output-dir", outputDir, "--init-script", path7.join(import.meta.dirname, "browser-init.js")];
|
|
1476
|
-
return process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
|
|
1477
|
-
}
|
|
1478
1513
|
function toolOutput(response) {
|
|
1479
1514
|
const images = [];
|
|
1480
1515
|
const blocks = Array.isArray(response) ? response : Array.isArray(response?.content) ? response.content : null;
|
|
@@ -1521,7 +1556,7 @@ function endLive(live) {
|
|
|
1521
1556
|
live.proc?.kill();
|
|
1522
1557
|
live.proc = null;
|
|
1523
1558
|
live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
|
|
1524
|
-
|
|
1559
|
+
fs7.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
|
|
1525
1560
|
return live.cli === "codex" && live.resumeId ? codexThread("archive", live.resumeId, gone) : Promise.resolve();
|
|
1526
1561
|
}
|
|
1527
1562
|
var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
|
|
@@ -1605,7 +1640,7 @@ function startCliSession(opts, callbacks) {
|
|
|
1605
1640
|
const label = cliLabel(cli);
|
|
1606
1641
|
const token = crypto3.randomUUID();
|
|
1607
1642
|
const sessionId = opts.resumeSessionId ?? crypto3.randomUUID();
|
|
1608
|
-
const dir =
|
|
1643
|
+
const dir = path8.join(SESSIONS_DIR, token);
|
|
1609
1644
|
const started = Date.now();
|
|
1610
1645
|
const statusLine = `\u{1F3E2} cubefarm${opts.label ? ` \xB7 ${opts.label}` : ""}`;
|
|
1611
1646
|
const toolNames = /* @__PURE__ */ new Map();
|
|
@@ -1633,10 +1668,10 @@ function startCliSession(opts, callbacks) {
|
|
|
1633
1668
|
const collectShots = (all = false) => {
|
|
1634
1669
|
const l2 = live;
|
|
1635
1670
|
if (!l2 || cli === "claude" || !opts.browserTesting) return;
|
|
1636
|
-
for (const shot of newScreenshots(
|
|
1671
|
+
for (const shot of newScreenshots(path8.join(l2.dir, "browser"), l2.shots, all ? Infinity : Date.now())) {
|
|
1637
1672
|
l2.shots.add(shot.name);
|
|
1638
1673
|
try {
|
|
1639
|
-
cb.screenshot(
|
|
1674
|
+
cb.screenshot(fs7.readFileSync(shot.file), shot.mime);
|
|
1640
1675
|
} catch {
|
|
1641
1676
|
}
|
|
1642
1677
|
}
|
|
@@ -1707,7 +1742,7 @@ function startCliSession(opts, callbacks) {
|
|
|
1707
1742
|
const event = String(b.hook_event_name ?? "");
|
|
1708
1743
|
const sub = typeof b.agent_id === "string" && b.agent_id !== "";
|
|
1709
1744
|
if (cli === "claude" && !sub && b.session_id !== kept) {
|
|
1710
|
-
const id = resumableSession(b,
|
|
1745
|
+
const id = resumableSession(b, fs7.existsSync);
|
|
1711
1746
|
if (id) {
|
|
1712
1747
|
kept = id;
|
|
1713
1748
|
cb.sessionId(id);
|
|
@@ -1859,7 +1894,9 @@ function startCliSession(opts, callbacks) {
|
|
|
1859
1894
|
}
|
|
1860
1895
|
const keeper = keeperHookUrl();
|
|
1861
1896
|
const hookUrl = `${keeper ?? officeUrl}/api/hooks/${token}`;
|
|
1862
|
-
const browser = opts.browserTesting ?
|
|
1897
|
+
const browser = opts.browserTesting ? playwrightMcp(path8.join(dir, "browser")) : null;
|
|
1898
|
+
const browserIssue = opts.browserTesting ? browserProblem(browser) : null;
|
|
1899
|
+
if (browserIssue) log([{ kind: "error", text: browserIssue }]);
|
|
1863
1900
|
let prompt = opts.prompt;
|
|
1864
1901
|
if (opts.outputSchema) {
|
|
1865
1902
|
prompt += `
|
|
@@ -1870,21 +1907,21 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1870
1907
|
let files;
|
|
1871
1908
|
try {
|
|
1872
1909
|
writeHelpers();
|
|
1873
|
-
|
|
1910
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1874
1911
|
const settings = {
|
|
1875
1912
|
hooks: Object.fromEntries(HOOK_EVENTS.map((e) => [e, [{ hooks: [{ type: "http", url: hookUrl, timeout: keeper ? 120 : 30 }] }]])),
|
|
1876
|
-
statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(
|
|
1913
|
+
statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(path8.join(BIN_DIR, "statusline.cjs"))} ${shellArg(hookUrl)}`, padding: 0 }
|
|
1877
1914
|
};
|
|
1878
1915
|
const servers = {};
|
|
1879
1916
|
if (browser) servers.playwright = browser;
|
|
1880
1917
|
if (opts.office) servers.office = { type: "http", url: `${officeUrl}/api/mcp/${token}` };
|
|
1881
|
-
files = { settings:
|
|
1882
|
-
|
|
1883
|
-
if (files.mcp)
|
|
1884
|
-
|
|
1918
|
+
files = { settings: path8.join(dir, "settings.json"), mcp: Object.keys(servers).length ? path8.join(dir, "mcp.json") : null, system: path8.join(dir, "instructions.md") };
|
|
1919
|
+
fs7.writeFileSync(files.settings, JSON.stringify(settings, null, 2));
|
|
1920
|
+
if (files.mcp) fs7.writeFileSync(files.mcp, JSON.stringify({ mcpServers: servers }, null, 2));
|
|
1921
|
+
fs7.writeFileSync(files.system, opts.systemAppend);
|
|
1885
1922
|
if (prompt.length > INLINE_PROMPT_MAX) {
|
|
1886
|
-
const file =
|
|
1887
|
-
|
|
1923
|
+
const file = path8.join(dir, "task.md");
|
|
1924
|
+
fs7.writeFileSync(file, prompt);
|
|
1888
1925
|
prompt = `Your task is in ${file}. Read all of it first, then do what it says.`;
|
|
1889
1926
|
}
|
|
1890
1927
|
} catch (err) {
|
|
@@ -1904,9 +1941,9 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1904
1941
|
role: opts.role,
|
|
1905
1942
|
additionalDirectories: opts.additionalDirectories,
|
|
1906
1943
|
files,
|
|
1907
|
-
notify: { script:
|
|
1908
|
-
codexHook:
|
|
1909
|
-
plugin: pathToFileURL(
|
|
1944
|
+
notify: { script: path8.join(BIN_DIR, "notify.cjs"), url: hookUrl },
|
|
1945
|
+
codexHook: path8.join(BIN_DIR, "codex-hook.cjs"),
|
|
1946
|
+
plugin: pathToFileURL(path8.join(BIN_DIR, "opencode-plugin.mjs")).href,
|
|
1910
1947
|
browser
|
|
1911
1948
|
});
|
|
1912
1949
|
const env = {};
|
|
@@ -1972,9 +2009,9 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1972
2009
|
// server/previewRunner.ts
|
|
1973
2010
|
import { spawn as spawn2 } from "node:child_process";
|
|
1974
2011
|
import crypto4 from "node:crypto";
|
|
1975
|
-
import
|
|
2012
|
+
import fs8 from "node:fs/promises";
|
|
1976
2013
|
import net2 from "node:net";
|
|
1977
|
-
import
|
|
2014
|
+
import path9 from "node:path";
|
|
1978
2015
|
var PREVIEW_SLUG = "preview";
|
|
1979
2016
|
var PREVIEW_BRANCH = "swarm-preview";
|
|
1980
2017
|
var PREVIEW_TMP = ".preview-tmp";
|
|
@@ -2010,7 +2047,7 @@ async function portOpen(port) {
|
|
|
2010
2047
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2011
2048
|
async function readJson(file) {
|
|
2012
2049
|
try {
|
|
2013
|
-
return JSON.parse(await
|
|
2050
|
+
return JSON.parse(await fs8.readFile(file, "utf8"));
|
|
2014
2051
|
} catch {
|
|
2015
2052
|
return null;
|
|
2016
2053
|
}
|
|
@@ -2071,9 +2108,9 @@ var RealPreview = class {
|
|
|
2071
2108
|
this.check();
|
|
2072
2109
|
await ensureLocalExcludes(job.fullName).catch(() => void 0);
|
|
2073
2110
|
cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
|
|
2074
|
-
const tmp =
|
|
2075
|
-
await
|
|
2076
|
-
const pkg = await readJson(
|
|
2111
|
+
const tmp = path9.join(wt, PREVIEW_TMP);
|
|
2112
|
+
await fs8.mkdir(tmp, { recursive: true });
|
|
2113
|
+
const pkg = await readJson(path9.join(wt, "package.json"));
|
|
2077
2114
|
const command = job.command ?? defaultCommand(pkg);
|
|
2078
2115
|
if (!command) {
|
|
2079
2116
|
this.stopped = true;
|
|
@@ -2112,11 +2149,11 @@ var RealPreview = class {
|
|
|
2112
2149
|
}
|
|
2113
2150
|
/** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
|
|
2114
2151
|
async install(wt, env) {
|
|
2115
|
-
const lockFile =
|
|
2116
|
-
const lock = await
|
|
2117
|
-
const hash = crypto4.createHash("sha256").update(await
|
|
2118
|
-
const marker =
|
|
2119
|
-
if (await
|
|
2152
|
+
const lockFile = path9.join(wt, "package-lock.json");
|
|
2153
|
+
const lock = await fs8.readFile(lockFile, "utf8").catch(() => null);
|
|
2154
|
+
const hash = crypto4.createHash("sha256").update(await fs8.readFile(path9.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
|
|
2155
|
+
const marker = path9.join(wt, "node_modules", INSTALL_MARKER);
|
|
2156
|
+
if (await fs8.readFile(marker, "utf8").catch(() => "") === hash) {
|
|
2120
2157
|
this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2121
2158
|
return;
|
|
2122
2159
|
}
|
|
@@ -2130,7 +2167,7 @@ var RealPreview = class {
|
|
|
2130
2167
|
}
|
|
2131
2168
|
if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
|
|
2132
2169
|
this.child = null;
|
|
2133
|
-
await
|
|
2170
|
+
await fs8.writeFile(marker, hash).catch(() => void 0);
|
|
2134
2171
|
}
|
|
2135
2172
|
/** Run a command line through the platform shell; resolves with its exit code. */
|
|
2136
2173
|
spawn(cmd, cwd, env) {
|
|
@@ -2161,12 +2198,12 @@ var RealPreview = class {
|
|
|
2161
2198
|
};
|
|
2162
2199
|
var realPreviews = {
|
|
2163
2200
|
start: (job, cb) => new RealPreview(job, cb),
|
|
2164
|
-
hasDefault: async (fullName) =>
|
|
2201
|
+
hasDefault: async (fullName) => fs8.access(path9.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
2165
2202
|
};
|
|
2166
2203
|
|
|
2167
2204
|
// server/officeUpdate.ts
|
|
2168
|
-
import
|
|
2169
|
-
import
|
|
2205
|
+
import fs9 from "node:fs/promises";
|
|
2206
|
+
import path10 from "node:path";
|
|
2170
2207
|
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
2171
2208
|
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
2172
2209
|
function underLauncher(proc = process) {
|
|
@@ -2203,10 +2240,10 @@ function parseLastUpdate(raw) {
|
|
|
2203
2240
|
}
|
|
2204
2241
|
}
|
|
2205
2242
|
async function takeLastUpdate(home) {
|
|
2206
|
-
const file =
|
|
2207
|
-
const raw = await
|
|
2243
|
+
const file = path10.join(home, "last-update.json");
|
|
2244
|
+
const raw = await fs9.readFile(file, "utf8").catch(() => null);
|
|
2208
2245
|
if (raw === null) return null;
|
|
2209
|
-
await
|
|
2246
|
+
await fs9.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
2210
2247
|
return parseLastUpdate(raw);
|
|
2211
2248
|
}
|
|
2212
2249
|
function lastUpdateMessage(u, commits) {
|
|
@@ -2214,8 +2251,8 @@ function lastUpdateMessage(u, commits) {
|
|
|
2214
2251
|
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
2215
2252
|
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
2216
2253
|
}
|
|
2217
|
-
var OFFICE_DIR =
|
|
2218
|
-
var isSamePath = (a, b) => process.platform === "win32" ?
|
|
2254
|
+
var OFFICE_DIR = path10.resolve(import.meta.dirname, "..");
|
|
2255
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path10.resolve(a).toLowerCase() === path10.resolve(b).toLowerCase() : path10.resolve(a) === path10.resolve(b);
|
|
2219
2256
|
var realOffice = {
|
|
2220
2257
|
launcher: underLauncher(),
|
|
2221
2258
|
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
@@ -2273,7 +2310,7 @@ var realBackend = {
|
|
|
2273
2310
|
// server/demo.ts
|
|
2274
2311
|
import crypto5 from "node:crypto";
|
|
2275
2312
|
import http from "node:http";
|
|
2276
|
-
import
|
|
2313
|
+
import path11 from "node:path";
|
|
2277
2314
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
2278
2315
|
function issue(n, title, body, fullName, labels = []) {
|
|
2279
2316
|
return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
@@ -2326,7 +2363,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
2326
2363
|
</svg>`;
|
|
2327
2364
|
}
|
|
2328
2365
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
2329
|
-
const branch =
|
|
2366
|
+
const branch = path11.basename(opts.cwd);
|
|
2330
2367
|
const hue = issueNumber * 67 % 360;
|
|
2331
2368
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
2332
2369
|
const port = 5200 + issueNumber % 50;
|
|
@@ -2465,7 +2502,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2465
2502
|
number: n,
|
|
2466
2503
|
title,
|
|
2467
2504
|
url,
|
|
2468
|
-
headRefName: `swarm/issue-${number}-${
|
|
2505
|
+
headRefName: `swarm/issue-${number}-${path11.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
2469
2506
|
state: "OPEN",
|
|
2470
2507
|
isDraft: false,
|
|
2471
2508
|
mergeable: "MERGEABLE",
|
|
@@ -2912,7 +2949,8 @@ function ceoSession(opts, cb) {
|
|
|
2912
2949
|
const s = JSON.parse(await office.call("company_status", {}));
|
|
2913
2950
|
const people = s.floors.reduce((n, f) => n + f.team.length, 0);
|
|
2914
2951
|
const issues = s.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
2915
|
-
|
|
2952
|
+
const free = s.floors.reduce((n, f) => n + f.seats.dev.free + f.seats.qa.free, 0);
|
|
2953
|
+
cb.log([{ kind: "result", text: ` \u23BF ${s.floors.length} floors \xB7 ${people} people \xB7 ${free} free seats \xB7 ${issues} open issues \xB7 ${s.pendingProposals.length} proposals pending` }]);
|
|
2916
2954
|
return s;
|
|
2917
2955
|
};
|
|
2918
2956
|
const use = async (name, args) => {
|
|
@@ -3103,8 +3141,8 @@ Acceptance criteria:
|
|
|
3103
3141
|
}
|
|
3104
3142
|
|
|
3105
3143
|
// server/swarm.ts
|
|
3106
|
-
import
|
|
3107
|
-
import
|
|
3144
|
+
import fs11 from "node:fs/promises";
|
|
3145
|
+
import path13 from "node:path";
|
|
3108
3146
|
import crypto6 from "node:crypto";
|
|
3109
3147
|
|
|
3110
3148
|
// server/ceo.ts
|
|
@@ -3189,7 +3227,7 @@ function createOfficeTools(h) {
|
|
|
3189
3227
|
const defs = [
|
|
3190
3228
|
tool(
|
|
3191
3229
|
"company_status",
|
|
3192
|
-
"Everything about the company right now: settings, every floor (repo, clone path, brief, profile, QA brief), its team, backlog, pull requests and QA, pending proposals and recent decisions by the manager. Call this first.",
|
|
3230
|
+
"Everything about the company right now: settings, every floor (repo, clone path, brief, profile, QA brief), its free seats, team, backlog, pull requests and QA, pending proposals and recent decisions by the manager. Call this first.",
|
|
3193
3231
|
{},
|
|
3194
3232
|
() => run2(() => h.companyStatus())
|
|
3195
3233
|
),
|
|
@@ -3281,6 +3319,12 @@ function createOfficeTools(h) {
|
|
|
3281
3319
|
}
|
|
3282
3320
|
};
|
|
3283
3321
|
}
|
|
3322
|
+
var FLOOR_DESKS = { dev: 12, qa: 3 };
|
|
3323
|
+
var MAX_PENDING_PROPOSALS = FLOOR_DESKS.dev + FLOOR_DESKS.qa;
|
|
3324
|
+
var seatCount = (total, taken, proposed) => ({ total, taken, proposed, free: Math.max(0, total - taken - proposed) });
|
|
3325
|
+
function checkPendingLimit(pending2, max = MAX_PENDING_PROPOSALS) {
|
|
3326
|
+
if (pending2 >= max) throw new Error(`${pending2} proposals are already waiting for the manager; propose the rest after they decide.`);
|
|
3327
|
+
}
|
|
3284
3328
|
function ceoSystemPrompt(o) {
|
|
3285
3329
|
const manager = o.manager ? `the manager, ${o.manager}` : "the human manager";
|
|
3286
3330
|
return [
|
|
@@ -3289,7 +3333,7 @@ function ceoSystemPrompt(o) {
|
|
|
3289
3333
|
"",
|
|
3290
3334
|
"Your job is to run the company, not to write code:",
|
|
3291
3335
|
"- 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.",
|
|
3292
|
-
`- 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.`,
|
|
3336
|
+
`- 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. When the manager asks for a bigger team, follow that, up to the floor's free seats (seats in company_status). Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
|
|
3293
3337
|
"- 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.",
|
|
3294
3338
|
"- 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).",
|
|
3295
3339
|
"",
|
|
@@ -3771,8 +3815,8 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
3771
3815
|
}
|
|
3772
3816
|
|
|
3773
3817
|
// server/terminal.ts
|
|
3774
|
-
import
|
|
3775
|
-
import
|
|
3818
|
+
import fs10 from "node:fs/promises";
|
|
3819
|
+
import path12 from "node:path";
|
|
3776
3820
|
import xtermHeadless from "@xterm/headless";
|
|
3777
3821
|
import xtermSerialize from "@xterm/addon-serialize";
|
|
3778
3822
|
var { Terminal } = xtermHeadless;
|
|
@@ -3866,12 +3910,12 @@ var AgentTerminal = class {
|
|
|
3866
3910
|
async save(file) {
|
|
3867
3911
|
this.dirty = false;
|
|
3868
3912
|
await this.flush();
|
|
3869
|
-
await
|
|
3870
|
-
await
|
|
3871
|
-
await
|
|
3913
|
+
await fs10.mkdir(path12.dirname(file), { recursive: true });
|
|
3914
|
+
await fs10.writeFile(`${file}.tmp`, this.snapshot());
|
|
3915
|
+
await fs10.rename(`${file}.tmp`, file);
|
|
3872
3916
|
}
|
|
3873
3917
|
async load(file) {
|
|
3874
|
-
const data = await
|
|
3918
|
+
const data = await fs10.readFile(file, "utf8").catch(() => "");
|
|
3875
3919
|
if (!data) return;
|
|
3876
3920
|
this.term.write(data);
|
|
3877
3921
|
this.note("(the office restarted)");
|
|
@@ -4001,16 +4045,15 @@ var FEMININE_NAMES = new Set(
|
|
|
4001
4045
|
);
|
|
4002
4046
|
var lookFor = (name) => FEMININE_NAMES.has(name.trim().split(/\s+/)[0].toLowerCase()) ? "feminine" : "masculine";
|
|
4003
4047
|
var LOOKS = ["feminine", "masculine"];
|
|
4004
|
-
var MAX_DESKS = {
|
|
4048
|
+
var MAX_DESKS = { ...FLOOR_DESKS, ceo: 1 };
|
|
4005
4049
|
var MAX_QA_ROUNDS = 3;
|
|
4006
4050
|
var DEFAULT_MODEL = "claude-opus-5-5";
|
|
4007
4051
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
4008
4052
|
var CEO_MODEL = "claude-opus-5-5";
|
|
4009
4053
|
var CEO_EFFORT = "xhigh";
|
|
4010
4054
|
var CEO_NAME = "Morgan";
|
|
4011
|
-
var CEO_DIR =
|
|
4012
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
4013
|
-
var MAX_PENDING_REQUESTS = 8;
|
|
4055
|
+
var CEO_DIR = path13.join(HOME_DIR, "ceo");
|
|
4056
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path13.resolve(import.meta.dirname, ".."));
|
|
4014
4057
|
var MAX_ISSUES_PER_JOB = 12;
|
|
4015
4058
|
var KEEP_MESSAGES = 200;
|
|
4016
4059
|
var KEEP_DECIDED_REQUESTS = 40;
|
|
@@ -4022,14 +4065,14 @@ var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
|
4022
4065
|
var MAX_ISSUE_FAILURES = 2;
|
|
4023
4066
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
4024
4067
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
4025
|
-
var SCREENS_DIR =
|
|
4068
|
+
var SCREENS_DIR = path13.join(HOME_DIR, "screens");
|
|
4026
4069
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
4027
|
-
var screenFile = (agentId, mime) =>
|
|
4070
|
+
var screenFile = (agentId, mime) => path13.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
4028
4071
|
async function loadScreen(agentId) {
|
|
4029
4072
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
4030
|
-
const file =
|
|
4073
|
+
const file = path13.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
4031
4074
|
try {
|
|
4032
|
-
const [data, stat] = await Promise.all([
|
|
4075
|
+
const [data, stat] = await Promise.all([fs11.readFile(file), fs11.stat(file)]);
|
|
4033
4076
|
return { data, mime, at: stat.mtimeMs };
|
|
4034
4077
|
} catch {
|
|
4035
4078
|
}
|
|
@@ -4037,10 +4080,10 @@ async function loadScreen(agentId) {
|
|
|
4037
4080
|
return null;
|
|
4038
4081
|
}
|
|
4039
4082
|
async function removeScreens(agentId) {
|
|
4040
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
4083
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs11.rm(path13.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
4041
4084
|
}
|
|
4042
|
-
var TERMINALS_DIR =
|
|
4043
|
-
var terminalFile = (agentId) =>
|
|
4085
|
+
var TERMINALS_DIR = path13.join(HOME_DIR, "terminals");
|
|
4086
|
+
var terminalFile = (agentId) => path13.join(TERMINALS_DIR, `${agentId}.ansi`);
|
|
4044
4087
|
var TERMINAL_SAVE_MS = 2e4;
|
|
4045
4088
|
var QA_SCHEMA = {
|
|
4046
4089
|
type: "object",
|
|
@@ -4194,7 +4237,7 @@ var Swarm = class {
|
|
|
4194
4237
|
// ---------- lifecycle ----------
|
|
4195
4238
|
async init() {
|
|
4196
4239
|
try {
|
|
4197
|
-
const raw = await
|
|
4240
|
+
const raw = await fs11.readFile(STATE_FILE, "utf8");
|
|
4198
4241
|
const loaded = JSON.parse(raw);
|
|
4199
4242
|
this.state = {
|
|
4200
4243
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -4515,10 +4558,10 @@ var Swarm = class {
|
|
|
4515
4558
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
4516
4559
|
this.saveTimer = null;
|
|
4517
4560
|
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
4518
|
-
await
|
|
4561
|
+
await fs11.mkdir(path13.dirname(STATE_FILE), { recursive: true });
|
|
4519
4562
|
const tmp = `${STATE_FILE}.tmp`;
|
|
4520
|
-
await
|
|
4521
|
-
await
|
|
4563
|
+
await fs11.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
4564
|
+
await fs11.rename(tmp, STATE_FILE);
|
|
4522
4565
|
}
|
|
4523
4566
|
// ---------- lookups ----------
|
|
4524
4567
|
repo(id) {
|
|
@@ -4600,7 +4643,7 @@ var Swarm = class {
|
|
|
4600
4643
|
const name = fullName.split("/")[1];
|
|
4601
4644
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
4602
4645
|
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.`);
|
|
4603
|
-
return
|
|
4646
|
+
return path13.join(root, name);
|
|
4604
4647
|
}
|
|
4605
4648
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
4606
4649
|
async createRepo(name, opts) {
|
|
@@ -4612,11 +4655,11 @@ var Swarm = class {
|
|
|
4612
4655
|
}
|
|
4613
4656
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
4614
4657
|
async listProjectFolders(dir) {
|
|
4615
|
-
const root = dir?.trim() ?
|
|
4658
|
+
const root = dir?.trim() ? path13.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
4616
4659
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
4617
4660
|
throw new HttpError(400, err.message);
|
|
4618
4661
|
});
|
|
4619
|
-
const same = (a, b) =>
|
|
4662
|
+
const same = (a, b) => path13.resolve(a).toLowerCase() === path13.resolve(b).toLowerCase();
|
|
4620
4663
|
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;
|
|
4621
4664
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
4622
4665
|
}
|
|
@@ -4796,7 +4839,7 @@ var Swarm = class {
|
|
|
4796
4839
|
rt.folderSync = status;
|
|
4797
4840
|
this.emitRepo(repo);
|
|
4798
4841
|
if (status && status !== before) {
|
|
4799
|
-
const name = repo.localPath ?
|
|
4842
|
+
const name = repo.localPath ? path13.basename(repo.localPath) : repo.fullName;
|
|
4800
4843
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
4801
4844
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
4802
4845
|
}
|
|
@@ -5020,7 +5063,7 @@ var Swarm = class {
|
|
|
5020
5063
|
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
5021
5064
|
this.agentRt.get(id)?.terminal?.dispose();
|
|
5022
5065
|
void removeScreens(id);
|
|
5023
|
-
void
|
|
5066
|
+
void fs11.rm(terminalFile(id), { force: true }).catch(() => void 0);
|
|
5024
5067
|
for (const q of this.state.qa) {
|
|
5025
5068
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
5026
5069
|
if (q.devAgentId === id) q.devAgentId = null;
|
|
@@ -5123,7 +5166,7 @@ var Swarm = class {
|
|
|
5123
5166
|
}
|
|
5124
5167
|
// ---------- terminals ----------
|
|
5125
5168
|
async loadTerminal(agentId) {
|
|
5126
|
-
if (!await
|
|
5169
|
+
if (!await fs11.stat(terminalFile(agentId)).catch(() => null)) return null;
|
|
5127
5170
|
const t = this.newTerminal(agentId);
|
|
5128
5171
|
await t.load(terminalFile(agentId));
|
|
5129
5172
|
return t;
|
|
@@ -5289,7 +5332,7 @@ Note from the manager: ${note}` : ""
|
|
|
5289
5332
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
5290
5333
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
5291
5334
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
5292
|
-
void removeScreens(a.id).then(() =>
|
|
5335
|
+
void removeScreens(a.id).then(() => fs11.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs11.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
|
|
5293
5336
|
},
|
|
5294
5337
|
limited: (at) => this.pauseForLimit(at),
|
|
5295
5338
|
usageWarning: (info) => this.paceForWarning(info),
|
|
@@ -5745,7 +5788,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5745
5788
|
if (patch.ceoHeartbeatMin !== void 0) s.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
5746
5789
|
if (typeof patch.managerName === "string") s.managerName = patch.managerName.trim().slice(0, 40);
|
|
5747
5790
|
if (typeof patch.companyName === "string") s.companyName = patch.companyName.trim().slice(0, 60);
|
|
5748
|
-
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir =
|
|
5791
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir = path13.resolve(patch.projectsDir.trim());
|
|
5749
5792
|
if (typeof patch.setupDone === "boolean") s.setupDone = patch.setupDone;
|
|
5750
5793
|
if (patch.tutorialStep !== void 0) s.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
5751
5794
|
if (typeof patch.autoUpdate === "boolean") s.autoUpdate = patch.autoUpdate;
|
|
@@ -6192,7 +6235,7 @@ ${job.text}` } : job;
|
|
|
6192
6235
|
this.emitAgent(a);
|
|
6193
6236
|
this.emitCeo();
|
|
6194
6237
|
this.save();
|
|
6195
|
-
await
|
|
6238
|
+
await fs11.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
6196
6239
|
if (a.status !== "working") {
|
|
6197
6240
|
this.state.ceo.job = null;
|
|
6198
6241
|
this.emitCeo();
|
|
@@ -6208,7 +6251,7 @@ ${job.text}` } : job;
|
|
|
6208
6251
|
name: a.name,
|
|
6209
6252
|
company: s.companyName,
|
|
6210
6253
|
manager: s.managerName,
|
|
6211
|
-
notesFile:
|
|
6254
|
+
notesFile: path13.join(CEO_DIR, "NOTES.md"),
|
|
6212
6255
|
sessionLimit: s.sessionLimit,
|
|
6213
6256
|
teamCap: s.teamCap,
|
|
6214
6257
|
hiring: s.hiring
|
|
@@ -6435,6 +6478,12 @@ ${t}`);
|
|
|
6435
6478
|
const mark = `\u2026 (truncated, ${brief.length} chars; see agent_detail)`;
|
|
6436
6479
|
return brief.slice(0, 400 - mark.length).trimEnd() + mark;
|
|
6437
6480
|
};
|
|
6481
|
+
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6482
|
+
const seats = (r, role) => seatCount(
|
|
6483
|
+
MAX_DESKS[role],
|
|
6484
|
+
this.state.agents.filter((a) => a.repoId === r.id && a.role === role).length,
|
|
6485
|
+
pending2.filter((p) => p.kind === "hire" && p.repoId === r.id && p.role === role).length
|
|
6486
|
+
);
|
|
6438
6487
|
const floors = [...this.state.repos].sort((x, y) => x.floor - y.floor).map((r) => {
|
|
6439
6488
|
const rt = this.repoRt.get(r.id);
|
|
6440
6489
|
const open = new Set(rt.issues.map((i) => i.number));
|
|
@@ -6450,6 +6499,7 @@ ${t}`);
|
|
|
6450
6499
|
autoAssign: r.autoAssign,
|
|
6451
6500
|
autoMerge: r.autoMerge,
|
|
6452
6501
|
folderSync: rt.folderSync,
|
|
6502
|
+
seats: { dev: seats(r, "dev"), qa: seats(r, "qa") },
|
|
6453
6503
|
capacity: {
|
|
6454
6504
|
developers: this.state.agents.filter((a) => a.repoId === r.id && a.role === "dev").length,
|
|
6455
6505
|
developersFree: this.available(r, "dev").length,
|
|
@@ -6503,10 +6553,11 @@ ${t}`);
|
|
|
6503
6553
|
sessionLimit: s.sessionLimit || "none",
|
|
6504
6554
|
sessionsRunning: this.running(),
|
|
6505
6555
|
usage: usageLabel(this.usageNow(), Date.now()),
|
|
6506
|
-
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa }
|
|
6556
|
+
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa },
|
|
6557
|
+
proposalLimit: { pending: pending2.length, max: MAX_PENDING_PROPOSALS }
|
|
6507
6558
|
},
|
|
6508
6559
|
floors,
|
|
6509
|
-
pendingProposals:
|
|
6560
|
+
pendingProposals: pending2.map(req),
|
|
6510
6561
|
recentDecisions: this.state.requests.filter((r) => r.status !== "pending").slice(-10).map(req)
|
|
6511
6562
|
},
|
|
6512
6563
|
null,
|
|
@@ -6565,7 +6616,7 @@ ${t}`);
|
|
|
6565
6616
|
if (!title) throw new Error("A hire needs a job title.");
|
|
6566
6617
|
const specialty = specialtySlug(x.specialty);
|
|
6567
6618
|
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6568
|
-
|
|
6619
|
+
checkPendingLimit(pending2.length);
|
|
6569
6620
|
const dup = pending2.find((r) => r.kind === "hire" && r.repoId === repo.id && r.role === role && r.specialty === specialty);
|
|
6570
6621
|
if (dup) throw new Error(`${dup.name} (${dup.title}) is already proposed for floor ${repo.floor} with that specialty.`);
|
|
6571
6622
|
const seated = this.state.agents.filter((a) => a.repoId === repo.id && a.role === role).length;
|
|
@@ -6613,7 +6664,7 @@ ${t}`);
|
|
|
6613
6664
|
}
|
|
6614
6665
|
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6615
6666
|
if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
|
|
6616
|
-
|
|
6667
|
+
checkPendingLimit(pending2.length);
|
|
6617
6668
|
const req = {
|
|
6618
6669
|
id: crypto6.randomUUID(),
|
|
6619
6670
|
kind: "let-go",
|
|
@@ -6814,10 +6865,10 @@ app.post(
|
|
|
6814
6865
|
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 }))
|
|
6815
6866
|
);
|
|
6816
6867
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
6817
|
-
var dist =
|
|
6818
|
-
if (
|
|
6868
|
+
var dist = path14.resolve(import.meta.dirname, "../dist");
|
|
6869
|
+
if (fs12.existsSync(dist)) {
|
|
6819
6870
|
app.use(express.static(dist));
|
|
6820
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
6871
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path14.join(dist, "index.html")));
|
|
6821
6872
|
}
|
|
6822
6873
|
app.use((err, _req, res, _next) => {
|
|
6823
6874
|
const parserStatus = err?.status;
|