patchcord 0.5.99 → 0.5.100

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.
Files changed (2) hide show
  1. package/bin/patchcord.mjs +168 -45
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -793,22 +793,25 @@ if (cmd === "subscribe") {
793
793
  // works the same as the space-form (--token foo). The internal flag parsing below
794
794
  // supports both. Non-flag commands (channel, init, skill, help, plugin-path) have
795
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") {
796
+ // ── Main agent: provisioning + team orchestration ──────────────────────────
797
+ // (docs/main-agent.md). The "main" is the managing agent of a team; its main
798
+ // token is a user-scoped provisioning credential stored at ~/.patchcord/main.json
799
+ // (legacy: master.json) or $PATCHCORD_MAIN_TOKEN (legacy: $PATCHCORD_MASTER_TOKEN).
800
+ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team") {
800
801
  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 MAIN_CONFIG = join(HOME, ".patchcord", "main.json");
803
+ const LEGACY_CONFIG = join(HOME, ".patchcord", "master.json"); // pre-rename
802
804
  const DEFAULT_API = process.env.PATCHCORD_BASE_URL || "https://api.patchcord.dev";
803
805
 
804
- const readMaster = () => {
805
- if (process.env.PATCHCORD_MASTER_TOKEN) {
806
- return { token: process.env.PATCHCORD_MASTER_TOKEN, baseUrl: DEFAULT_API };
806
+ const readMain = () => {
807
+ const envTok = process.env.PATCHCORD_MAIN_TOKEN || process.env.PATCHCORD_MASTER_TOKEN;
808
+ if (envTok) return { token: envTok, baseUrl: DEFAULT_API };
809
+ for (const p of [MAIN_CONFIG, LEGACY_CONFIG]) {
810
+ try {
811
+ const m = JSON.parse(readFileSync(p, "utf-8"));
812
+ if (m && m.token) return { token: m.token, baseUrl: m.baseUrl || DEFAULT_API };
813
+ } catch {}
807
814
  }
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
815
  return null;
813
816
  };
814
817
  const flagVal = (name, def = "") => {
@@ -818,9 +821,9 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
818
821
  if (i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("-")) return process.argv[i + 1];
819
822
  return def;
820
823
  };
821
- const requireMaster = () => {
822
- const m = readMaster();
823
- if (!m) { console.error("No master token. Run: patchcord master connect"); process.exit(1); }
824
+ const requireMain = () => {
825
+ const m = readMain();
826
+ if (!m) { console.error("No main token. Run: patchcord main connect"); process.exit(1); }
824
827
  return m;
825
828
  };
826
829
  const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
@@ -862,16 +865,16 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
862
865
  });
863
866
  };
864
867
 
865
- if (cmd === "master") {
868
+ if (cmd === "main" || cmd === "master") {
866
869
  const sub = process.argv[3];
867
870
  if (sub === "connect") {
868
871
  const harness = flagVal("harness", flagVal("tool", ""));
869
872
  const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/master/session" -H "Content-Type: application/json" -d '{"harness":"${harness}"}'`);
870
873
  let sessionId = "";
871
874
  try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
872
- if (!sessionId) { console.error("Could not start master-connect session."); process.exit(1); }
875
+ if (!sessionId) { console.error("Could not start main-auth session."); process.exit(1); }
873
876
  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`);
877
+ console.log(`\n Authorize the main agent in your browser:\n ${M.cyan}${url}${M.rst}\n`);
875
878
  console.log(` ${M.dim}Waiting for authorization...${M.rst}`);
876
879
  const http = await import("http");
877
880
  const https = await import("https");
@@ -894,24 +897,24 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
894
897
  setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
895
898
  });
896
899
  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
+ mkdirSync(dirname(MAIN_CONFIG), { recursive: true });
901
+ writeFileSync(MAIN_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
902
+ console.log(`\n ${M.green}✓${M.rst} Main token saved: ${M.dim}${MAIN_CONFIG}${M.rst}`);
900
903
  process.exit(0);
901
904
  }
902
905
  if (sub === "whoami") {
903
- const m = requireMaster();
906
+ const m = requireMain();
904
907
  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`);
908
+ if (status !== "200" || !json) { console.error(`main whoami failed (HTTP ${status})`); process.exit(1); }
909
+ console.log(`main · user ${json.user_id} · ${json.agents}/${json.quota} agents`);
907
910
  process.exit(0);
908
911
  }
909
- console.error("Usage: patchcord master <connect|whoami>");
912
+ console.error("Usage: patchcord main <connect|whoami>");
910
913
  process.exit(1);
911
914
  }
912
915
 
913
916
  if (cmd === "provision") {
914
- const m = requireMaster();
917
+ const m = requireMain();
915
918
  const arg = process.argv[3];
916
919
  if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
917
920
  if (arg === "revoke") {
@@ -928,7 +931,7 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
928
931
  const ns = flagVal("namespace");
929
932
  const subdir = flagVal("dir", arg);
930
933
  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}` });
934
+ const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: arg, tool, role, label: `main:${tool}` });
932
935
  if (status !== "200" || !json?.token) { console.error(`provision failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
933
936
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
934
937
  const dir = join(process.cwd(), subdir);
@@ -955,39 +958,39 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
955
958
  const sub = process.argv[3];
956
959
  if (sub === "init") {
957
960
  const project = process.argv[4];
958
- if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns] [--tool <master-harness>]"); process.exit(1); }
961
+ if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns] [--tool <main-harness>]"); process.exit(1); }
959
962
  const ns = flagVal("namespace", project.replace(/[^a-z0-9-]/gi, "-").toLowerCase());
960
- const masterTool = flagVal("tool", "claude_code");
963
+ const mainTool = flagVal("tool", "claude_code");
961
964
  const root = join(process.cwd(), project);
962
965
  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();
966
+ const manifest = { project, namespace: ns, pattern: "architect-workers-reviewer", main: null, agents: [] };
967
+ // Provision the main's OWN messaging identity IN the team namespace and
968
+ // write its config at the project root, so the main — running here —
969
+ // resolves to ns:main and SEES every worker it provisions into ns.
970
+ // Without this the main has no identity and whoami grabs a stale global.
971
+ const m = readMain();
969
972
  if (m) {
970
973
  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" });
974
+ const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: "main", tool: mainTool, role: "main", label: "main:self" });
972
975
  if (status === "200" && json?.token) {
973
976
  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
+ writeWorkerConfig(mainTool, root, base, json.token, hostname);
978
+ manifest.main = { agent: "main", tool: mainTool };
979
+ console.log(`✓ main identity: ${M.green}${ns}:main${M.rst} [${mainTool}] — config written at ${root}`);
977
980
  } else {
978
- console.log(`⚠ could not provision master identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
981
+ console.log(`⚠ could not provision main identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
979
982
  }
980
983
  } else {
981
- console.log(`⚠ no master token — run 'patchcord master connect' first, then re-run team init`);
984
+ console.log(`⚠ no main token — run 'patchcord main connect' first, then re-run team init`);
982
985
  }
983
986
  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`);
987
+ writeFileSync(join(root, "AGENTS.md"), `# ${project} — Patchcord team\n\nNamespace: ${ns}\nMain identity: ${ns}:main (you, when running patchcord from this folder)\nProvision workers: patchcord provision <agent> --tool X --role Y --namespace ${ns} --dir <agent>/\n`);
985
988
  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}`);
989
+ console.log(` ${M.dim}cd ${project} — your identity there is ${ns}:main; you'll see every worker you provision into ${ns}.${M.rst}`);
987
990
  process.exit(0);
988
991
  }
989
992
  if (sub === "list") {
990
- const m = requireMaster();
993
+ const m = requireMain();
991
994
  const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
992
995
  if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
993
996
  const agents = json?.agents || [];
@@ -995,20 +998,140 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
995
998
  if (!agents.length) console.log(" (no agents)");
996
999
  process.exit(0);
997
1000
  }
1001
+ if (sub === "status") {
1002
+ // Reconciler: folder ↔ patchcord identity ↔ tmux session ↔ token-active.
1003
+ // Truth = .patchcord/team.json. For each agent we resolve the token from
1004
+ // ITS folder's config (NOT cwd — identity is the folder), ask the server
1005
+ // who that token is, and cross-check against tmux panes by cwd.
1006
+ let manifest = null;
1007
+ try { manifest = JSON.parse(readFileSync(join(process.cwd(), ".patchcord", "team.json"), "utf-8")); } catch {}
1008
+ if (!manifest) { console.error("No .patchcord/team.json here — cd into the project root."); process.exit(1); }
1009
+ const ns = manifest.namespace;
1010
+ const real = (p) => run(`realpath -m ${JSON.stringify(p)}`) || p;
1011
+ const AGENT_CMDS = /(^|\/)(claude|codex|kimi|kimi-code|opencode|gemini|node)$/;
1012
+
1013
+ // Token reader for a specific folder + tool (folder IS the identity).
1014
+ const tokenInDir = (tool, dir) => {
1015
+ try {
1016
+ if (tool === "codex") {
1017
+ const p = join(dir, ".codex", "config.toml");
1018
+ if (!existsSync(p)) return null;
1019
+ const blk = readFileSync(p, "utf-8").match(/\[mcp_servers\.patchcord[-\w]*\]([\s\S]*?)(?=\n\[|$)/);
1020
+ if (!blk) return null;
1021
+ const u = blk[1].match(/url\s*=\s*"([^"]+)"/);
1022
+ const t = blk[1].match(/Bearer\s+([^\s"]+)/);
1023
+ if (!u || !t) return null;
1024
+ return { token: t[1], baseUrl: u[1].replace(/\/mcp(\/bearer)?$/, "") };
1025
+ }
1026
+ const map = {
1027
+ opencode: [join(dir, "opencode.json"), ["mcp", "patchcord"]],
1028
+ kimi: [join(dir, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"]],
1029
+ "kimi-code": [join(dir, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"]],
1030
+ claude_code: [join(dir, ".mcp.json"), ["mcpServers", "patchcord"]],
1031
+ };
1032
+ const [p, keyPath] = map[tool] || map.claude_code;
1033
+ if (!existsSync(p)) return null;
1034
+ const entry = keyPath.reduce((o, k) => o?.[k], JSON.parse(readFileSync(p, "utf-8")));
1035
+ const auth = entry?.headers?.Authorization;
1036
+ const url = entry?.url;
1037
+ if (!auth || !url) return null;
1038
+ return { token: auth.replace(/^Bearer\s+/i, ""), baseUrl: url.replace(/\/mcp(\/bearer)?$/, "") };
1039
+ } catch { return null; }
1040
+ };
1041
+
1042
+ // tmux pane inventory (best-effort).
1043
+ const panes = [];
1044
+ const tmuxOut = run(`tmux list-panes -a -F '#{session_name}\t#{window_index}\t#{pane_current_path}\t#{pane_current_command}'`);
1045
+ if (tmuxOut) for (const l of tmuxOut.split("\n").filter(Boolean)) {
1046
+ const [session, window, path, cmd] = l.split("\t");
1047
+ panes.push({ session, window, path: real(path), cmd, claimed: false });
1048
+ }
1049
+
1050
+ // Build the roster: main first, then workers.
1051
+ const roster = [];
1052
+ const mainEntry = manifest.main || manifest.master; // master = pre-rename
1053
+ if (mainEntry) roster.push({ ...mainEntry, role: "main", dir: ".", _main: true });
1054
+ for (const a of (manifest.agents || [])) roster.push(a);
1055
+
1056
+ const rows = [];
1057
+ for (const a of roster) {
1058
+ const dir = a.dir || a.agent;
1059
+ const abs = real(join(process.cwd(), dir));
1060
+ const exists = existsSync(abs);
1061
+ const cfg = exists ? tokenInDir(a.tool, abs) : null;
1062
+ let token = "no-config", identity = "—";
1063
+ if (cfg) {
1064
+ const { status, json } = await _httpJSON("GET", `${cfg.baseUrl}/api/agent/whoami`, cfg.token);
1065
+ if (status === "200" && json) {
1066
+ identity = `${json.namespace}:${json.agent}`;
1067
+ const expected = `${a.namespace || ns}:${a.agent}`;
1068
+ token = identity === expected ? "live" : `MISMATCH(want ${expected})`;
1069
+ } else if (status === "401" || status === "403") token = `dead(${status})`;
1070
+ else token = `err(${status})`;
1071
+ } else if (!exists) token = "no-folder";
1072
+ // tmux match by folder cwd.
1073
+ let tmux = "—";
1074
+ const hit = panes.find((p) => p.path === abs && AGENT_CMDS.test("/" + p.cmd));
1075
+ if (hit) { hit.claimed = true; tmux = `${hit.session}:${hit.window}(${hit.cmd})`; }
1076
+ else if (panes.some((p) => p.path === abs)) { const p = panes.find((p) => p.path === abs); p.claimed = true; tmux = `${p.session}:${p.window}(shell)`; }
1077
+ rows.push({ role: a.role || "", agent: a.agent, dir: dir === "." ? "./" : dir + "/", identity, token, tmux });
1078
+ }
1079
+
1080
+ // Render.
1081
+ const C = (s, c) => `${c}${s}${M.rst}`;
1082
+ console.log(`\n team ${C(manifest.project, M.cyan)} · namespace ${C(ns, M.cyan)}\n`);
1083
+ const cols = [["ROLE", "role"], ["AGENT", "agent"], ["FOLDER", "dir"], ["IDENTITY", "identity"], ["TOKEN", "token"], ["TMUX", "tmux"]];
1084
+ const w = cols.map(([h, k]) => Math.max(h.length, ...rows.map((r) => String(r[k]).length)));
1085
+ const line = (cells) => " " + cells.map((c, i) => String(c).padEnd(w[i])).join(" ");
1086
+ console.log(C(line(cols.map((c) => c[0])), M.dim));
1087
+ for (const r of rows) {
1088
+ const tokColor = r.token === "live" ? M.green : (r.token.startsWith("MISMATCH") || r.token.startsWith("dead") ? "\x1b[31m" : M.dim);
1089
+ const cells = cols.map(([, k]) => k === "token" ? C(String(r[k]).padEnd(w[cols.findIndex((c) => c[1] === "token")]), tokColor) : String(r[k]).padEnd(w[cols.findIndex((c) => c[1] === k)]));
1090
+ console.log(" " + cells.join(" "));
1091
+ }
1092
+
1093
+ // Orphans: agent CLIs running in folders that are NOT team agents.
1094
+ const orphans = panes.filter((p) => !p.claimed && AGENT_CMDS.test("/" + p.cmd));
1095
+ if (orphans.length) {
1096
+ console.log(`\n ${C("⚠ unanchored agent sessions", "\x1b[33m")} (running outside any team folder — they get GLOBAL identity, not a team seat):`);
1097
+ for (const p of orphans) {
1098
+ const anchored = existsSync(join(p.path, ".mcp.json")) || existsSync(join(p.path, "opencode.json")) || existsSync(join(p.path, ".codex", "config.toml")) || existsSync(join(p.path, ".kimi-code", "mcp.json"));
1099
+ console.log(` ${p.session}:${p.window} ${p.cmd} ${C(p.path, M.dim)}${anchored ? "" : C(" [no project config → falls back to a stale global identity]", "\x1b[33m")}`);
1100
+ }
1101
+ console.log(`\n ${C("fix:", M.dim)} provision a seat for the folder, then relaunch in place WITH the resume flag:`);
1102
+ console.log(` ${C(`patchcord provision <agent> --tool <tool> --role <role> --namespace ${ns} --dir <folder>/`, M.dim)}`);
1103
+ }
1104
+ console.log();
1105
+ process.exit(0);
1106
+ }
998
1107
  if (sub === "launch") {
999
1108
  let manifest = null;
1000
1109
  try { manifest = JSON.parse(readFileSync(join(process.cwd(), ".patchcord", "team.json"), "utf-8")); } catch {}
1001
1110
  if (!manifest) { console.error("No .patchcord/team.json here — cd into the project root."); process.exit(1); }
1002
1111
  const { spawnSync } = await import("child_process");
1112
+ // Already-running folders: `mux new` would FRESH-launch and wipe their
1113
+ // context. Skip them — relaunch-for-identity must resume in place
1114
+ // (see the team-ops / mux skill). launch is for INITIAL boot only.
1115
+ const real = (p) => run(`realpath -m ${JSON.stringify(p)}`) || p;
1116
+ const running = new Set();
1117
+ const tmuxOut = run(`tmux list-panes -a -F '#{pane_current_path}\t#{pane_current_command}'`);
1118
+ if (tmuxOut) for (const l of tmuxOut.split("\n").filter(Boolean)) {
1119
+ const [path, cmd] = l.split("\t");
1120
+ if (/(^|\/)(claude|codex|kimi|kimi-code|opencode|gemini|node)$/.test("/" + cmd)) running.add(real(path));
1121
+ }
1003
1122
  for (const a of (manifest.agents || [])) {
1004
1123
  const dir = join(process.cwd(), a.dir || a.agent);
1124
+ if (running.has(real(dir))) {
1125
+ console.log(` ${M.dim}skip ${a.agent} — already running in ${dir}; relaunch-for-identity must resume in place, not fresh-launch (see team-ops skill).${M.rst}`);
1126
+ continue;
1127
+ }
1005
1128
  const muxTool = a.tool === "claude_code" ? "claude" : (a.tool === "kimi-code" ? "kimi" : a.tool);
1006
1129
  console.log(` launching ${a.agent} (${muxTool}) in ${dir}`);
1007
1130
  spawnSync("mux", ["new", muxTool, "--dir", dir], { stdio: "inherit" });
1008
1131
  }
1009
1132
  process.exit(0);
1010
1133
  }
1011
- console.error("Usage: patchcord team <init|list|launch>");
1134
+ console.error("Usage: patchcord team <init|list|launch|status>");
1012
1135
  process.exit(1);
1013
1136
  }
1014
1137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.99",
3
+ "version": "0.5.100",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",