patchcord 0.5.96 → 0.5.98
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/patchcord.mjs +202 -11
- package/package.json +1 -1
package/bin/patchcord.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
4
|
import { join, dirname, basename } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { execSync } from "child_process";
|
|
@@ -787,6 +787,194 @@ if (cmd === "subscribe") {
|
|
|
787
787
|
// works the same as the space-form (--token foo). The internal flag parsing below
|
|
788
788
|
// supports both. Non-flag commands (channel, init, skill, help, plugin-path) have
|
|
789
789
|
// their own branches above and below.
|
|
790
|
+
// ── Master agent: provisioning + team orchestration ────────────────────────
|
|
791
|
+
// (docs/master-agent.md). A master token is a user-scoped provisioning
|
|
792
|
+
// credential stored at ~/.patchcord/master.json or $PATCHCORD_MASTER_TOKEN.
|
|
793
|
+
if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
794
|
+
const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
|
|
795
|
+
const MASTER_CONFIG = join(HOME, ".patchcord", "master.json");
|
|
796
|
+
const DEFAULT_API = process.env.PATCHCORD_BASE_URL || "https://api.patchcord.dev";
|
|
797
|
+
|
|
798
|
+
const readMaster = () => {
|
|
799
|
+
if (process.env.PATCHCORD_MASTER_TOKEN) {
|
|
800
|
+
return { token: process.env.PATCHCORD_MASTER_TOKEN, baseUrl: DEFAULT_API };
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
const m = JSON.parse(readFileSync(MASTER_CONFIG, "utf-8"));
|
|
804
|
+
if (m && m.token) return { token: m.token, baseUrl: m.baseUrl || DEFAULT_API };
|
|
805
|
+
} catch {}
|
|
806
|
+
return null;
|
|
807
|
+
};
|
|
808
|
+
const flagVal = (name, def = "") => {
|
|
809
|
+
const eq = process.argv.find((a) => a.startsWith(`--${name}=`));
|
|
810
|
+
if (eq) return eq.split("=").slice(1).join("=");
|
|
811
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
812
|
+
if (i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("-")) return process.argv[i + 1];
|
|
813
|
+
return def;
|
|
814
|
+
};
|
|
815
|
+
const requireMaster = () => {
|
|
816
|
+
const m = readMaster();
|
|
817
|
+
if (!m) { console.error("No master token. Run: patchcord master connect"); process.exit(1); }
|
|
818
|
+
return m;
|
|
819
|
+
};
|
|
820
|
+
const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
|
|
821
|
+
mkdirSync(dir, { recursive: true });
|
|
822
|
+
const hdr = { Authorization: `Bearer ${token}`, "X-Patchcord-Machine": hostname };
|
|
823
|
+
const writeJson = (p, mutate) => {
|
|
824
|
+
let obj = {};
|
|
825
|
+
try { obj = JSON.parse(readFileSync(p, "utf-8")); } catch {}
|
|
826
|
+
mutate(obj);
|
|
827
|
+
writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
|
|
828
|
+
return p;
|
|
829
|
+
};
|
|
830
|
+
if (tool === "codex") {
|
|
831
|
+
const cdir = join(dir, ".codex"); mkdirSync(cdir, { recursive: true });
|
|
832
|
+
const p = join(cdir, "config.toml");
|
|
833
|
+
let ex = existsSync(p) ? readFileSync(p, "utf-8") : "";
|
|
834
|
+
ex = ex.replace(/\[mcp_servers\.patchcord[\w.-]*\][^\[]*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
835
|
+
ex = (ex ? ex + "\n\n" : "") + `[mcp_servers.patchcord-codex]\nurl = "${baseUrl}/mcp"\nhttp_headers = { "Authorization" = "Bearer ${token}", "X-Patchcord-Machine" = "${hostname}" }\ntool_timeout_sec = 300\n`;
|
|
836
|
+
writeFileSync(p, ex);
|
|
837
|
+
return p;
|
|
838
|
+
}
|
|
839
|
+
if (tool === "kimi" || tool === "kimi-code") {
|
|
840
|
+
const kdir = join(dir, ".kimi-code"); mkdirSync(kdir, { recursive: true });
|
|
841
|
+
return writeJson(join(kdir, "mcp.json"), (o) => {
|
|
842
|
+
o.mcpServers = o.mcpServers || {};
|
|
843
|
+
o.mcpServers.patchcord = { url: `${baseUrl}/mcp/bearer`, headers: hdr };
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (tool === "opencode") {
|
|
847
|
+
return writeJson(join(dir, "opencode.json"), (o) => {
|
|
848
|
+
o.mcp = o.mcp || {};
|
|
849
|
+
o.mcp.patchcord = { type: "remote", url: `${baseUrl}/mcp`, headers: hdr };
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
// default: claude_code
|
|
853
|
+
return writeJson(join(dir, ".mcp.json"), (o) => {
|
|
854
|
+
o.mcpServers = o.mcpServers || {};
|
|
855
|
+
o.mcpServers.patchcord = { url: `${baseUrl}/mcp`, headers: hdr };
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
if (cmd === "master") {
|
|
860
|
+
const sub = process.argv[3];
|
|
861
|
+
if (sub === "connect") {
|
|
862
|
+
const harness = flagVal("harness", flagVal("tool", ""));
|
|
863
|
+
const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/master/session" -H "Content-Type: application/json" -d '{"harness":"${harness}"}'`);
|
|
864
|
+
let sessionId = "";
|
|
865
|
+
try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
|
|
866
|
+
if (!sessionId) { console.error("Could not start master-connect session."); process.exit(1); }
|
|
867
|
+
const url = `https://patchcord.dev/master?session=${sessionId}`;
|
|
868
|
+
console.log(`\n Authorize the master agent in your browser:\n ${M.cyan}${url}${M.rst}\n`);
|
|
869
|
+
console.log(` ${M.dim}Waiting for authorization...${M.rst}`);
|
|
870
|
+
const http = await import("http");
|
|
871
|
+
const https = await import("https");
|
|
872
|
+
const token = await new Promise((resolve) => {
|
|
873
|
+
const lib = DEFAULT_API.startsWith("https") ? https : http;
|
|
874
|
+
const req = lib.get(`${DEFAULT_API}/api/connect/session/${sessionId}/wait`, { headers: { Accept: "text/event-stream" } }, (res) => {
|
|
875
|
+
let buf = "";
|
|
876
|
+
res.on("data", (c) => {
|
|
877
|
+
buf += c.toString();
|
|
878
|
+
for (const line of buf.split("\n")) {
|
|
879
|
+
if (line.startsWith("data: ")) {
|
|
880
|
+
try { const d = JSON.parse(line.slice(6)); if (d.token) { resolve(d.token); try { req.destroy(); } catch {} return; } } catch {}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
res.on("end", () => resolve(null));
|
|
885
|
+
res.on("error", () => resolve(null));
|
|
886
|
+
});
|
|
887
|
+
req.on("error", () => resolve(null));
|
|
888
|
+
setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
|
|
889
|
+
});
|
|
890
|
+
if (!token) { console.error("\n Authorization timed out or failed."); process.exit(1); }
|
|
891
|
+
mkdirSync(dirname(MASTER_CONFIG), { recursive: true });
|
|
892
|
+
writeFileSync(MASTER_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
|
|
893
|
+
console.log(`\n ${M.green}✓${M.rst} Master token saved: ${M.dim}${MASTER_CONFIG}${M.rst}`);
|
|
894
|
+
process.exit(0);
|
|
895
|
+
}
|
|
896
|
+
if (sub === "whoami") {
|
|
897
|
+
const m = requireMaster();
|
|
898
|
+
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/master/whoami`, m.token);
|
|
899
|
+
if (status !== "200" || !json) { console.error(`master whoami failed (HTTP ${status})`); process.exit(1); }
|
|
900
|
+
console.log(`master · user ${json.user_id} · ${json.agents}/${json.quota} agents`);
|
|
901
|
+
process.exit(0);
|
|
902
|
+
}
|
|
903
|
+
console.error("Usage: patchcord master <connect|whoami>");
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (cmd === "provision") {
|
|
908
|
+
const m = requireMaster();
|
|
909
|
+
const arg = process.argv[3];
|
|
910
|
+
if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
|
|
911
|
+
if (arg === "revoke") {
|
|
912
|
+
const ra = process.argv[4];
|
|
913
|
+
const ns = flagVal("namespace");
|
|
914
|
+
if (!ra || !ns) { console.error("Usage: patchcord provision revoke <agent> --namespace ns"); process.exit(1); }
|
|
915
|
+
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision/revoke`, m.token, { namespace_id: ns, agent_id: ra });
|
|
916
|
+
if (status !== "200") { console.error(`revoke failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
917
|
+
console.log(`✓ revoked ${ns}:${ra} (${json.count} token(s))`);
|
|
918
|
+
process.exit(0);
|
|
919
|
+
}
|
|
920
|
+
const tool = flagVal("tool", "claude_code");
|
|
921
|
+
const role = flagVal("role", "");
|
|
922
|
+
const ns = flagVal("namespace");
|
|
923
|
+
const subdir = flagVal("dir", arg);
|
|
924
|
+
if (!ns) { console.error("--namespace <project-namespace> required"); process.exit(1); }
|
|
925
|
+
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: arg, tool, role, label: `master:${tool}` });
|
|
926
|
+
if (status !== "200" || !json?.token) { console.error(`provision failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
927
|
+
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
928
|
+
const dir = join(process.cwd(), subdir);
|
|
929
|
+
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
930
|
+
writeWorkerConfig(tool, dir, base, json.token, hostname);
|
|
931
|
+
if (role) {
|
|
932
|
+
try { writeFileSync(join(dir, "AGENTS.md"), `# ${arg} — role: ${role}\n\nYou are \`${arg}\` in the \`${ns}\` Patchcord team. Role: ${role}.\nCoordinate with teammates over Patchcord (inbox / send_message / reply).\n`); } catch {}
|
|
933
|
+
}
|
|
934
|
+
console.log(`✓ provisioned ${M.green}${ns}:${arg}${M.rst} [${tool}${role ? "/" + role : ""}] → ${dir}`);
|
|
935
|
+
process.exit(0);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (cmd === "team") {
|
|
939
|
+
const sub = process.argv[3];
|
|
940
|
+
if (sub === "init") {
|
|
941
|
+
const project = process.argv[4];
|
|
942
|
+
if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns]"); process.exit(1); }
|
|
943
|
+
const ns = flagVal("namespace", project.replace(/[^a-z0-9-]/gi, "-").toLowerCase());
|
|
944
|
+
const root = join(process.cwd(), project);
|
|
945
|
+
mkdirSync(join(root, ".patchcord"), { recursive: true });
|
|
946
|
+
writeFileSync(join(root, ".patchcord", "team.json"), JSON.stringify({ project, namespace: ns, pattern: "architect-workers-reviewer", agents: [] }, null, 2) + "\n");
|
|
947
|
+
writeFileSync(join(root, "AGENTS.md"), `# ${project} — Patchcord team\n\nNamespace: ${ns}\nProvision: patchcord provision <agent> --tool X --role Y --namespace ${ns} --dir <agent>/\n`);
|
|
948
|
+
console.log(`✓ team scaffolded: ${root} (namespace ${ns})`);
|
|
949
|
+
process.exit(0);
|
|
950
|
+
}
|
|
951
|
+
if (sub === "list") {
|
|
952
|
+
const m = requireMaster();
|
|
953
|
+
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
|
|
954
|
+
if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
|
|
955
|
+
const agents = json?.agents || [];
|
|
956
|
+
for (const a of agents) console.log(` ${a.namespace_id}:${a.agent_id} ${M.dim}${a.label || ""}${M.rst}`);
|
|
957
|
+
if (!agents.length) console.log(" (no agents)");
|
|
958
|
+
process.exit(0);
|
|
959
|
+
}
|
|
960
|
+
if (sub === "launch") {
|
|
961
|
+
let manifest = null;
|
|
962
|
+
try { manifest = JSON.parse(readFileSync(join(process.cwd(), ".patchcord", "team.json"), "utf-8")); } catch {}
|
|
963
|
+
if (!manifest) { console.error("No .patchcord/team.json here — cd into the project root."); process.exit(1); }
|
|
964
|
+
const { spawnSync } = await import("child_process");
|
|
965
|
+
for (const a of (manifest.agents || [])) {
|
|
966
|
+
const dir = join(process.cwd(), a.dir || a.agent);
|
|
967
|
+
const muxTool = a.tool === "claude_code" ? "claude" : (a.tool === "kimi-code" ? "kimi" : a.tool);
|
|
968
|
+
console.log(` launching ${a.agent} (${muxTool}) in ${dir}`);
|
|
969
|
+
spawnSync("mux", ["new", muxTool, "--dir", dir], { stdio: "inherit" });
|
|
970
|
+
}
|
|
971
|
+
process.exit(0);
|
|
972
|
+
}
|
|
973
|
+
console.error("Usage: patchcord team <init|list|launch>");
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
790
978
|
if (cmd === "update" || cmd === "--update") {
|
|
791
979
|
const { spawnSync: _spawn } = await import("child_process");
|
|
792
980
|
const r = _spawn("npx", ["--min-release-age=0", "patchcord@latest", "--update-only"], { stdio: "inherit" });
|
|
@@ -1225,16 +1413,19 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1225
1413
|
copyFileSync(hookScriptSrc, hookScriptDest);
|
|
1226
1414
|
chmodSync(hookScriptDest, 0o755);
|
|
1227
1415
|
|
|
1228
|
-
// Enable hooks feature flag
|
|
1229
|
-
//
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1416
|
+
// Enable the `hooks` feature flag exactly once. Older installs / Codex
|
|
1417
|
+
// versions may leave `codex_hooks = true`, `hooks=true` (no spaces), a
|
|
1418
|
+
// `hooks = false`, or even a duplicate `hooks = true` — any of which makes
|
|
1419
|
+
// config.toml fail to load with a "duplicate key" error. Strip every
|
|
1420
|
+
// feature-flag variant first, then add exactly one under [features].
|
|
1421
|
+
globalCodexContent = globalCodexContent
|
|
1422
|
+
.replace(/^[ \t]*(?:codex_)?hooks[ \t]*=[ \t]*(?:true|false)[ \t]*(?:#[^\n]*)?\r?\n?/gm, "")
|
|
1423
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1424
|
+
.trimEnd() + "\n";
|
|
1425
|
+
if (/^\[features\][ \t]*$/m.test(globalCodexContent)) {
|
|
1426
|
+
globalCodexContent = globalCodexContent.replace(/^(\[features\][ \t]*\r?\n)/m, "$1hooks = true\n");
|
|
1427
|
+
} else {
|
|
1428
|
+
globalCodexContent = globalCodexContent.trimEnd() + "\n\n[features]\nhooks = true\n";
|
|
1238
1429
|
}
|
|
1239
1430
|
|
|
1240
1431
|
// Remove any old patchcord stop hook entry from config.toml (moved to hooks.json)
|