patchcord 0.5.97 → 0.5.99
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 +229 -3
- 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";
|
|
@@ -273,7 +273,7 @@ async function _resolveBearer(options = {}) {
|
|
|
273
273
|
while (dir && dir !== "/") {
|
|
274
274
|
for (const r of projectReaders) {
|
|
275
275
|
const found = r(dir);
|
|
276
|
-
if (found) return found;
|
|
276
|
+
if (found) { found.scope = "project"; return found; }
|
|
277
277
|
}
|
|
278
278
|
dir = dirname(dir);
|
|
279
279
|
}
|
|
@@ -323,7 +323,7 @@ async function _resolveBearer(options = {}) {
|
|
|
323
323
|
: [...defaultGlobalCandidates, kimiGlobalReader, kimiCodeGlobalReader];
|
|
324
324
|
for (const r of globalCandidates) {
|
|
325
325
|
const found = r();
|
|
326
|
-
if (found) return found;
|
|
326
|
+
if (found) { found.scope = "global"; return found; }
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
return null;
|
|
@@ -353,6 +353,12 @@ if (cmd === "whoami") {
|
|
|
353
353
|
console.error("No patchcord config found in current directory or any parent. Run `npx patchcord@latest` from a project directory first.");
|
|
354
354
|
process.exit(1);
|
|
355
355
|
}
|
|
356
|
+
if (found.scope === "global") {
|
|
357
|
+
console.error(`\x1b[33m⚠ No project patchcord config in ${process.cwd()} or any parent.`);
|
|
358
|
+
console.error(` Falling back to a GLOBAL config: ${found.configFile} (tool: ${found.tool}).`);
|
|
359
|
+
console.error(` If this isn't the agent you expected, you're in a directory without its own .mcp.json —`);
|
|
360
|
+
console.error(` the identity below belongs to that global config, NOT to whatever harness launched this.\x1b[0m`);
|
|
361
|
+
}
|
|
356
362
|
const { token, baseUrl } = found;
|
|
357
363
|
if (!isSafeToken(token) || !isSafeUrl(baseUrl)) {
|
|
358
364
|
console.error(`Invalid patchcord URL or token in config.`);
|
|
@@ -787,6 +793,226 @@ if (cmd === "subscribe") {
|
|
|
787
793
|
// works the same as the space-form (--token foo). The internal flag parsing below
|
|
788
794
|
// supports both. Non-flag commands (channel, init, skill, help, plugin-path) have
|
|
789
795
|
// their own branches above and below.
|
|
796
|
+
// ── Master agent: provisioning + team orchestration ────────────────────────
|
|
797
|
+
// (docs/master-agent.md). A master token is a user-scoped provisioning
|
|
798
|
+
// credential stored at ~/.patchcord/master.json or $PATCHCORD_MASTER_TOKEN.
|
|
799
|
+
if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
800
|
+
const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
|
|
801
|
+
const MASTER_CONFIG = join(HOME, ".patchcord", "master.json");
|
|
802
|
+
const DEFAULT_API = process.env.PATCHCORD_BASE_URL || "https://api.patchcord.dev";
|
|
803
|
+
|
|
804
|
+
const readMaster = () => {
|
|
805
|
+
if (process.env.PATCHCORD_MASTER_TOKEN) {
|
|
806
|
+
return { token: process.env.PATCHCORD_MASTER_TOKEN, baseUrl: DEFAULT_API };
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
const m = JSON.parse(readFileSync(MASTER_CONFIG, "utf-8"));
|
|
810
|
+
if (m && m.token) return { token: m.token, baseUrl: m.baseUrl || DEFAULT_API };
|
|
811
|
+
} catch {}
|
|
812
|
+
return null;
|
|
813
|
+
};
|
|
814
|
+
const flagVal = (name, def = "") => {
|
|
815
|
+
const eq = process.argv.find((a) => a.startsWith(`--${name}=`));
|
|
816
|
+
if (eq) return eq.split("=").slice(1).join("=");
|
|
817
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
818
|
+
if (i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("-")) return process.argv[i + 1];
|
|
819
|
+
return def;
|
|
820
|
+
};
|
|
821
|
+
const requireMaster = () => {
|
|
822
|
+
const m = readMaster();
|
|
823
|
+
if (!m) { console.error("No master token. Run: patchcord master connect"); process.exit(1); }
|
|
824
|
+
return m;
|
|
825
|
+
};
|
|
826
|
+
const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
|
|
827
|
+
mkdirSync(dir, { recursive: true });
|
|
828
|
+
const hdr = { Authorization: `Bearer ${token}`, "X-Patchcord-Machine": hostname };
|
|
829
|
+
const writeJson = (p, mutate) => {
|
|
830
|
+
let obj = {};
|
|
831
|
+
try { obj = JSON.parse(readFileSync(p, "utf-8")); } catch {}
|
|
832
|
+
mutate(obj);
|
|
833
|
+
writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
|
|
834
|
+
return p;
|
|
835
|
+
};
|
|
836
|
+
if (tool === "codex") {
|
|
837
|
+
const cdir = join(dir, ".codex"); mkdirSync(cdir, { recursive: true });
|
|
838
|
+
const p = join(cdir, "config.toml");
|
|
839
|
+
let ex = existsSync(p) ? readFileSync(p, "utf-8") : "";
|
|
840
|
+
ex = ex.replace(/\[mcp_servers\.patchcord[\w.-]*\][^\[]*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
841
|
+
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`;
|
|
842
|
+
writeFileSync(p, ex);
|
|
843
|
+
return p;
|
|
844
|
+
}
|
|
845
|
+
if (tool === "kimi" || tool === "kimi-code") {
|
|
846
|
+
const kdir = join(dir, ".kimi-code"); mkdirSync(kdir, { recursive: true });
|
|
847
|
+
return writeJson(join(kdir, "mcp.json"), (o) => {
|
|
848
|
+
o.mcpServers = o.mcpServers || {};
|
|
849
|
+
o.mcpServers.patchcord = { url: `${baseUrl}/mcp/bearer`, headers: hdr };
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (tool === "opencode") {
|
|
853
|
+
return writeJson(join(dir, "opencode.json"), (o) => {
|
|
854
|
+
o.mcp = o.mcp || {};
|
|
855
|
+
o.mcp.patchcord = { type: "remote", url: `${baseUrl}/mcp`, headers: hdr };
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
// default: claude_code
|
|
859
|
+
return writeJson(join(dir, ".mcp.json"), (o) => {
|
|
860
|
+
o.mcpServers = o.mcpServers || {};
|
|
861
|
+
o.mcpServers.patchcord = { url: `${baseUrl}/mcp`, headers: hdr };
|
|
862
|
+
});
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
if (cmd === "master") {
|
|
866
|
+
const sub = process.argv[3];
|
|
867
|
+
if (sub === "connect") {
|
|
868
|
+
const harness = flagVal("harness", flagVal("tool", ""));
|
|
869
|
+
const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/master/session" -H "Content-Type: application/json" -d '{"harness":"${harness}"}'`);
|
|
870
|
+
let sessionId = "";
|
|
871
|
+
try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
|
|
872
|
+
if (!sessionId) { console.error("Could not start master-connect session."); process.exit(1); }
|
|
873
|
+
const url = `https://patchcord.dev/master?session=${sessionId}`;
|
|
874
|
+
console.log(`\n Authorize the master agent in your browser:\n ${M.cyan}${url}${M.rst}\n`);
|
|
875
|
+
console.log(` ${M.dim}Waiting for authorization...${M.rst}`);
|
|
876
|
+
const http = await import("http");
|
|
877
|
+
const https = await import("https");
|
|
878
|
+
const token = await new Promise((resolve) => {
|
|
879
|
+
const lib = DEFAULT_API.startsWith("https") ? https : http;
|
|
880
|
+
const req = lib.get(`${DEFAULT_API}/api/connect/session/${sessionId}/wait`, { headers: { Accept: "text/event-stream" } }, (res) => {
|
|
881
|
+
let buf = "";
|
|
882
|
+
res.on("data", (c) => {
|
|
883
|
+
buf += c.toString();
|
|
884
|
+
for (const line of buf.split("\n")) {
|
|
885
|
+
if (line.startsWith("data: ")) {
|
|
886
|
+
try { const d = JSON.parse(line.slice(6)); if (d.token) { resolve(d.token); try { req.destroy(); } catch {} return; } } catch {}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
res.on("end", () => resolve(null));
|
|
891
|
+
res.on("error", () => resolve(null));
|
|
892
|
+
});
|
|
893
|
+
req.on("error", () => resolve(null));
|
|
894
|
+
setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
|
|
895
|
+
});
|
|
896
|
+
if (!token) { console.error("\n Authorization timed out or failed."); process.exit(1); }
|
|
897
|
+
mkdirSync(dirname(MASTER_CONFIG), { recursive: true });
|
|
898
|
+
writeFileSync(MASTER_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
|
|
899
|
+
console.log(`\n ${M.green}✓${M.rst} Master token saved: ${M.dim}${MASTER_CONFIG}${M.rst}`);
|
|
900
|
+
process.exit(0);
|
|
901
|
+
}
|
|
902
|
+
if (sub === "whoami") {
|
|
903
|
+
const m = requireMaster();
|
|
904
|
+
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/master/whoami`, m.token);
|
|
905
|
+
if (status !== "200" || !json) { console.error(`master whoami failed (HTTP ${status})`); process.exit(1); }
|
|
906
|
+
console.log(`master · user ${json.user_id} · ${json.agents}/${json.quota} agents`);
|
|
907
|
+
process.exit(0);
|
|
908
|
+
}
|
|
909
|
+
console.error("Usage: patchcord master <connect|whoami>");
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (cmd === "provision") {
|
|
914
|
+
const m = requireMaster();
|
|
915
|
+
const arg = process.argv[3];
|
|
916
|
+
if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
|
|
917
|
+
if (arg === "revoke") {
|
|
918
|
+
const ra = process.argv[4];
|
|
919
|
+
const ns = flagVal("namespace");
|
|
920
|
+
if (!ra || !ns) { console.error("Usage: patchcord provision revoke <agent> --namespace ns"); process.exit(1); }
|
|
921
|
+
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision/revoke`, m.token, { namespace_id: ns, agent_id: ra });
|
|
922
|
+
if (status !== "200") { console.error(`revoke failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
923
|
+
console.log(`✓ revoked ${ns}:${ra} (${json.count} token(s))`);
|
|
924
|
+
process.exit(0);
|
|
925
|
+
}
|
|
926
|
+
const tool = flagVal("tool", "claude_code");
|
|
927
|
+
const role = flagVal("role", "");
|
|
928
|
+
const ns = flagVal("namespace");
|
|
929
|
+
const subdir = flagVal("dir", arg);
|
|
930
|
+
if (!ns) { console.error("--namespace <project-namespace> required"); process.exit(1); }
|
|
931
|
+
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: arg, tool, role, label: `master:${tool}` });
|
|
932
|
+
if (status !== "200" || !json?.token) { console.error(`provision failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
933
|
+
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
934
|
+
const dir = join(process.cwd(), subdir);
|
|
935
|
+
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
936
|
+
writeWorkerConfig(tool, dir, base, json.token, hostname);
|
|
937
|
+
if (role) {
|
|
938
|
+
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 {}
|
|
939
|
+
}
|
|
940
|
+
// Record in the local team manifest so `team launch` / `team status` see it.
|
|
941
|
+
try {
|
|
942
|
+
const tj = join(process.cwd(), ".patchcord", "team.json");
|
|
943
|
+
if (existsSync(tj)) {
|
|
944
|
+
const man = JSON.parse(readFileSync(tj, "utf-8"));
|
|
945
|
+
man.agents = (man.agents || []).filter((a) => a.agent !== arg);
|
|
946
|
+
man.agents.push({ agent: arg, tool, role, dir: subdir, namespace: ns });
|
|
947
|
+
writeFileSync(tj, JSON.stringify(man, null, 2) + "\n");
|
|
948
|
+
}
|
|
949
|
+
} catch {}
|
|
950
|
+
console.log(`✓ provisioned ${M.green}${ns}:${arg}${M.rst} [${tool}${role ? "/" + role : ""}] → ${dir}`);
|
|
951
|
+
process.exit(0);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (cmd === "team") {
|
|
955
|
+
const sub = process.argv[3];
|
|
956
|
+
if (sub === "init") {
|
|
957
|
+
const project = process.argv[4];
|
|
958
|
+
if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns] [--tool <master-harness>]"); process.exit(1); }
|
|
959
|
+
const ns = flagVal("namespace", project.replace(/[^a-z0-9-]/gi, "-").toLowerCase());
|
|
960
|
+
const masterTool = flagVal("tool", "claude_code");
|
|
961
|
+
const root = join(process.cwd(), project);
|
|
962
|
+
mkdirSync(join(root, ".patchcord"), { recursive: true });
|
|
963
|
+
const manifest = { project, namespace: ns, pattern: "architect-workers-reviewer", master: null, agents: [] };
|
|
964
|
+
// Provision the master's OWN messaging identity IN the team namespace and
|
|
965
|
+
// write its config at the project root, so the master — running here —
|
|
966
|
+
// resolves to ns:master and SEES every worker it provisions into ns.
|
|
967
|
+
// Without this the master has no identity and whoami grabs a stale global.
|
|
968
|
+
const m = readMaster();
|
|
969
|
+
if (m) {
|
|
970
|
+
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
971
|
+
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: "master", tool: masterTool, role: "master", label: "master:self" });
|
|
972
|
+
if (status === "200" && json?.token) {
|
|
973
|
+
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
974
|
+
writeWorkerConfig(masterTool, root, base, json.token, hostname);
|
|
975
|
+
manifest.master = { agent: "master", tool: masterTool };
|
|
976
|
+
console.log(`✓ master identity: ${M.green}${ns}:master${M.rst} [${masterTool}] — config written at ${root}`);
|
|
977
|
+
} else {
|
|
978
|
+
console.log(`⚠ could not provision master identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
|
|
979
|
+
}
|
|
980
|
+
} else {
|
|
981
|
+
console.log(`⚠ no master token — run 'patchcord master connect' first, then re-run team init`);
|
|
982
|
+
}
|
|
983
|
+
writeFileSync(join(root, ".patchcord", "team.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
984
|
+
writeFileSync(join(root, "AGENTS.md"), `# ${project} — Patchcord team\n\nNamespace: ${ns}\nMaster identity: ${ns}:master (you, when running patchcord from this folder)\nProvision workers: patchcord provision <agent> --tool X --role Y --namespace ${ns} --dir <agent>/\n`);
|
|
985
|
+
console.log(`✓ team scaffolded: ${root} (namespace ${ns})`);
|
|
986
|
+
console.log(` ${M.dim}cd ${project} — your identity there is ${ns}:master; you'll see every worker you provision into ${ns}.${M.rst}`);
|
|
987
|
+
process.exit(0);
|
|
988
|
+
}
|
|
989
|
+
if (sub === "list") {
|
|
990
|
+
const m = requireMaster();
|
|
991
|
+
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
|
|
992
|
+
if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
|
|
993
|
+
const agents = json?.agents || [];
|
|
994
|
+
for (const a of agents) console.log(` ${a.namespace_id}:${a.agent_id} ${M.dim}${a.label || ""}${M.rst}`);
|
|
995
|
+
if (!agents.length) console.log(" (no agents)");
|
|
996
|
+
process.exit(0);
|
|
997
|
+
}
|
|
998
|
+
if (sub === "launch") {
|
|
999
|
+
let manifest = null;
|
|
1000
|
+
try { manifest = JSON.parse(readFileSync(join(process.cwd(), ".patchcord", "team.json"), "utf-8")); } catch {}
|
|
1001
|
+
if (!manifest) { console.error("No .patchcord/team.json here — cd into the project root."); process.exit(1); }
|
|
1002
|
+
const { spawnSync } = await import("child_process");
|
|
1003
|
+
for (const a of (manifest.agents || [])) {
|
|
1004
|
+
const dir = join(process.cwd(), a.dir || a.agent);
|
|
1005
|
+
const muxTool = a.tool === "claude_code" ? "claude" : (a.tool === "kimi-code" ? "kimi" : a.tool);
|
|
1006
|
+
console.log(` launching ${a.agent} (${muxTool}) in ${dir}`);
|
|
1007
|
+
spawnSync("mux", ["new", muxTool, "--dir", dir], { stdio: "inherit" });
|
|
1008
|
+
}
|
|
1009
|
+
process.exit(0);
|
|
1010
|
+
}
|
|
1011
|
+
console.error("Usage: patchcord team <init|list|launch>");
|
|
1012
|
+
process.exit(1);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
790
1016
|
if (cmd === "update" || cmd === "--update") {
|
|
791
1017
|
const { spawnSync: _spawn } = await import("child_process");
|
|
792
1018
|
const r = _spawn("npx", ["--min-release-age=0", "patchcord@latest", "--update-only"], { stdio: "inherit" });
|