patchcord 0.5.99 → 0.5.102
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
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
|
-
const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/
|
|
872
|
+
const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/main/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 =
|
|
904
|
-
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/
|
|
905
|
-
if (status !== "200" || !json) { console.error(`
|
|
906
|
-
console.log(`
|
|
906
|
+
const m = requireMain();
|
|
907
|
+
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/main/whoami`, m.token);
|
|
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
|
}
|
|
@@ -1335,20 +1458,19 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1335
1458
|
// Kimi CLI
|
|
1336
1459
|
const hasKimi = run("which kimi");
|
|
1337
1460
|
if (hasKimi) {
|
|
1338
|
-
// Clean up old skill names
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1351
|
-
const kimiSubDir = join(HOME, ".kimi", "skills", "patchcord:subscribe");
|
|
1461
|
+
// Clean up old skill names: the combined skill AND the colon-named flow
|
|
1462
|
+
// skills (patchcord:inbox …). Kimi requires skill names to be
|
|
1463
|
+
// lowercase/digits/hyphens only — a colon kept them out of /flow:, so they
|
|
1464
|
+
// showed only under /skill:. Renamed to patchcord-inbox etc.
|
|
1465
|
+
for (const stale of ["patchcord", "patchcord-wait", "patchcord:inbox", "patchcord:wait", "patchcord:subscribe"]) {
|
|
1466
|
+
const d = join(HOME, ".kimi", "skills", stale);
|
|
1467
|
+
if (existsSync(d)) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Install three focused Kimi flow skills (hyphen names → /flow:patchcord-inbox)
|
|
1471
|
+
const kimiInboxDir = join(HOME, ".kimi", "skills", "patchcord-inbox");
|
|
1472
|
+
const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord-wait");
|
|
1473
|
+
const kimiSubDir = join(HOME, ".kimi", "skills", "patchcord-subscribe");
|
|
1352
1474
|
let kimiChanged = false;
|
|
1353
1475
|
|
|
1354
1476
|
const installKimiSkill = (destDir, relSrc) => {
|
|
@@ -2210,14 +2332,20 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2210
2332
|
console.log(`\n ${green}✓${r} Kimi Code configured: ${dim}${kcPath}${r}`);
|
|
2211
2333
|
console.log(` ${dim}Kimi Code auto-loads project-local .kimi-code/mcp.json — just run ${r}${bold}kimi${r}${dim} in this project (no wrapper needed).${r}`);
|
|
2212
2334
|
}
|
|
2213
|
-
// Skills → user-level Kimi Code skills dir
|
|
2335
|
+
// Skills → user-level Kimi Code skills dir. Hyphen names so they register
|
|
2336
|
+
// as /flow:patchcord-inbox (a colon kept them out of /flow:). Remove any
|
|
2337
|
+
// stale colon-named dirs from prior installs.
|
|
2214
2338
|
try {
|
|
2215
|
-
for (const
|
|
2339
|
+
for (const stale of ["patchcord:inbox", "patchcord:wait", "patchcord:subscribe", "patchcord"]) {
|
|
2340
|
+
const d = join(kimiCodeHome, "skills", stale);
|
|
2341
|
+
if (existsSync(d)) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
2342
|
+
}
|
|
2343
|
+
for (const [label, rel] of [["patchcord-inbox", "inbox"], ["patchcord-wait", "wait"], ["patchcord-subscribe", "subscribe"]]) {
|
|
2216
2344
|
const dest = join(kimiCodeHome, "skills", label);
|
|
2217
2345
|
mkdirSync(dest, { recursive: true });
|
|
2218
2346
|
cpSync(join(pluginRoot, "per-project-skills", "kimi", rel, "SKILL.md"), join(dest, "SKILL.md"));
|
|
2219
2347
|
}
|
|
2220
|
-
console.log(` ${green}✓${r} Skills installed: ${dim}${join(kimiCodeHome, "skills")}${r}`);
|
|
2348
|
+
console.log(` ${green}✓${r} Skills installed: ${dim}${join(kimiCodeHome, "skills")}${r} ${dim}(/flow:patchcord-inbox)${r}`);
|
|
2221
2349
|
} catch {}
|
|
2222
2350
|
} else if (isKimi) {
|
|
2223
2351
|
// Kimi CLI (Python): per-project .kimi/mcp.json + shell wrapper for --mcp-config-file
|
|
@@ -2237,22 +2365,22 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2237
2365
|
console.log(`\n ${green}✓${r} Kimi CLI configured: ${dim}${kimiPath}${r}`);
|
|
2238
2366
|
console.log(` ${dim}Per-project — use the kimi-pc wrapper (see below) so Kimi loads this config.${r}`);
|
|
2239
2367
|
}
|
|
2240
|
-
// Install/update global skills
|
|
2241
|
-
|
|
2242
|
-
const
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
const kimiInboxDir = join(HOME, ".kimi", "skills", "patchcord
|
|
2247
|
-
const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord
|
|
2248
|
-
const kimiSubDir = join(HOME, ".kimi", "skills", "patchcord
|
|
2368
|
+
// Install/update global skills. Hyphen names → /flow:patchcord-inbox
|
|
2369
|
+
// (colon names never registered as flows). Remove old combined + colon dirs.
|
|
2370
|
+
for (const stale of ["patchcord", "patchcord-wait", "patchcord:inbox", "patchcord:wait", "patchcord:subscribe"]) {
|
|
2371
|
+
const d = join(HOME, ".kimi", "skills", stale);
|
|
2372
|
+
if (existsSync(d)) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
2373
|
+
}
|
|
2374
|
+
const kimiInboxDir = join(HOME, ".kimi", "skills", "patchcord-inbox");
|
|
2375
|
+
const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord-wait");
|
|
2376
|
+
const kimiSubDir = join(HOME, ".kimi", "skills", "patchcord-subscribe");
|
|
2249
2377
|
mkdirSync(kimiInboxDir, { recursive: true });
|
|
2250
2378
|
mkdirSync(kimiWaitDir, { recursive: true });
|
|
2251
2379
|
mkdirSync(kimiSubDir, { recursive: true });
|
|
2252
2380
|
cpSync(join(pluginRoot, "per-project-skills", "kimi", "inbox", "SKILL.md"), join(kimiInboxDir, "SKILL.md"));
|
|
2253
2381
|
cpSync(join(pluginRoot, "per-project-skills", "kimi", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
|
|
2254
2382
|
cpSync(join(pluginRoot, "per-project-skills", "kimi", "subscribe", "SKILL.md"), join(kimiSubDir, "SKILL.md"));
|
|
2255
|
-
console.log(` ${green}✓${r} Skills installed: ${dim}patchcord
|
|
2383
|
+
console.log(` ${green}✓${r} Skills installed: ${dim}/flow:patchcord-inbox${r}, ${dim}-wait${r}, ${dim}-subscribe${r}`);
|
|
2256
2384
|
|
|
2257
2385
|
// Install alias for per-project --mcp-config-file
|
|
2258
2386
|
const aliasLine = `alias kimi-pc='kimi --mcp-config-file .kimi/mcp.json'`;
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: patchcord
|
|
2
|
+
name: patchcord-inbox
|
|
3
3
|
description: Read Patchcord inbox and reply to messages
|
|
4
4
|
type: flow
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
> Use `/flow:patchcord
|
|
7
|
+
> Use `/flow:patchcord-inbox` to run this. `/skill:` only loads it as docs (Kimi design).
|
|
8
8
|
|
|
9
9
|
```mermaid
|
|
10
10
|
flowchart TD
|
|
11
|
-
A([BEGIN]) --> B[Call the inbox MCP tool. Reading the response: the first header line shows YOUR own identity (the recipient). The actual sender of each message is
|
|
12
|
-
B --> C[Call the Shell tool with these exact parameters to re-arm the listener: command=
|
|
11
|
+
A([BEGIN]) --> B["Call the inbox MCP tool. Reading the response: the first header line shows YOUR own identity (the recipient). The actual sender of each message is on a 'From X' line — never confuse the header with the sender. For each pending message, classify it: (1) ACK — short signals like thanks, noted, works, great, ok, 👍 with no task. Close silently with reply(message_id, resolve=true) and NO content. Never send a text reply to an ack — it creates infinite ack chains. (2) BLOCKED — you cannot do the work right now (busy, missing credentials, ambiguous target). Use reply(message_id, reason, defer=true) — the message stays in your inbox as a reminder. Never silently skip a message. (3) ACTIONABLE — do the work the message asks for first (edit the file, run the command, write the code), THEN call reply(message_id, concrete summary) with file paths and line numbers. Never reply will-do or understood before doing the work."]
|
|
12
|
+
B --> C["Call the Shell tool with these exact parameters to re-arm the listener: command=patchcord subscribe --kimi 5, run_in_background=true, timeout=86400, description=Patchcord listener."]
|
|
13
13
|
C --> D([END])
|
|
14
14
|
```
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: patchcord
|
|
2
|
+
name: patchcord-subscribe
|
|
3
3
|
description: Start the Patchcord background polling listener
|
|
4
4
|
type: flow
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
> Use `/flow:patchcord
|
|
7
|
+
> Use `/flow:patchcord-subscribe` to run this. `/skill:` only loads it as docs (Kimi design).
|
|
8
8
|
|
|
9
9
|
```mermaid
|
|
10
10
|
flowchart TD
|
|
11
|
-
A([BEGIN]) --> B[First, call the inbox MCP tool. Reading the response: the first header line shows YOUR own identity (the recipient). The actual sender of each message is
|
|
12
|
-
B --> C[Now call the Shell tool with these exact parameters: command=
|
|
11
|
+
A([BEGIN]) --> B["First, call the inbox MCP tool. Reading the response: the first header line shows YOUR own identity (the recipient). The actual sender of each message is on a 'From X' line — never confuse the header with the sender. Drain each pending message by classifying it: ACK (thanks, noted, works, ok, 👍) → close silently with reply(message_id, resolve=true), no content; BLOCKED (you cannot do the work right now) → reply(message_id, reason, defer=true); ACTIONABLE → do the work first, then reply(message_id, concrete summary). Do NOT skip the drain — the listener exits immediately if messages are already waiting."]
|
|
12
|
+
B --> C["Now call the Shell tool with these exact parameters: command=patchcord subscribe --kimi 5, run_in_background=true, timeout=86400, description=Patchcord listener. Then tell the user in one sentence: Patchcord listener active."]
|
|
13
13
|
C --> D([END])
|
|
14
14
|
```
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: patchcord
|
|
2
|
+
name: patchcord-wait
|
|
3
3
|
description: Wait for one incoming Patchcord message
|
|
4
4
|
type: flow
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
> Use `/flow:patchcord
|
|
7
|
+
> Use `/flow:patchcord-wait` to run this. `/skill:` only loads it as docs (Kimi design).
|
|
8
8
|
|
|
9
9
|
```mermaid
|
|
10
10
|
flowchart TD
|
|
11
|
-
A([BEGIN]) --> B[Call the wait_for_message MCP tool to block until a message arrives or 5 minutes elapse.]
|
|
12
|
-
B --> C{Message arrived?}
|
|
13
|
-
C -->|Yes| D[Classify the message: ACK (
|
|
11
|
+
A([BEGIN]) --> B["Call the wait_for_message MCP tool to block until a message arrives or 5 minutes elapse."]
|
|
12
|
+
B --> C{"Message arrived?"}
|
|
13
|
+
C -->|Yes| D["Classify the message: ACK (thanks, noted, works, ok, 👍) → close silently with reply(message_id, resolve=true), no content; BLOCKED (cannot do work right now) → reply(message_id, reason, defer=true); ACTIONABLE → do the work first, then reply(message_id, concrete summary). Never reply to an ack with text."]
|
|
14
14
|
C -->|No| E([END])
|
|
15
|
-
D --> F[Call the Shell tool with these exact parameters to re-arm the listener: command=
|
|
15
|
+
D --> F["Call the Shell tool with these exact parameters to re-arm the listener: command=patchcord subscribe --kimi 5, run_in_background=true, timeout=86400, description=Patchcord listener."]
|
|
16
16
|
F --> E
|
|
17
17
|
```
|