patchcord 0.5.117 → 0.5.119

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.
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "description": "Cross-machine agent messaging with push delivery. Messages from other agents arrive as native channel notifications.",
4
- "version": "0.5.14",
3
+ "description": "Cross-machine agent messaging. Messages from other agents arrive via inbox with Stop-hook wake-up.",
4
+ "version": "0.5.119",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
8
8
  "repository": "https://github.com/ppravdin/patchcord",
9
- "skills": "./skills/",
10
- "channel": {
11
- "server": "./channel/server.ts"
12
- }
9
+ "skills": "./skills/"
13
10
  }
package/bin/patchcord.mjs CHANGED
@@ -66,12 +66,20 @@ function detectFolder(dir) {
66
66
  }
67
67
 
68
68
 
69
- if (cmd === "help" || cmd === "--help" || cmd === "-h") {
69
+ // A trailing --help/-h on ANY subcommand must show help and exit never run the
70
+ // command. (patchcord orchestrator --help used to PROVISION an orchestrator;
71
+ // patchcord login --help hung on the browser login. Side-effecting --help is a
72
+ // footgun.) We short-circuit here, before any account/provision/login dispatch.
73
+ if (
74
+ cmd === "help" || cmd === "--help" || cmd === "-h" ||
75
+ process.argv.slice(3).some((a) => a === "--help" || a === "-h")
76
+ ) {
70
77
  console.log(`patchcord — cross-agent messaging + team orchestration for AI coding agents
71
78
 
72
79
  Two layers: THIS AGENT (a per-project identity, by bearer token) and your ACCOUNT
73
- (user-level; provisions agents, runs teams + schedules). Account/team/schedule
74
- commands authenticate the CLI in your browser automatically on first use.
80
+ (user-level; provisions agents, runs teams + schedules). Run \`patchcord login\`
81
+ once to authenticate the CLI; account/team/schedule commands then use that token
82
+ (they never open a browser on their own).
75
83
 
76
84
  SETUP (per agent / project)
77
85
  patchcord Set up this project's agent (browser)
@@ -341,8 +349,12 @@ async function _resolveBearer(options = {}) {
341
349
  );
342
350
  })();
343
351
 
344
- const kimiGlobalReader = () => readJsonAt(join(HOME, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
345
- const kimiCodeGlobalReader = () => readJsonAt(join(process.env.KIMI_CODE_HOME || join(HOME, ".kimi-code"), "mcp.json"), ["mcpServers", "patchcord"], "kimi");
352
+ // NO global kimi/kimi-code reader. Kimi is a PER-PROJECT identity
353
+ // (.kimi-code/mcp.json in the worktree); a global ~/.kimi(-code)/mcp.json would
354
+ // make a leftover token hijack every kimi session everywhere. A kimi outside a
355
+ // patchcord worktree is simply not an agent. (The tools below are global-ONLY
356
+ // by nature — Windsurf/Gemini/Zed/etc. store MCP config globally — so they keep
357
+ // their global readers.)
346
358
  const defaultGlobalCandidates = [
347
359
  () => readJsonAt(join(HOME, ".codeium", "windsurf", "mcp_config.json"), ["mcpServers", "patchcord"], "windsurf"),
348
360
  () => readJsonAt(join(HOME, ".gemini", "settings.json"), ["mcpServers", "patchcord"], "gemini"),
@@ -352,9 +364,7 @@ async function _resolveBearer(options = {}) {
352
364
  () => readHermesShape(join(HOME, ".hermes", "config.yaml")),
353
365
  ...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
354
366
  ];
355
- const globalCandidates = preferKimi
356
- ? [kimiGlobalReader, kimiCodeGlobalReader, ...defaultGlobalCandidates]
357
- : [...defaultGlobalCandidates, kimiGlobalReader, kimiCodeGlobalReader];
367
+ const globalCandidates = defaultGlobalCandidates;
358
368
  for (const r of globalCandidates) {
359
369
  const found = r();
360
370
  if (found) { found.scope = "global"; return found; }
@@ -902,7 +912,19 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
902
912
  return { token, baseUrl: DEFAULT_API };
903
913
  };
904
914
  // Auth on demand: any command needing the account logs in if not already.
905
- const requireAuth = async () => readAuth() || await doLogin();
915
+ const requireAuth = async () => {
916
+ const a = readAuth();
917
+ if (a) return a;
918
+ // No account token. NEVER auto-drop into the interactive browser login here:
919
+ // doLogin() prints a URL and BLOCKS up to 15 minutes for a browser that never
920
+ // comes in a headless/agent context (the orchestrator misread that hang as
921
+ // "provision produced no output"). TTY detection is unreliable (a pty/agent
922
+ // shell can report isTTY=true), so we fail fast unconditionally. Only the
923
+ // explicit `patchcord login` command opens a browser.
924
+ console.error("No patchcord account token — no pcm- token in ~/.patchcord/auth.json or $PATCHCORD_TOKEN.");
925
+ console.error("Run `patchcord login` once (interactive), or set $PATCHCORD_TOKEN. Cannot continue.");
926
+ process.exit(1);
927
+ };
906
928
  // Account-authed request with stale-token recovery. If the server rejects the
907
929
  // stored token (401/403), forget it, re-log-in once, and retry. Returns the
908
930
  // _httpJSON result plus the auth used (for callers that need m.baseUrl).
@@ -910,9 +932,11 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
910
932
  let m = await requireAuth();
911
933
  let r = await _httpJSON(method, `${m.baseUrl}${path}`, m.token, body);
912
934
  if (r.status === "401" || r.status === "403") {
935
+ // Stale/invalid account token. Forget it and fail fast — do NOT auto-relogin
936
+ // (would hang headless, see requireAuth). The user re-runs `patchcord login`.
913
937
  forgetAuth();
914
- m = await doLogin();
915
- r = await _httpJSON(method, `${m.baseUrl}${path}`, m.token, body);
938
+ console.error(`patchcord account token rejected (HTTP ${r.status}). Run \`patchcord login\` again. Cannot continue.`);
939
+ process.exit(1);
916
940
  }
917
941
  return { ...r, m };
918
942
  };
@@ -957,6 +981,31 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
957
981
  o.mcpServers.patchcord = { serverUrl: `${baseUrl}/mcp`, headers: hdr };
958
982
  });
959
983
  }
984
+ if (tool === "hermes") {
985
+ // Hermes reads MCP servers ONLY from its GLOBAL ~/.hermes/config.yaml
986
+ // (mcp_servers key) — it ignores a project-local .mcp.json. So unlike the
987
+ // other tools we cannot write into `dir`; we upsert the global config,
988
+ // mirroring the `npx patchcord` installer's Hermes path. NOTE: global =
989
+ // one patchcord identity per machine, so provisioning a second hermes
990
+ // worker in another namespace overwrites the first's token. Single-hermes
991
+ // teams only until Hermes gains project-scoped MCP config.
992
+ const hermesPath = join(HOME, ".hermes", "config.yaml");
993
+ mkdirSync(dirname(hermesPath), { recursive: true });
994
+ let existingYaml = "";
995
+ try { existingYaml = existsSync(hermesPath) ? readFileSync(hermesPath, "utf-8") : ""; } catch {}
996
+ writeFileSync(hermesPath, upsertHermesConfig(existingYaml, `${baseUrl}/mcp`, token));
997
+ // Install the Hermes patchcord skills (inbox/subscribe/wait) so the worker
998
+ // knows the messaging flow, same as the installer does.
999
+ try {
1000
+ const hermesSkillsSrc = join(pluginRoot, "per-project-skills", "hermes");
1001
+ if (existsSync(hermesSkillsSrc)) {
1002
+ const hermesSkillsDest = join(HOME, ".hermes", "skills", "integrations");
1003
+ mkdirSync(hermesSkillsDest, { recursive: true });
1004
+ cpSync(hermesSkillsSrc, hermesSkillsDest, { recursive: true });
1005
+ }
1006
+ } catch {}
1007
+ return hermesPath;
1008
+ }
960
1009
  // default: claude_code. type:"http" is REQUIRED — without it Claude Code
961
1010
  // defaults to stdio transport and rejects the entry ("command: expected
962
1011
  // string, received undefined").
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.117",
3
+ "version": "0.5.119",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
+ "scripts": {
6
+ "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json"
7
+ },
5
8
  "author": "ppravdin",
6
9
  "license": "MIT",
7
10
  "repository": {
@@ -18,23 +18,28 @@ INPUT=$(cat)
18
18
  STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
19
19
  [ "$STOP_ACTIVE" = "true" ] && exit 0
20
20
 
21
- # Resolve MCP config: per-project first, then global
21
+ # Resolve MCP config: PROJECT-scoped ONLY (walk up from cwd for .kimi/mcp.json).
22
+ # NO global ~/.kimi/mcp.json fallback — a Kimi launched outside a patchcord
23
+ # worktree is not that agent and must not be nagged. No MCP authed in the cwd
24
+ # tree => no inbox check. (This Stop hook is registered globally in
25
+ # ~/.kimi/config.toml, so the project-scoped token is the ONLY thing that should
26
+ # make it act.)
22
27
  KIMI_MCP=""
23
28
  CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || echo "")
24
29
 
25
30
  if [ -n "$CWD" ]; then
26
31
  dir="$CWD"
27
32
  while [ "$dir" != "/" ]; do
28
- if [ -f "$dir/.kimi/mcp.json" ]; then
29
- KIMI_MCP="$dir/.kimi/mcp.json"
33
+ if [ -f "$dir/.kimi-code/mcp.json" ]; then
34
+ KIMI_MCP="$dir/.kimi-code/mcp.json"
30
35
  break
31
36
  fi
32
37
  dir=$(dirname "$dir")
33
38
  done
34
39
  fi
35
40
 
36
- # Fallback to global config
37
- [ -z "$KIMI_MCP" ] && KIMI_MCP="${HOME}/.kimi/mcp.json"
41
+ # No project-scoped .kimi-code/mcp.json in the cwd tree => not a patchcord agent here.
42
+ [ -z "$KIMI_MCP" ] && exit 0
38
43
 
39
44
  TOKEN=""
40
45
  URL=""
@@ -14,25 +14,26 @@ set -euo pipefail
14
14
 
15
15
  command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 1; }
16
16
 
17
- # Resolve MCP config: per-project first, then global
17
+ # Resolve MCP config: PROJECT-scoped ONLY (walk up from cwd for .kimi-code/mcp.json).
18
+ # Kimi Code (the current product) uses .kimi-code/ — the legacy .kimi/ path is
19
+ # deprecated and MUST NOT be used; it also mismatched where `patchcord provision`
20
+ # writes the config (.kimi-code/mcp.json), so the listener never found it.
21
+ # NO global fallback: auto-launched by the (global) SessionStart hook on EVERY
22
+ # Kimi session, so a global fallback would start a listener for some leftover
23
+ # identity in every folder. No project config in the cwd tree => no patchcord
24
+ # agent here => exit quietly (0, not 1: must not spam stderr in non-patchcord dirs).
18
25
  KIMI_MCP=""
19
26
  dir="$PWD"
20
27
  while [ "$dir" != "/" ]; do
21
- if [ -f "$dir/.kimi/mcp.json" ]; then
22
- KIMI_MCP="$dir/.kimi/mcp.json"
28
+ if [ -f "$dir/.kimi-code/mcp.json" ]; then
29
+ KIMI_MCP="$dir/.kimi-code/mcp.json"
23
30
  break
24
31
  fi
25
32
  dir=$(dirname "$dir")
26
33
  done
27
34
 
28
- # Fallback to global config
29
- if [ -z "$KIMI_MCP" ]; then
30
- KIMI_MCP="${HOME}/.kimi/mcp.json"
31
- fi
32
-
33
- if [ ! -f "$KIMI_MCP" ]; then
34
- echo "Kimi MCP config not found at $KIMI_MCP or any parent — run npx patchcord@latest first" >&2
35
- exit 1
35
+ if [ -z "$KIMI_MCP" ] || [ ! -f "$KIMI_MCP" ]; then
36
+ exit 0
36
37
  fi
37
38
 
38
39
  TOKEN=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$KIMI_MCP" 2>/dev/null | sed 's/^Bearer //i' || true)
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // Keep .claude-plugin/plugin.json's version in lockstep with package.json.
3
+ //
4
+ // Runs from the npm "version" lifecycle (see package.json scripts.version), so
5
+ // every `npm version <x>` rewrites the plugin manifest and stages it in the same
6
+ // commit. This prevents the drift that shipped a 0.5.14 manifest inside a 0.5.118
7
+ // package — which, combined with a manifest field pointing at an unshipped file,
8
+ // crashed every interactive Claude Code startup.
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { dirname, join } from "node:path";
12
+
13
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
14
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
15
+ const manifestPath = join(root, ".claude-plugin", "plugin.json");
16
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
17
+
18
+ if (manifest.version === pkg.version) {
19
+ process.exit(0);
20
+ }
21
+
22
+ manifest.version = pkg.version;
23
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
24
+ process.stderr.write(`synced plugin.json version -> ${pkg.version}\n`);