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
- // ── 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
- const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/master/session" -H "Content-Type: application/json" -d '{"harness":"${harness}"}'`);
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 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();
904
- const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/master/whoami`, m.token);
905
- if (status !== "200" || !json) { console.error(`master whoami failed (HTTP ${status})`); process.exit(1); }
906
- console.log(`master · user ${json.user_id} · ${json.agents}/${json.quota} agents`);
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 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
  }
@@ -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 (hyphen-style combined skill)
1339
- const oldKimiSkillDir = join(HOME, ".kimi", "skills", "patchcord");
1340
- const oldKimiWaitDir = join(HOME, ".kimi", "skills", "patchcord-wait");
1341
- if (existsSync(oldKimiSkillDir)) {
1342
- rmSync(oldKimiSkillDir, { recursive: true, force: true });
1343
- }
1344
- if (existsSync(oldKimiWaitDir)) {
1345
- rmSync(oldKimiWaitDir, { recursive: true, force: true });
1346
- }
1347
-
1348
- // Install three focused Kimi skills
1349
- const kimiInboxDir = join(HOME, ".kimi", "skills", "patchcord:inbox");
1350
- const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord:wait");
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 [label, rel] of [["patchcord:inbox", "inbox"], ["patchcord:wait", "wait"], ["patchcord:subscribe", "subscribe"]]) {
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 (remove old combined skill, install three focused ones)
2241
- const oldKimiSkillDir = join(HOME, ".kimi", "skills", "patchcord");
2242
- const oldKimiWaitDir = join(HOME, ".kimi", "skills", "patchcord-wait");
2243
- if (existsSync(oldKimiSkillDir)) rmSync(oldKimiSkillDir, { recursive: true, force: true });
2244
- if (existsSync(oldKimiWaitDir)) rmSync(oldKimiWaitDir, { recursive: true, force: true });
2245
-
2246
- const kimiInboxDir = join(HOME, ".kimi", "skills", "patchcord:inbox");
2247
- const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord:wait");
2248
- const kimiSubDir = join(HOME, ".kimi", "skills", "patchcord:subscribe");
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:inbox${r}, ${dim}patchcord:wait${r}, ${dim}patchcord:subscribe${r}`);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.99",
3
+ "version": "0.5.102",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -1,14 +1,14 @@
1
1
  ---
2
- name: patchcord:inbox
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:inbox` to run this. `/skill:` only loads it as docs (Kimi design).
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 shown 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', 'good', '👍', '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 of what you did') with file paths and line numbers. Never reply 'will do' / 'understood' / 'ready' 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'.]
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:subscribe
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:subscribe` to run this. `/skill:` only loads it as docs (Kimi design).
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 shown 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.]
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:wait
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:wait` to run this. `/skill:` only loads it as docs (Kimi design).
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 ('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.]
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='patchcord subscribe --kimi 5', run_in_background=true, timeout=86400, description='Patchcord listener'.]
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
  ```