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.
- package/bin/patchcord.mjs +168 -45
- 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
|
-
// ──
|
|
797
|
-
// (docs/
|
|
798
|
-
// credential stored at ~/.patchcord/
|
|
799
|
-
|
|
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
|
|
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
|
|
805
|
-
|
|
806
|
-
|
|
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
|
|
822
|
-
const m =
|
|
823
|
-
if (!m) { console.error("No
|
|
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
|
|
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
|
|
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(
|
|
898
|
-
writeFileSync(
|
|
899
|
-
console.log(`\n ${M.green}✓${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 =
|
|
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(`
|
|
906
|
-
console.log(`
|
|
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
|
|
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 =
|
|
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: `
|
|
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 <
|
|
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
|
|
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",
|
|
964
|
-
// Provision the
|
|
965
|
-
// write its config at the project root, so the
|
|
966
|
-
// resolves to ns:
|
|
967
|
-
// Without this the
|
|
968
|
-
const m =
|
|
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: "
|
|
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(
|
|
975
|
-
manifest.
|
|
976
|
-
console.log(`✓
|
|
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
|
|
981
|
+
console.log(`⚠ could not provision main identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
|
|
979
982
|
}
|
|
980
983
|
} else {
|
|
981
|
-
console.log(`⚠ no
|
|
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}\
|
|
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}:
|
|
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 =
|
|
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
|
}
|