patchcord 0.5.98 → 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 +195 -34
- package/package.json +1 -1
package/bin/patchcord.mjs
CHANGED
|
@@ -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,22 +793,25 @@ 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.
|
|
790
|
-
// ──
|
|
791
|
-
// (docs/
|
|
792
|
-
// credential stored at ~/.patchcord/
|
|
793
|
-
|
|
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") {
|
|
794
801
|
const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
|
|
795
|
-
const
|
|
802
|
+
const MAIN_CONFIG = join(HOME, ".patchcord", "main.json");
|
|
803
|
+
const LEGACY_CONFIG = join(HOME, ".patchcord", "master.json"); // pre-rename
|
|
796
804
|
const DEFAULT_API = process.env.PATCHCORD_BASE_URL || "https://api.patchcord.dev";
|
|
797
805
|
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
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 {}
|
|
801
814
|
}
|
|
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
815
|
return null;
|
|
807
816
|
};
|
|
808
817
|
const flagVal = (name, def = "") => {
|
|
@@ -812,9 +821,9 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
|
812
821
|
if (i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("-")) return process.argv[i + 1];
|
|
813
822
|
return def;
|
|
814
823
|
};
|
|
815
|
-
const
|
|
816
|
-
const m =
|
|
817
|
-
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); }
|
|
818
827
|
return m;
|
|
819
828
|
};
|
|
820
829
|
const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
|
|
@@ -856,16 +865,16 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
|
856
865
|
});
|
|
857
866
|
};
|
|
858
867
|
|
|
859
|
-
if (cmd === "master") {
|
|
868
|
+
if (cmd === "main" || cmd === "master") {
|
|
860
869
|
const sub = process.argv[3];
|
|
861
870
|
if (sub === "connect") {
|
|
862
871
|
const harness = flagVal("harness", flagVal("tool", ""));
|
|
863
872
|
const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/master/session" -H "Content-Type: application/json" -d '{"harness":"${harness}"}'`);
|
|
864
873
|
let sessionId = "";
|
|
865
874
|
try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
|
|
866
|
-
if (!sessionId) { console.error("Could not start
|
|
875
|
+
if (!sessionId) { console.error("Could not start main-auth session."); process.exit(1); }
|
|
867
876
|
const url = `https://patchcord.dev/master?session=${sessionId}`;
|
|
868
|
-
console.log(`\n Authorize the
|
|
877
|
+
console.log(`\n Authorize the main agent in your browser:\n ${M.cyan}${url}${M.rst}\n`);
|
|
869
878
|
console.log(` ${M.dim}Waiting for authorization...${M.rst}`);
|
|
870
879
|
const http = await import("http");
|
|
871
880
|
const https = await import("https");
|
|
@@ -888,24 +897,24 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
|
888
897
|
setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
|
|
889
898
|
});
|
|
890
899
|
if (!token) { console.error("\n Authorization timed out or failed."); process.exit(1); }
|
|
891
|
-
mkdirSync(dirname(
|
|
892
|
-
writeFileSync(
|
|
893
|
-
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}`);
|
|
894
903
|
process.exit(0);
|
|
895
904
|
}
|
|
896
905
|
if (sub === "whoami") {
|
|
897
|
-
const m =
|
|
906
|
+
const m = requireMain();
|
|
898
907
|
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/master/whoami`, m.token);
|
|
899
|
-
if (status !== "200" || !json) { console.error(`
|
|
900
|
-
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`);
|
|
901
910
|
process.exit(0);
|
|
902
911
|
}
|
|
903
|
-
console.error("Usage: patchcord
|
|
912
|
+
console.error("Usage: patchcord main <connect|whoami>");
|
|
904
913
|
process.exit(1);
|
|
905
914
|
}
|
|
906
915
|
|
|
907
916
|
if (cmd === "provision") {
|
|
908
|
-
const m =
|
|
917
|
+
const m = requireMain();
|
|
909
918
|
const arg = process.argv[3];
|
|
910
919
|
if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
|
|
911
920
|
if (arg === "revoke") {
|
|
@@ -922,7 +931,7 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
|
922
931
|
const ns = flagVal("namespace");
|
|
923
932
|
const subdir = flagVal("dir", arg);
|
|
924
933
|
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: `
|
|
934
|
+
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: arg, tool, role, label: `main:${tool}` });
|
|
926
935
|
if (status !== "200" || !json?.token) { console.error(`provision failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
927
936
|
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
928
937
|
const dir = join(process.cwd(), subdir);
|
|
@@ -931,6 +940,16 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
|
931
940
|
if (role) {
|
|
932
941
|
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
942
|
}
|
|
943
|
+
// Record in the local team manifest so `team launch` / `team status` see it.
|
|
944
|
+
try {
|
|
945
|
+
const tj = join(process.cwd(), ".patchcord", "team.json");
|
|
946
|
+
if (existsSync(tj)) {
|
|
947
|
+
const man = JSON.parse(readFileSync(tj, "utf-8"));
|
|
948
|
+
man.agents = (man.agents || []).filter((a) => a.agent !== arg);
|
|
949
|
+
man.agents.push({ agent: arg, tool, role, dir: subdir, namespace: ns });
|
|
950
|
+
writeFileSync(tj, JSON.stringify(man, null, 2) + "\n");
|
|
951
|
+
}
|
|
952
|
+
} catch {}
|
|
934
953
|
console.log(`✓ provisioned ${M.green}${ns}:${arg}${M.rst} [${tool}${role ? "/" + role : ""}] → ${dir}`);
|
|
935
954
|
process.exit(0);
|
|
936
955
|
}
|
|
@@ -939,17 +958,39 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
|
939
958
|
const sub = process.argv[3];
|
|
940
959
|
if (sub === "init") {
|
|
941
960
|
const project = process.argv[4];
|
|
942
|
-
if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns]"); process.exit(1); }
|
|
961
|
+
if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns] [--tool <main-harness>]"); process.exit(1); }
|
|
943
962
|
const ns = flagVal("namespace", project.replace(/[^a-z0-9-]/gi, "-").toLowerCase());
|
|
963
|
+
const mainTool = flagVal("tool", "claude_code");
|
|
944
964
|
const root = join(process.cwd(), project);
|
|
945
965
|
mkdirSync(join(root, ".patchcord"), { recursive: true });
|
|
946
|
-
|
|
947
|
-
|
|
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();
|
|
972
|
+
if (m) {
|
|
973
|
+
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
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" });
|
|
975
|
+
if (status === "200" && json?.token) {
|
|
976
|
+
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
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}`);
|
|
980
|
+
} else {
|
|
981
|
+
console.log(`⚠ could not provision main identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
|
|
982
|
+
}
|
|
983
|
+
} else {
|
|
984
|
+
console.log(`⚠ no main token — run 'patchcord main connect' first, then re-run team init`);
|
|
985
|
+
}
|
|
986
|
+
writeFileSync(join(root, ".patchcord", "team.json"), JSON.stringify(manifest, null, 2) + "\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`);
|
|
948
988
|
console.log(`✓ team scaffolded: ${root} (namespace ${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}`);
|
|
949
990
|
process.exit(0);
|
|
950
991
|
}
|
|
951
992
|
if (sub === "list") {
|
|
952
|
-
const m =
|
|
993
|
+
const m = requireMain();
|
|
953
994
|
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
|
|
954
995
|
if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
|
|
955
996
|
const agents = json?.agents || [];
|
|
@@ -957,20 +998,140 @@ if (cmd === "master" || cmd === "provision" || cmd === "team") {
|
|
|
957
998
|
if (!agents.length) console.log(" (no agents)");
|
|
958
999
|
process.exit(0);
|
|
959
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
|
+
}
|
|
960
1107
|
if (sub === "launch") {
|
|
961
1108
|
let manifest = null;
|
|
962
1109
|
try { manifest = JSON.parse(readFileSync(join(process.cwd(), ".patchcord", "team.json"), "utf-8")); } catch {}
|
|
963
1110
|
if (!manifest) { console.error("No .patchcord/team.json here — cd into the project root."); process.exit(1); }
|
|
964
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
|
+
}
|
|
965
1122
|
for (const a of (manifest.agents || [])) {
|
|
966
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
|
+
}
|
|
967
1128
|
const muxTool = a.tool === "claude_code" ? "claude" : (a.tool === "kimi-code" ? "kimi" : a.tool);
|
|
968
1129
|
console.log(` launching ${a.agent} (${muxTool}) in ${dir}`);
|
|
969
1130
|
spawnSync("mux", ["new", muxTool, "--dir", dir], { stdio: "inherit" });
|
|
970
1131
|
}
|
|
971
1132
|
process.exit(0);
|
|
972
1133
|
}
|
|
973
|
-
console.error("Usage: patchcord team <init|list|launch>");
|
|
1134
|
+
console.error("Usage: patchcord team <init|list|launch|status>");
|
|
974
1135
|
process.exit(1);
|
|
975
1136
|
}
|
|
976
1137
|
}
|