patchcord 0.5.133 → 0.6.0

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,7 +1,7 @@
1
1
  {
2
2
  "name": "patchcord",
3
3
  "description": "Cross-machine agent messaging. Messages from other agents land in the inbox and wake the agent to reply.",
4
- "version": "0.5.133",
4
+ "version": "0.6.0",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/README.md CHANGED
@@ -8,7 +8,7 @@ Cross-machine messaging between AI coding agents.
8
8
  npx patchcord@latest
9
9
  ```
10
10
 
11
- One command. Opens browser, configures everything. Works with Claude Code, Codex CLI, Cursor, Windsurf, Gemini CLI, VS Code, Zed, OpenCode, Kimi CLI.
11
+ One command. Opens browser, configures everything. Works with Claude Code, Codex CLI, Cursor, Grok CLI, Windsurf, Gemini CLI, VS Code, Zed, OpenCode, Kimi CLI.
12
12
 
13
13
  Self-hosted:
14
14
 
@@ -26,6 +26,10 @@ npx patchcord@latest --token <token> --server https://patchcord.yourdomain.com
26
26
  - **Cursor CLI** — bearer-only MCP endpoint, `patchcord:*` pre-allowed in
27
27
  `~/.cursor/permissions.json`, and inbox/wait skills installed when
28
28
  `cursor-agent` is detected
29
+ - **Cursor subscribe** — `/patchcord:subscribe` starts a background Shell
30
+ listener; `^PATCHCORD:` output wakes the interactive agent
31
+ - **Grok CLI** — project-scoped `.grok/config.toml` plus inbox/subscribe/wait
32
+ skills; `/patchcord-subscribe` uses persistent `monitor` + WebSocket wakeup
29
33
  - **Kimi CLI** — global MCP config plus skills in `~/.kimi/skills/`
30
34
 
31
35
  ## How it works
package/bin/patchcord.mjs CHANGED
@@ -46,7 +46,7 @@ const PROJECT_MARKERS = [
46
46
  ".git", "package.json", "package-lock.json", "Cargo.toml", "go.mod", "go.sum",
47
47
  "pyproject.toml", "pom.xml", "build.gradle", "Makefile", "CMakeLists.txt",
48
48
  "Gemfile", "composer.json", "mix.exs", "requirements.txt", "setup.py",
49
- ".claude", ".codex", ".cursor", ".vscode", ".kimi", ".openclaw",
49
+ ".claude", ".codex", ".cursor", ".grok", ".vscode", ".kimi", ".openclaw",
50
50
  ];
51
51
 
52
52
  function detectFolder(dir) {
@@ -153,7 +153,7 @@ if (cmd === "plugin-path") {
153
153
  }
154
154
 
155
155
  // Shared bearer/base-url resolver. Project-local configs win over globals.
156
- // Supports all 12 installer targets: claude_code, codex, cursor, vscode,
156
+ // Supports all 13 installer targets: claude_code, codex, cursor, grok, vscode,
157
157
  // opencode (per-project) + windsurf, gemini, zed, openclaw, antigravity,
158
158
  // cline, kimi (global). Each tool stores the bearer in its own shape.
159
159
  function _kimiContextRequested(options = {}) {
@@ -224,6 +224,32 @@ function readHermesShape(path) {
224
224
  return { token, baseUrl: url.replace(/\/mcp(\/bearer)?$/, ""), configFile: path, tool: "hermes" };
225
225
  }
226
226
 
227
+ // Read url + bearer token from Grok CLI's project-scoped .grok/config.toml.
228
+ // Grok supports both nested and inline TOML headers, e.g.:
229
+ // [mcp_servers.patchcord.headers] / Authorization = "Bearer pct-..."
230
+ function readGrokTomlShape(path) {
231
+ if (!existsSync(path)) return null;
232
+ try {
233
+ const content = readFileSync(path, "utf-8");
234
+ const header = content.match(/^\[mcp_servers\.patchcord(?:[-\w]*)?\]\s*$/m);
235
+ if (!header) return null;
236
+ const rest = content.slice(header.index + header[0].length);
237
+ // Keep nested [mcp_servers.patchcord.headers] fields in the same server
238
+ // block, but stop at the next server or unrelated top-level TOML table.
239
+ const nextSection = rest.search(/^\[(?!mcp_servers\.patchcord(?:\.|[-\w]*\]))/m);
240
+ const block = nextSection === -1 ? rest : rest.slice(0, nextSection);
241
+ const urlMatch = block.match(/^\s*url\s*=\s*["']([^"']+)["']/m);
242
+ const tokenMatch = block.match(/["']?Authorization["']?\s*=\s*["']Bearer\s+([^"']+)["']/i);
243
+ if (!urlMatch || !tokenMatch) return null;
244
+ return {
245
+ token: tokenMatch[1],
246
+ baseUrl: urlMatch[1].replace(/\/mcp(\/bearer)?$/, ""),
247
+ configFile: path,
248
+ tool: "grok",
249
+ };
250
+ } catch { return null; }
251
+ }
252
+
227
253
  async function _resolveBearer(options = {}) {
228
254
  const { readFileSync } = await import("fs");
229
255
  const preferKimi = _kimiContextRequested(options);
@@ -324,6 +350,7 @@ async function _resolveBearer(options = {}) {
324
350
  (cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode"),
325
351
  (cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode"),
326
352
  (cwd) => readJsonAt(join(cwd, ".agents", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
353
+ (cwd) => readGrokTomlShape(join(cwd, ".grok", "config.toml")),
327
354
  (cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml")),
328
355
  ];
329
356
  const projectReaders = preferKimi
@@ -738,6 +765,7 @@ if (cmd === "subscribe") {
738
765
  vscode: (cwd) => readMcpJsonShape(join(cwd, ".vscode", "mcp.json"), "servers"),
739
766
  opencode: (cwd) => readOpenCodeShape(join(cwd, "opencode.json")),
740
767
  codex: (cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml")),
768
+ grok: (cwd) => readGrokTomlShape(join(cwd, ".grok", "config.toml")),
741
769
  };
742
770
 
743
771
  if (tool && !READERS[tool]) {
@@ -764,7 +792,7 @@ if (cmd === "subscribe") {
764
792
  if (!found) {
765
793
  const whichFile = tool
766
794
  ? `the ${tool} config file in or above ${cwd}`
767
- : `any supported tool config (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, opencode.json, .codex/config.toml) above ${cwd}`;
795
+ : `any supported tool config (.mcp.json, .cursor/mcp.json, .grok/config.toml, .vscode/mcp.json, opencode.json, .codex/config.toml) above ${cwd}`;
768
796
  console.error(`No patchcord bearer found in ${whichFile}. Run from the agent's project folder.`);
769
797
  process.exit(1);
770
798
  }
@@ -995,6 +1023,15 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
995
1023
  writeFileSync(p, ex);
996
1024
  return p;
997
1025
  }
1026
+ if (tool === "grok") {
1027
+ const gdir = join(dir, ".grok"); mkdirSync(gdir, { recursive: true });
1028
+ const p = join(gdir, "config.toml");
1029
+ let ex = existsSync(p) ? readFileSync(p, "utf-8") : "";
1030
+ ex = ex.replace(/\[mcp_servers\.patchcord(?:[-\w]*)?\][\s\S]*?(?=\n\[(?!mcp_servers\.patchcord(?:\.|[-\w]*\]))|$)/g, "").replace(/\n{3,}/g, "\n\n").trim();
1031
+ ex = (ex ? ex + "\n\n" : "") + `[mcp_servers.patchcord]\nurl = "${baseUrl}/mcp"\nenabled = true\n\n[mcp_servers.patchcord.headers]\nAuthorization = "Bearer ${token}"\nX-Patchcord-Machine = "${hostname}"\n`;
1032
+ writeFileSync(p, ex);
1033
+ return p;
1034
+ }
998
1035
  if (tool === "kimi" || tool === "kimi-code") {
999
1036
  const kdir = join(dir, ".kimi-code"); mkdirSync(kdir, { recursive: true });
1000
1037
  return writeJson(join(kdir, "mcp.json"), (o) => {
@@ -1297,7 +1334,7 @@ you design the team, provision its agents, launch them, and manage them.
1297
1334
  if (!manifest) { console.error("No .patchcord/team.json here — cd into the project root."); process.exit(1); }
1298
1335
  const ns = manifest.namespace;
1299
1336
  const real = (p) => run(`realpath -m ${JSON.stringify(p)}`) || p;
1300
- const AGENT_CMDS = /(^|\/)(claude|codex|kimi|kimi-code|opencode|gemini|agy|node)$/;
1337
+ const AGENT_CMDS = /(^|\/)(claude|codex|grok|kimi|kimi-code|opencode|gemini|agy|node)$/;
1301
1338
 
1302
1339
  // Token reader for a specific folder + tool (folder IS the identity).
1303
1340
  const tokenInDir = (tool, dir) => {
@@ -1312,6 +1349,9 @@ you design the team, provision its agents, launch them, and manage them.
1312
1349
  if (!u || !t) return null;
1313
1350
  return { token: t[1], baseUrl: u[1].replace(/\/mcp(\/bearer)?$/, "") };
1314
1351
  }
1352
+ if (tool === "grok") {
1353
+ return readGrokTomlShape(join(dir, ".grok", "config.toml"));
1354
+ }
1315
1355
  const map = {
1316
1356
  opencode: [join(dir, "opencode.json"), ["mcp", "patchcord"]],
1317
1357
  kimi: [join(dir, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"]],
@@ -1758,6 +1798,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1758
1798
  mkdirSync(cursorSkillsRoot, { recursive: true });
1759
1799
  const cursorSkillDir = join(cursorSkillsRoot, "patchcord");
1760
1800
  const cursorWaitDir = join(cursorSkillsRoot, "patchcord-wait");
1801
+ const cursorSubscribeDir = join(cursorSkillsRoot, "patchcord-subscribe");
1761
1802
  let cursorChanged = false;
1762
1803
  if (!existsSync(cursorSkillDir)) {
1763
1804
  mkdirSync(cursorSkillDir, { recursive: true });
@@ -1769,6 +1810,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1769
1810
  cpSync(join(pluginRoot, "skills", "wait", "SKILL.md"), join(cursorWaitDir, "SKILL.md"));
1770
1811
  cursorChanged = true;
1771
1812
  }
1813
+ if (!existsSync(cursorSubscribeDir)) {
1814
+ mkdirSync(cursorSubscribeDir, { recursive: true });
1815
+ cpSync(
1816
+ join(pluginRoot, "per-project-skills", "cursor", "subscribe", "SKILL.md"),
1817
+ join(cursorSubscribeDir, "SKILL.md")
1818
+ );
1819
+ cursorChanged = true;
1820
+ }
1772
1821
  if (cursorChanged) globalChanges.push("Cursor skills installed");
1773
1822
 
1774
1823
  if (hasCursorAgent) {
@@ -2144,7 +2193,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2144
2193
  let choice = "";
2145
2194
 
2146
2195
  const CLIENT_TYPE_MAP = {
2147
- "claude_code": "1", "codex": "2", "cursor": "3", "windsurf": "4",
2196
+ "claude_code": "1", "codex": "2", "cursor": "3", "grok": "14", "grok_cli": "14", "grok_build": "14", "windsurf": "4",
2148
2197
  "gemini": "5", "vscode": "6", "zed": "7", "opencode": "8", "openclaw": "9", "antigravity": "10",
2149
2198
  "cline": "11", "kimi": "12", "hermes": "13",
2150
2199
  };
@@ -2239,11 +2288,13 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2239
2288
  let existingConfigFile = "";
2240
2289
  const mcpJsonPath = join(cwd, ".mcp.json");
2241
2290
  const codexTomlPath = join(cwd, ".codex", "config.toml");
2291
+ const grokTomlPath = join(cwd, ".grok", "config.toml");
2242
2292
  const kimiJsonPath = join(cwd, ".kimi", "mcp.json");
2243
2293
 
2244
2294
  const slugForCheck = toolSlug ? toolSlug.replace(/-/g, "_") : "";
2245
2295
  const checkMcpJson = !slugForCheck || slugForCheck === "claude_code";
2246
2296
  const checkCodexToml = !slugForCheck || slugForCheck === "codex";
2297
+ const checkGrokToml = !slugForCheck || ["grok", "grok_cli", "grok_build"].includes(slugForCheck);
2247
2298
  const checkKimiJson = !slugForCheck || slugForCheck === "kimi";
2248
2299
 
2249
2300
  if (checkMcpJson && existsSync(mcpJsonPath)) {
@@ -2266,6 +2317,13 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2266
2317
  }
2267
2318
  } catch {}
2268
2319
  }
2320
+ if (!existingToken && checkGrokToml && existsSync(grokTomlPath)) {
2321
+ const existing = readGrokTomlShape(grokTomlPath);
2322
+ if (existing) {
2323
+ existingToken = existing.token;
2324
+ existingConfigFile = grokTomlPath;
2325
+ }
2326
+ }
2269
2327
  if (!existingToken && checkKimiJson && existsSync(kimiJsonPath)) {
2270
2328
  try {
2271
2329
  const existing = JSON.parse(readFileSync(kimiJsonPath, "utf-8"));
@@ -2283,6 +2341,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2283
2341
  // Figure out which tool is already configured
2284
2342
  const existingToolName = existingConfigFile.includes(".kimi") ? "Kimi Code"
2285
2343
  : existingConfigFile.includes(".codex") ? "Codex"
2344
+ : existingConfigFile.includes(".grok") ? "Grok CLI"
2286
2345
  : existingConfigFile.includes(".agents") ? "Antigravity CLI"
2287
2346
  : existingConfigFile.includes("antigravity") ? "Antigravity"
2288
2347
  : existingConfigFile.includes("openclaw") ? "OpenClaw"
@@ -2517,6 +2576,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2517
2576
 
2518
2577
  const isCodex = choice === "2";
2519
2578
  const isCursor = choice === "3";
2579
+ const isGrok = choice === "14";
2520
2580
  const isWindsurf = choice === "4";
2521
2581
  const isGemini = choice === "5";
2522
2582
  const isVSCode = choice === "6";
@@ -2575,6 +2635,29 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2575
2635
  console.log(`\n ${green}✓${r} Cursor configured: ${dim}${cursorPath}${r}`);
2576
2636
  console.log(` ${dim}Per-project only — other projects won't see this agent.${r}`);
2577
2637
  }
2638
+ } else if (isGrok) {
2639
+ // Grok CLI: project-scoped .grok/config.toml. Grok loads this file from
2640
+ // the current directory up through the git root, so the bearer remains
2641
+ // isolated to this project. Use Grok's native HTTP MCP form.
2642
+ const grokDir = join(cwd, ".grok");
2643
+ mkdirSync(grokDir, { recursive: true });
2644
+ const grokPath = join(grokDir, "config.toml");
2645
+ let grokConfig = existsSync(grokPath) ? readFileSync(grokPath, "utf-8") : "";
2646
+ grokConfig = grokConfig.replace(/\[mcp_servers\.patchcord(?:[-\w]*)?\][\s\S]*?(?=\n\[(?!mcp_servers\.patchcord(?:\.|[-\w]*\]))|$)/g, "").replace(/\n{3,}/g, "\n\n").trim();
2647
+ grokConfig = (grokConfig ? grokConfig + "\n\n" : "") + `[mcp_servers.patchcord]\nurl = "${serverUrl}/mcp"\nenabled = true\n\n[mcp_servers.patchcord.headers]\nAuthorization = "Bearer ${token}"\nX-Patchcord-Machine = "${hostname}"\n`;
2648
+ try {
2649
+ writeFileSync(grokPath, grokConfig);
2650
+ console.log(`\n ${green}✓${r} Grok CLI configured: ${dim}${grokPath}${r}`);
2651
+ console.log(` ${dim}Per-project only — Grok loads configs from this folder up to the git root.${r}`);
2652
+ const grokSkillsSrc = join(pluginRoot, "per-project-skills", "grok");
2653
+ const grokSkillsDest = join(grokDir, "skills");
2654
+ if (existsSync(grokSkillsSrc)) {
2655
+ cpSync(grokSkillsSrc, grokSkillsDest, { recursive: true, force: true });
2656
+ console.log(` ${green}✓${r} Grok Patchcord skills installed: ${dim}${grokSkillsDest}${r}`);
2657
+ }
2658
+ } catch (e) {
2659
+ console.log(`\n ${yellow}⚠ Failed to write ${grokPath}: ${e.message}${r}`);
2660
+ }
2578
2661
  } else if (isHermes) {
2579
2662
  // Hermes: global only (~/.hermes/config.yaml, YAML, mcp_servers key)
2580
2663
  const hermesPath = join(HOME, ".hermes", "config.yaml");
@@ -3160,7 +3243,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3160
3243
  // Hermes is global config (~/.hermes/config.yaml) — no per-project file to ignore.
3161
3244
  if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isHermes) {
3162
3245
  const gitignorePath = join(cwd, ".gitignore");
3163
- const configFile = isKimiCode ? ".kimi-code/mcp.json" : isKimi ? ".kimi/mcp.json" : isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : isAntigravity ? ".agents/mcp_config.json" : ".mcp.json";
3246
+ const configFile = isKimiCode ? ".kimi-code/mcp.json" : isKimi ? ".kimi/mcp.json" : isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isGrok ? ".grok/config.toml" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : isAntigravity ? ".agents/mcp_config.json" : ".mcp.json";
3164
3247
  // Forms that already cover this config (its file or its dir)
3165
3248
  const patterns = [configFile];
3166
3249
  if (isKimiCode) patterns.push(".kimi-code/");
@@ -3188,7 +3271,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3188
3271
  }
3189
3272
  }
3190
3273
 
3191
- const toolName = isHermes ? "Hermes" : isKimiCode ? "Kimi Code" : isKimi ? "Kimi Code" : isAntigravity ? "Antigravity CLI" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
3274
+ const toolName = isHermes ? "Hermes" : isKimiCode ? "Kimi Code" : isKimi ? "Kimi Code" : isAntigravity ? "Antigravity CLI" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isGrok ? "Grok CLI" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
3192
3275
 
3193
3276
  if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isKimi && !isHermes) {
3194
3277
  console.log(`\n ${dim}To connect a second agent:${r}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.133",
3
+ "version": "0.6.0",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "scripts": {
6
6
  "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json"
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: patchcord:subscribe
3
+ description: >
4
+ Start a persistent background WebSocket listener that wakes the Cursor agent
5
+ when new Patchcord messages arrive. Use ONLY when the user runs
6
+ /patchcord:subscribe or when a project explicitly asks to auto-arm it.
7
+ ---
8
+
9
+ User invoked `/patchcord:subscribe` — do NOT substitute `wait_for_message()`.
10
+ Start the persistent listener in a background Shell.
11
+
12
+ ## Start
13
+
14
+ 1. **Drain the inbox first.** Call the Patchcord `inbox` MCP tool (Cursor may
15
+ expose it as `mcp_patchcord_inbox`). Process every pending message according
16
+ to the patchcord inbox skill before starting the listener.
17
+
18
+ 2. **Find the absolute project root.** Use the current session's git root or
19
+ agent worktree. Do not rely on the ambient Shell cwd: the listener must read
20
+ the `.cursor/mcp.json` for this project and therefore needs an explicit
21
+ `cd`.
22
+
23
+ 3. **Spawn the background listener** with Cursor's Shell tool:
24
+
25
+ ```text
26
+ Shell(
27
+ description: "Patchcord realtime listener",
28
+ block_until_ms: 0,
29
+ command: "cd <ABSOLUTE_PROJECT_DIR> && patchcord subscribe | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}",
30
+ notify_on_output: { pattern: "^PATCHCORD:", reason: "Patchcord message" }
31
+ )
32
+ ```
33
+
34
+ Replace `<ABSOLUTE_PROJECT_DIR>` with the root found in step 2. The grep
35
+ filter drops listener diagnostics and heartbeat lines; only `PATCHCORD:`
36
+ lines wake the agent. `${PIPESTATUS[0]}` preserves `patchcord subscribe`'s
37
+ exit code through the pipe. `subscribe.mjs` owns the pidfile and
38
+ reconnect/backoff loop; do not re-arm it after every notification.
39
+
40
+ 4. **Confirm one line:**
41
+ `Patchcord listener active — I'll pick up new messages as they arrive.`
42
+
43
+ ## When a notification fires
44
+
45
+ Cursor surfaces a line such as `PATCHCORD: 1 new from <sender>`:
46
+
47
+ 1. Say: `Got a Patchcord ping — checking inbox.`
48
+ 2. Call the Patchcord `inbox` MCP tool.
49
+ 3. Process and reply to each message according to the patchcord inbox skill.
50
+ 4. Do not spawn another listener unless the background Shell exited.
51
+
52
+ ## Stopping
53
+
54
+ - End the Cursor agent session, or
55
+ - Kill the listener with:
56
+ `kill $(cat /tmp/patchcord_subscribe_<namespace>_<agent>.pid)`
57
+
58
+ ## Errors
59
+
60
+ Read the background Shell terminal output and surface the exact cause:
61
+
62
+ - `already running (pid N)` (exit 2) — another listener is active; report it
63
+ and do not respawn.
64
+ - `ticket: token rejected (HTTP 401|403)` — the bearer token is invalid or
65
+ expired.
66
+ - `no patchcord config found` — the listener was not started from a project
67
+ containing `.cursor/mcp.json` (or a parent project directory).
68
+ - `subscribe: fatal: ...` — report the fatal line verbatim.
69
+
70
+ If the stream ends without one of these errors, it likely ended with the Cursor
71
+ session or because its stdout consumer closed. Start it again only when the
72
+ user invokes `/patchcord:subscribe` again.
73
+
74
+ **Forbidden on failure:** no hand-rolled `nohup ... &`, no pidfile editing, no
75
+ `curl`-to-MCP workarounds, and no re-arm loop after each inbox notification.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: patchcord-inbox
3
+ description: >
4
+ Check and process Patchcord messages for this Grok project. Use when the
5
+ user asks to check inbox, mentions Patchcord or another agent, or a monitor
6
+ notification reports a new message.
7
+ user-invocable: true
8
+ ---
9
+
10
+ # Patchcord inbox for Grok
11
+
12
+ Use this skill only when the Patchcord MCP tools are loaded in the current
13
+ Grok session. If they are not loaded, do not read `.grok/config.toml`, scrape a
14
+ token, or call Patchcord over HTTP; report that the MCP is unavailable.
15
+
16
+ The Grok MCP server namespaces its tools as `patchcord__*`:
17
+
18
+ - `patchcord__inbox` — read pending messages and presence
19
+ - `patchcord__send_message` — send to one or more agents
20
+ - `patchcord__reply` — reply to a received message
21
+ - `patchcord__wait_for_message` — wait for a response
22
+ - `patchcord__attachment`, `patchcord__recall`, `patchcord__unsend`
23
+
24
+ ## Intake
25
+
26
+ 1. Call `patchcord__inbox` first.
27
+ 2. For every actionable message, do the requested work before replying.
28
+ 3. Reply with concrete files, changes, and verification; never send an ack-only
29
+ response. Close an ack-only thread silently when needed.
30
+ 4. If a request is destructive or needs credentials, stop and ask the user.
31
+ 5. Do not reply to conversation-ending acks such as “ok”, “thanks”, or
32
+ “noted”.
33
+
34
+ ## Sending
35
+
36
+ Call `patchcord__inbox` before sending. Use `patchcord__send_message` with the
37
+ agent name and a specific, complete message. Use named threads for multi-turn
38
+ work. If the recipient is not actively listening, the message is still queued;
39
+ do not retry in a loop.
40
+
41
+ ## Message classification
42
+
43
+ For each new message, classify it as ACK, BLOCKED, ACTIONABLE, or FYI. For
44
+ ACTIONABLE work, implement and verify it, then report `DONE` or `BLOCKED` with
45
+ the evidence. Never claim a test or fix without running it in this checkout.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: patchcord-subscribe
3
+ description: >
4
+ Start a persistent Patchcord realtime listener that wakes Grok when new
5
+ messages arrive. Use ONLY when the user explicitly runs
6
+ /patchcord-subscribe or asks to subscribe to Patchcord.
7
+ disable-model-invocation: true
8
+ user-invocable: true
9
+ ---
10
+
11
+ # Patchcord subscribe for Grok
12
+
13
+ The user explicitly requested `/patchcord-subscribe`. Do not replace this with
14
+ `patchcord__wait_for_message` or a polling loop.
15
+
16
+ ## Start
17
+
18
+ 1. Drain the backlog with `patchcord__inbox`. Process pending actionable
19
+ messages before starting the listener.
20
+ 2. Start a persistent Grok `monitor` with this command:
21
+
22
+ ```text
23
+ monitor(
24
+ description: "patchcord realtime listener",
25
+ persistent: true,
26
+ timeout_ms: 3600000,
27
+ command: "patchcord subscribe | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}"
28
+ )
29
+ ```
30
+
31
+ The explicit `cd` is not needed when Grok runs from the project directory;
32
+ `patchcord subscribe` resolves `.grok/config.toml` by walking to the git
33
+ root. The listener owns its pidfile and reconnects itself.
34
+ 3. Tell the user: **“Patchcord listener active — I’ll pick up new messages as
35
+ they arrive.”**
36
+
37
+ ## On each notification
38
+
39
+ When monitor surfaces `PATCHCORD: 1 new from <sender>`:
40
+
41
+ 1. Say that a Patchcord ping arrived and call `patchcord__inbox`.
42
+ 2. Do the requested work.
43
+ 3. Reply with concrete results using `patchcord__reply`; classify the outcome
44
+ as DONE, BLOCKED, or ACTIONABLE.
45
+ 4. Do not start another monitor. The existing listener persists across turns.
46
+
47
+ ## Errors and stopping
48
+
49
+ If the monitor stream ends, inspect its final output and report one sentence:
50
+
51
+ - `already running (pid N)` — another listener is active; do not respawn.
52
+ - `ticket: token rejected (HTTP 401|403)` — the project bearer is invalid.
53
+ - `no patchcord config found` — Grok is outside a configured project.
54
+ - `subscribe: fatal: ...` — report the fatal line verbatim.
55
+
56
+ Do not use `pgrep`, `ps`, `pkill`, `killall`, hand-written pidfiles, or
57
+ respawn loops. Stop by ending the Grok session or killing the pid from:
58
+ `/tmp/patchcord_subscribe_<namespace>_<agent>.pid`.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: patchcord-wait
3
+ description: >
4
+ Wait for one Patchcord response in this Grok project. Use only when the user
5
+ explicitly asks Grok to wait for a Patchcord message.
6
+ user-invocable: true
7
+ ---
8
+
9
+ # Patchcord wait for Grok
10
+
11
+ Use only when the user explicitly asks to wait. If Patchcord MCP tools are not
12
+ loaded, do not substitute the CLI, HTTP, or a config-file token.
13
+
14
+ Call `patchcord__wait_for_message` once. When it returns, process the message
15
+ before replying with concrete results. Use `patchcord__reply` with `resolve`
16
+ when the thread is complete. Do not send text replies to ack-only messages.
17
+
18
+ This is a one-shot wait. For a persistent realtime listener, use
19
+ `patchcord-subscribe` instead.