patchcord 0.5.133 → 0.6.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/bin/patchcord.mjs +97 -18
- package/package.json +1 -1
- package/per-project-skills/cursor/inbox/SKILL.md +193 -0
- package/per-project-skills/cursor/subscribe/SKILL.md +75 -0
- package/per-project-skills/cursor/wait/SKILL.md +31 -0
- package/per-project-skills/grok/inbox/SKILL.md +45 -0
- package/per-project-skills/grok/subscribe/SKILL.md +58 -0
- package/per-project-skills/grok/wait/SKILL.md +19 -0
- package/skills/subscribe/SKILL.md +2 -2
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
|
|
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"]],
|
|
@@ -1756,19 +1796,24 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1756
1796
|
const hasCursorAgent = run("which cursor-agent");
|
|
1757
1797
|
if (hasCursorAgent || existsSync(cursorSkillsRoot)) {
|
|
1758
1798
|
mkdirSync(cursorSkillsRoot, { recursive: true });
|
|
1759
|
-
const
|
|
1799
|
+
const cursorInboxDir = join(cursorSkillsRoot, "patchcord-inbox");
|
|
1760
1800
|
const cursorWaitDir = join(cursorSkillsRoot, "patchcord-wait");
|
|
1801
|
+
const cursorSubscribeDir = join(cursorSkillsRoot, "patchcord-subscribe");
|
|
1802
|
+
const staleCursorSkillDir = join(cursorSkillsRoot, "patchcord");
|
|
1761
1803
|
let cursorChanged = false;
|
|
1762
|
-
if (
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1804
|
+
if (existsSync(staleCursorSkillDir)) {
|
|
1805
|
+
try { rmSync(staleCursorSkillDir, { recursive: true, force: true }); cursorChanged = true; } catch {}
|
|
1806
|
+
}
|
|
1807
|
+
mkdirSync(cursorInboxDir, { recursive: true });
|
|
1808
|
+
cpSync(join(pluginRoot, "per-project-skills", "cursor", "inbox", "SKILL.md"), join(cursorInboxDir, "SKILL.md"));
|
|
1809
|
+
cursorChanged = true;
|
|
1810
|
+
mkdirSync(cursorWaitDir, { recursive: true });
|
|
1811
|
+
cpSync(join(pluginRoot, "per-project-skills", "cursor", "wait", "SKILL.md"), join(cursorWaitDir, "SKILL.md"));
|
|
1812
|
+
mkdirSync(cursorSubscribeDir, { recursive: true });
|
|
1813
|
+
cpSync(
|
|
1814
|
+
join(pluginRoot, "per-project-skills", "cursor", "subscribe", "SKILL.md"),
|
|
1815
|
+
join(cursorSubscribeDir, "SKILL.md")
|
|
1816
|
+
);
|
|
1772
1817
|
if (cursorChanged) globalChanges.push("Cursor skills installed");
|
|
1773
1818
|
|
|
1774
1819
|
if (hasCursorAgent) {
|
|
@@ -2144,7 +2189,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2144
2189
|
let choice = "";
|
|
2145
2190
|
|
|
2146
2191
|
const CLIENT_TYPE_MAP = {
|
|
2147
|
-
"claude_code": "1", "codex": "2", "cursor": "3", "windsurf": "4",
|
|
2192
|
+
"claude_code": "1", "codex": "2", "cursor": "3", "grok": "14", "grok_cli": "14", "grok_build": "14", "windsurf": "4",
|
|
2148
2193
|
"gemini": "5", "vscode": "6", "zed": "7", "opencode": "8", "openclaw": "9", "antigravity": "10",
|
|
2149
2194
|
"cline": "11", "kimi": "12", "hermes": "13",
|
|
2150
2195
|
};
|
|
@@ -2239,11 +2284,13 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2239
2284
|
let existingConfigFile = "";
|
|
2240
2285
|
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
2241
2286
|
const codexTomlPath = join(cwd, ".codex", "config.toml");
|
|
2287
|
+
const grokTomlPath = join(cwd, ".grok", "config.toml");
|
|
2242
2288
|
const kimiJsonPath = join(cwd, ".kimi", "mcp.json");
|
|
2243
2289
|
|
|
2244
2290
|
const slugForCheck = toolSlug ? toolSlug.replace(/-/g, "_") : "";
|
|
2245
2291
|
const checkMcpJson = !slugForCheck || slugForCheck === "claude_code";
|
|
2246
2292
|
const checkCodexToml = !slugForCheck || slugForCheck === "codex";
|
|
2293
|
+
const checkGrokToml = !slugForCheck || ["grok", "grok_cli", "grok_build"].includes(slugForCheck);
|
|
2247
2294
|
const checkKimiJson = !slugForCheck || slugForCheck === "kimi";
|
|
2248
2295
|
|
|
2249
2296
|
if (checkMcpJson && existsSync(mcpJsonPath)) {
|
|
@@ -2266,6 +2313,13 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2266
2313
|
}
|
|
2267
2314
|
} catch {}
|
|
2268
2315
|
}
|
|
2316
|
+
if (!existingToken && checkGrokToml && existsSync(grokTomlPath)) {
|
|
2317
|
+
const existing = readGrokTomlShape(grokTomlPath);
|
|
2318
|
+
if (existing) {
|
|
2319
|
+
existingToken = existing.token;
|
|
2320
|
+
existingConfigFile = grokTomlPath;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2269
2323
|
if (!existingToken && checkKimiJson && existsSync(kimiJsonPath)) {
|
|
2270
2324
|
try {
|
|
2271
2325
|
const existing = JSON.parse(readFileSync(kimiJsonPath, "utf-8"));
|
|
@@ -2283,6 +2337,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2283
2337
|
// Figure out which tool is already configured
|
|
2284
2338
|
const existingToolName = existingConfigFile.includes(".kimi") ? "Kimi Code"
|
|
2285
2339
|
: existingConfigFile.includes(".codex") ? "Codex"
|
|
2340
|
+
: existingConfigFile.includes(".grok") ? "Grok CLI"
|
|
2286
2341
|
: existingConfigFile.includes(".agents") ? "Antigravity CLI"
|
|
2287
2342
|
: existingConfigFile.includes("antigravity") ? "Antigravity"
|
|
2288
2343
|
: existingConfigFile.includes("openclaw") ? "OpenClaw"
|
|
@@ -2517,6 +2572,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2517
2572
|
|
|
2518
2573
|
const isCodex = choice === "2";
|
|
2519
2574
|
const isCursor = choice === "3";
|
|
2575
|
+
const isGrok = choice === "14";
|
|
2520
2576
|
const isWindsurf = choice === "4";
|
|
2521
2577
|
const isGemini = choice === "5";
|
|
2522
2578
|
const isVSCode = choice === "6";
|
|
@@ -2575,6 +2631,29 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2575
2631
|
console.log(`\n ${green}✓${r} Cursor configured: ${dim}${cursorPath}${r}`);
|
|
2576
2632
|
console.log(` ${dim}Per-project only — other projects won't see this agent.${r}`);
|
|
2577
2633
|
}
|
|
2634
|
+
} else if (isGrok) {
|
|
2635
|
+
// Grok CLI: project-scoped .grok/config.toml. Grok loads this file from
|
|
2636
|
+
// the current directory up through the git root, so the bearer remains
|
|
2637
|
+
// isolated to this project. Use Grok's native HTTP MCP form.
|
|
2638
|
+
const grokDir = join(cwd, ".grok");
|
|
2639
|
+
mkdirSync(grokDir, { recursive: true });
|
|
2640
|
+
const grokPath = join(grokDir, "config.toml");
|
|
2641
|
+
let grokConfig = existsSync(grokPath) ? readFileSync(grokPath, "utf-8") : "";
|
|
2642
|
+
grokConfig = grokConfig.replace(/\[mcp_servers\.patchcord(?:[-\w]*)?\][\s\S]*?(?=\n\[(?!mcp_servers\.patchcord(?:\.|[-\w]*\]))|$)/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
2643
|
+
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`;
|
|
2644
|
+
try {
|
|
2645
|
+
writeFileSync(grokPath, grokConfig);
|
|
2646
|
+
console.log(`\n ${green}✓${r} Grok CLI configured: ${dim}${grokPath}${r}`);
|
|
2647
|
+
console.log(` ${dim}Per-project only — Grok loads configs from this folder up to the git root.${r}`);
|
|
2648
|
+
const grokSkillsSrc = join(pluginRoot, "per-project-skills", "grok");
|
|
2649
|
+
const grokSkillsDest = join(grokDir, "skills");
|
|
2650
|
+
if (existsSync(grokSkillsSrc)) {
|
|
2651
|
+
cpSync(grokSkillsSrc, grokSkillsDest, { recursive: true, force: true });
|
|
2652
|
+
console.log(` ${green}✓${r} Grok Patchcord skills installed: ${dim}${grokSkillsDest}${r}`);
|
|
2653
|
+
}
|
|
2654
|
+
} catch (e) {
|
|
2655
|
+
console.log(`\n ${yellow}⚠ Failed to write ${grokPath}: ${e.message}${r}`);
|
|
2656
|
+
}
|
|
2578
2657
|
} else if (isHermes) {
|
|
2579
2658
|
// Hermes: global only (~/.hermes/config.yaml, YAML, mcp_servers key)
|
|
2580
2659
|
const hermesPath = join(HOME, ".hermes", "config.yaml");
|
|
@@ -3160,7 +3239,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
3160
3239
|
// Hermes is global config (~/.hermes/config.yaml) — no per-project file to ignore.
|
|
3161
3240
|
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isHermes) {
|
|
3162
3241
|
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";
|
|
3242
|
+
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
3243
|
// Forms that already cover this config (its file or its dir)
|
|
3165
3244
|
const patterns = [configFile];
|
|
3166
3245
|
if (isKimiCode) patterns.push(".kimi-code/");
|
|
@@ -3188,7 +3267,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
3188
3267
|
}
|
|
3189
3268
|
}
|
|
3190
3269
|
|
|
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";
|
|
3270
|
+
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
3271
|
|
|
3193
3272
|
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isKimi && !isHermes) {
|
|
3194
3273
|
console.log(`\n ${dim}To connect a second agent:${r}`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord-inbox
|
|
3
|
+
description: >
|
|
4
|
+
Cross-agent messaging across MCP-connected agents. Use when user mentions
|
|
5
|
+
other agents, patchcord, check inbox, send message, who's online, agent
|
|
6
|
+
coordination, or when you receive additionalContext about pending patchcord
|
|
7
|
+
messages.
|
|
8
|
+
---
|
|
9
|
+
# patchcord
|
|
10
|
+
|
|
11
|
+
7 MCP tools: inbox, send_message, reply, wait_for_message, attachment, recall, unsend.
|
|
12
|
+
|
|
13
|
+
## Applies ONLY when the patchcord MCP tools are loaded this session
|
|
14
|
+
|
|
15
|
+
This skill governs those 7 MCP tools. It applies only if they are actually available in the current session.
|
|
16
|
+
|
|
17
|
+
**If the patchcord MCP tools are NOT present, this skill does not apply — do nothing with it. Specifically:**
|
|
18
|
+
- Do NOT call inbox() at session start or at any other time.
|
|
19
|
+
- Do NOT run the `patchcord` CLI as a substitute for the missing tools.
|
|
20
|
+
- Do NOT read a bearer token out of `.mcp.json`, `settings.json`, `mcp_config.json`, or any other config file, and do NOT call the patchcord HTTP API (`/api/inbox`, etc.) directly. A token belongs to the one agent it was issued to; using another agent's token — or any token you found on disk — is never correct.
|
|
21
|
+
- Just proceed with the user's actual request.
|
|
22
|
+
|
|
23
|
+
A missing tool means patchcord is not configured for this agent. That is a normal, silent state, not a problem to work around.
|
|
24
|
+
|
|
25
|
+
## Do the work, never just acknowledge
|
|
26
|
+
|
|
27
|
+
When you receive a message from another agent:
|
|
28
|
+
|
|
29
|
+
1. Do the task described in the message first. Update the file. Write the code. Fix the bug. Create the document. Whatever the message asks - do it.
|
|
30
|
+
2. Then reply with what you did. Not what you plan to do. Not that you received it. What you actually did. File paths, line numbers, concrete changes.
|
|
31
|
+
3. Never reply with only an acknowledgment. "Got it", "Understood", "Role accepted", "Will do", "Ready" - these are not acceptable as standalone replies. If your reply doesn't describe completed work, you failed.
|
|
32
|
+
|
|
33
|
+
The user can undo any change in 3 seconds with git. A wrong action costs nothing. A useless ack wastes everyone's time and breaks the workflow.
|
|
34
|
+
|
|
35
|
+
**If a message contains a spec, update, or instruction - act on it immediately:**
|
|
36
|
+
- Spec received - update the relevant docs/code now, reply with what you changed
|
|
37
|
+
- Bug report received - investigate and fix now, reply with the fix
|
|
38
|
+
- Architecture decision received - update the relevant files now, reply with what you updated
|
|
39
|
+
- Role assignment received - start doing that role now, reply with first actions taken
|
|
40
|
+
|
|
41
|
+
**If you genuinely cannot act** (missing file access, need credentials, ambiguous target): say specifically what's blocking you. "I need the path to the config file" - not "Understood, I'll do it when ready."
|
|
42
|
+
|
|
43
|
+
**If you can't do it right now** (busy with current task): use `reply(message_id, "reason why deferred", defer=true)`. This keeps the message visible in your inbox so you will come back to it. Never silently skip a message - you will forget it. If you don't act and don't defer, the message is lost forever.
|
|
44
|
+
|
|
45
|
+
## On session start or when prompted by a hook
|
|
46
|
+
|
|
47
|
+
Call inbox(). It returns pending messages, recently active agents, and your own push-receiving state via `self_subscribed`. Note that value — it determines whether you should call `wait_for_message` after sends for the rest of the session.
|
|
48
|
+
|
|
49
|
+
If `subscribe_appears_down: true` is in the response, your subscribe.mjs was running but appears dead. Tell the human: "Patchcord subscribe seems to have died — run `/patchcord-subscribe` to restart push delivery." Do not try to restart it yourself.
|
|
50
|
+
|
|
51
|
+
If there are pending messages, reply to all of them immediately. Do not ask the human first. Do not explain what you plan to reply. Just do the work described in each message, then reply with what you did, then tell the human what you received and what you did about it.
|
|
52
|
+
|
|
53
|
+
## Sending
|
|
54
|
+
|
|
55
|
+
1. inbox() - clear any pending messages that block outbound sends. From the response, note `self_subscribed` (your own push-receiving state).
|
|
56
|
+
2. send_message("agent_name", "specific question with file paths and context") - or "agent1, agent2" for multiple recipients. Use `@username` for cross-user Gate messaging. To start or join a named thread: `send_message("frontend", "content", thread="auth-migration")`.
|
|
57
|
+
3. Decide whether to wait based on **two signals** in the send response:
|
|
58
|
+
- `self_subscribed` (from the most recent inbox call) — are YOU push-receiving?
|
|
59
|
+
- `recipient_subscribed` (in the send response) — is the recipient push-receiving?
|
|
60
|
+
|
|
61
|
+
| self_subscribed | recipient_subscribed | What to do |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| true | true | **Do NOT call wait_for_message.** Continue working. Their reply will arrive via your subscribe push and your Monitor will surface it. Tell the human: "Sent — [agent] will see it within seconds." |
|
|
64
|
+
| true | false | **Do NOT call wait_for_message.** Continue working. Tell the human: "Sent — [agent] isn't actively listening right now, may take a while to respond." |
|
|
65
|
+
| false | true | **Call wait_for_message** with default timeout. Recipient is live, expect a reply soon. |
|
|
66
|
+
| false | false | **Skip wait_for_message.** Tell the human: "Sent — [agent] isn't currently active. Ask them to check inbox in their session." |
|
|
67
|
+
|
|
68
|
+
Always send regardless of recipient state. Messages are stored and delivered when the recipient checks inbox.
|
|
69
|
+
|
|
70
|
+
If `recipient_subscribed` is missing from the response (older server, registry disabled), fall back to the legacy `recipient_online` field for the same decision.
|
|
71
|
+
|
|
72
|
+
If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
|
|
73
|
+
|
|
74
|
+
## Receiving (inbox has messages)
|
|
75
|
+
|
|
76
|
+
Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
|
|
77
|
+
|
|
78
|
+
1. Read the message. If it belongs to a thread, `message.thread` and `message.thread_id` will be present.
|
|
79
|
+
2. Do the work described in the message - using your project's actual code, real files, real lines
|
|
80
|
+
3. Reply with what you did, choosing the right flag:
|
|
81
|
+
- `reply(message_id, "done: [details]")` — work done, sender might follow up. Thread is auto-inherited.
|
|
82
|
+
- `reply(message_id, "done: [details]", resolve=true)` — work done, thread closed. Stamps `thread_resolved_at` and notifies sender.
|
|
83
|
+
- `reply(message_id, resolve=true)` — silently close a thread without sending anything (e.g. clearing misfired messages)
|
|
84
|
+
- `reply(message_id, "ack, prioritizing [other task] first", defer=true)` — you acknowledged but haven't done the work yet. The message stays in your inbox as a reminder.
|
|
85
|
+
4. After replying, decide whether to stay listening using the same two-signal rule as for sends — `self_subscribed` × `recipient_subscribed` (in the reply response). If `self_subscribed` is true, return to your work; your Monitor will wake you when a follow-up arrives. If `self_subscribed` is false and `recipient_subscribed` is true, call `wait_for_message()` to stay responsive. Otherwise (both false), tell the human you've replied and continue with other work.
|
|
86
|
+
5. If you can't do the work, say specifically what's blocking you. Don't guess about another agent's code.
|
|
87
|
+
|
|
88
|
+
When you have multiple pending messages, prioritize by urgency. Use `defer=true` for tasks you'll do later — if you reply without doing the work and don't defer, the message vanishes from your inbox and you will never remember to do it.
|
|
89
|
+
|
|
90
|
+
Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
|
|
91
|
+
|
|
92
|
+
## Cross-user messaging (Gate)
|
|
93
|
+
|
|
94
|
+
To message a user outside your namespace, use `@username` as the to_agent. Example: `send_message("@maria", "hello")`. The message goes through their Gate - connection approval and guardrails apply. If the connection isn't approved yet, your message is held pending their approval (cap 5, 7-day TTL).
|
|
95
|
+
|
|
96
|
+
### Humans
|
|
97
|
+
|
|
98
|
+
- Humans are NOT in the agents list. Use `send_message("@username", "...")` anyway — they don't need to be online or in the roster.
|
|
99
|
+
- The message goes through their Gate for approval. It may be held pending their approval (cap 5, 7-day TTL).
|
|
100
|
+
- Write plainly: who you are, what you need, no raw JSON or logs.
|
|
101
|
+
|
|
102
|
+
## File sharing
|
|
103
|
+
|
|
104
|
+
**Files on disk → `patchcord upload` (CLI, preferred):**
|
|
105
|
+
```
|
|
106
|
+
patchcord upload /path/to/report.md --mime text/markdown
|
|
107
|
+
```
|
|
108
|
+
Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. 25MB cap.
|
|
109
|
+
|
|
110
|
+
**Public URLs → `attachment(relay=true, ...)`:**
|
|
111
|
+
```
|
|
112
|
+
attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
|
|
113
|
+
```
|
|
114
|
+
Server fetches the URL and stores it. Use when the file already lives at a public URL.
|
|
115
|
+
|
|
116
|
+
**Web agents (no shell) → inline base64 last resort:**
|
|
117
|
+
```
|
|
118
|
+
attachment(upload=true, filename="notes.txt", file_data="<base64>")
|
|
119
|
+
```
|
|
120
|
+
Only for agents that cannot run shell commands. Wastes context tokens. Never use if you can run `patchcord upload`.
|
|
121
|
+
|
|
122
|
+
**Downloading:**
|
|
123
|
+
```
|
|
124
|
+
attachment(path_or_url="namespace/agent/timestamp_file.md")
|
|
125
|
+
```
|
|
126
|
+
Pass the storage path from the sender's message.
|
|
127
|
+
|
|
128
|
+
Always send the storage path (not the file content) to the other agent.
|
|
129
|
+
|
|
130
|
+
## Identity (`patchcord whoami` / `patchcord agents`)
|
|
131
|
+
|
|
132
|
+
`whoami` and `agents` are CLI commands, not MCP tools. Run them only when the patchcord MCP tools are present (see the gate at the top of this skill). They read the bearer token from the **current project's own** `.mcp.json` automatically — same namespace scope, no extra setup. Never go hunting for a token in another project's config or another agent's file, and never pass a token on the command line. Cheap to call (input tokens only), don't bloat MCP.
|
|
133
|
+
|
|
134
|
+
- **Run `patchcord whoami` once per session.** Returns your `agent`, `namespace`, project summary, and your 300-char `self` description. Use it on first turn after `/clear` or a fresh session to orient.
|
|
135
|
+
- **Run `patchcord agents`** to see the full roster (every peer's whoami). One call, ~3KB, complete picture of the namespace.
|
|
136
|
+
- **Run `patchcord agents <name>`** when an unknown agent messages you and you want to know who they are before acting on their request.
|
|
137
|
+
|
|
138
|
+
### Updating your own whoami
|
|
139
|
+
|
|
140
|
+
300-char hard limit (CLI enforces client-side).
|
|
141
|
+
|
|
142
|
+
Server responds with one of three statuses:
|
|
143
|
+
|
|
144
|
+
- **applied** — done. Either it was your first-ever whoami (no prior value → set directly, no gate), or it was a confirmed second-shot. Print and move on.
|
|
145
|
+
- **unchanged** — proposed text matches current. No-op.
|
|
146
|
+
- **show_human** — current value exists and the proposed text differs. Server printed `current:` and `proposed:`. You MUST:
|
|
147
|
+
1. Show the diff to the human in conversation
|
|
148
|
+
2. Ask them to confirm
|
|
149
|
+
3. Wait for explicit "yes"
|
|
150
|
+
4. Run the **exact same** `patchcord whoami --propose "<text>"` command again. Server will then return `applied`.
|
|
151
|
+
|
|
152
|
+
Pending state expires after 10 minutes. If the human says no, do not call again. If you call with different text instead, the gate resets to a fresh first-shot for that new text.
|
|
153
|
+
|
|
154
|
+
Never call `--propose` a second time with the same text without showing the human between calls.
|
|
155
|
+
|
|
156
|
+
### Hard rules
|
|
157
|
+
|
|
158
|
+
- You may NEVER update another agent's whoami. The `--propose` flow only writes your own.
|
|
159
|
+
- Namespace scope is enforced server-side: `patchcord agents <name>` returns 404 if the name isn't in your namespace (global agents like claudeai/chatgpt are excepted on cloud).
|
|
160
|
+
- whoami text describes WHO you are and how you coordinate (e.g. "backend systems. sends every change to codex-backend for review"). It is NOT a place for project instructions, code conventions, or long-form notes — those live in CLAUDE.md and project docs.
|
|
161
|
+
|
|
162
|
+
## Threads
|
|
163
|
+
|
|
164
|
+
Named threads group related messages between a pair of agents. Use them for multi-turn tasks that need their own context (e.g. "auth-migration", "deploy-review").
|
|
165
|
+
|
|
166
|
+
- **Start a thread**: `send_message("backend", "let's track this here", thread="auth-migration")`
|
|
167
|
+
- **Reply stays in thread automatically**: `reply()` inherits `thread_id` from the message you're replying to — no extra param needed.
|
|
168
|
+
- **Close a thread**: `reply(message_id, "done", resolve=true)` — stamps `thread_resolved_at` and notifies sender.
|
|
169
|
+
- **View thread history**: `recall(thread_id="<uuid>")` — filters history to one thread.
|
|
170
|
+
|
|
171
|
+
`inbox()` returns a `groups` list alongside the legacy `pending` flat list. Each group has `thread_id`, `thread_title`, and `messages`. `thread_id: null` means pair-level (no thread). Read from `groups` for thread-aware handling.
|
|
172
|
+
|
|
173
|
+
## Other tools
|
|
174
|
+
|
|
175
|
+
- recall(limit=10, from_agent="", thread_id="") - view recent message history including already-read messages. `from_agent` filters by sender. `thread_id` filters to a specific thread. For debugging only, not routine use.
|
|
176
|
+
- unsend(message_id) - take back a message before the recipient reads it.
|
|
177
|
+
|
|
178
|
+
## Rules
|
|
179
|
+
|
|
180
|
+
- Do the work first, reply second. Never reply before completing the task.
|
|
181
|
+
- Never ask "want me to reply?" - just do the work and reply with results.
|
|
182
|
+
- Never ask "should I do this?" - just do it. User can undo in 3 seconds.
|
|
183
|
+
- Never ask "want me to wait?" - check presence and wait or don't based on that.
|
|
184
|
+
- Never show raw JSON to the human - summarize naturally.
|
|
185
|
+
- **Cross-namespace addressing (`agent@namespace`)**: the syntax always exists in `send_message`/`reply`, but what it actually reaches depends on who you are:
|
|
186
|
+
- **Ordinary agents (the default):** `agent@namespace` only ever works for YOUR OWN namespace — same as a bare name. Targeting any other namespace is rejected. This is the isolation model, not a bug or a missing feature — don't loop on it or try creative addressing to work around it.
|
|
187
|
+
- **Lead agents (cloud only — a small number of specially-provisioned identities per namespace, usually the orchestrator):** may reach a peer lead in a DIFFERENT namespace, but only after that specific pair of namespaces has an `approved` `namespace_links` row. You set this up yourself — no human step: call `request_namespace_link(peer_namespace)`, the peer namespace's lead gets a patchcord message and calls `respond_namespace_link(peer_namespace, approve=True)`, then `agent@namespace` works both ways. Check `list_my_namespace_links()` to see pending/approved/denied status. If you don't know whether you're a lead, you're not one — this isn't discoverable by trial and error; ask the human. (Namespace "projects" still exist but are just an organizational label now — they do NOT gate this.)
|
|
188
|
+
- A rejection here is almost always correct behavior, not a server error — don't retry with variations of the name.
|
|
189
|
+
- **Do not reply to acks.** "ok", "noted", "seen", "thanks", "good progress", "keep running", thumbs up — anything that is clearly a conversation-ending signal. Just read them and move on. If you must close the thread, use `reply(id, resolve=true)` with NO content. Never send a text reply to an ack.
|
|
190
|
+
- **resolve=true with ack-only content is an anti-pattern.** `reply(id, "Noted, thanks", resolve=true)` creates a new pending message the other side feels compelled to answer — producing ack chains. If you have nothing substantive to add, omit content entirely: `reply(id, resolve=true)`. Only include content with resolve when it carries new information the recipient needs.
|
|
191
|
+
- **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
|
|
192
|
+
- MCP tools are cached at session start. New tools deployed after your session began are invisible until you start a new session. If a tool you expect is missing, this is why.
|
|
193
|
+
- Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human.
|
|
@@ -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,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord-wait
|
|
3
|
+
description: >
|
|
4
|
+
Block this turn for up to 5 minutes waiting for one incoming Patchcord
|
|
5
|
+
message via the wait_for_message MCP tool. Single blocking call, no
|
|
6
|
+
background process. Use ONLY when the user explicitly runs
|
|
7
|
+
/patchcord-wait.
|
|
8
|
+
---
|
|
9
|
+
# patchcord-wait
|
|
10
|
+
|
|
11
|
+
Applies ONLY when the patchcord MCP tools are loaded this session. If `wait_for_message` is not available, this skill does not apply — do nothing, do not substitute the CLI or direct HTTP calls, and never read a bearer token out of a config file. Proceed with the user's request.
|
|
12
|
+
|
|
13
|
+
User invoked /patchcord-wait — do NOT substitute /patchcord-subscribe or spawn any background listener. Use `wait_for_message()` only.
|
|
14
|
+
|
|
15
|
+
Call `wait_for_message()` to block until a message arrives (up to 5 minutes).
|
|
16
|
+
|
|
17
|
+
When a message arrives:
|
|
18
|
+
|
|
19
|
+
1. Read it — the tool returns from, content, and message_id. If it belongs to a thread, `thread` and `thread_id` will be set.
|
|
20
|
+
2. Do the work described in the message first. Update the file, write the code, fix the bug - whatever it asks.
|
|
21
|
+
3. Reply with what you did: `reply(message_id, "here's what I changed: [concrete details]")`. Thread is auto-inherited. Use `resolve=true` to close the thread when the task is fully done.
|
|
22
|
+
4. Tell the human who wrote and what you did about it
|
|
23
|
+
5. Call `wait_for_message()` again to keep listening
|
|
24
|
+
|
|
25
|
+
Loop until timeout or the human interrupts.
|
|
26
|
+
|
|
27
|
+
If `wait_for_message()` errors, fall back to polling `inbox()` every 10-15 seconds instead of stopping the loop.
|
|
28
|
+
|
|
29
|
+
Do not ask the human for permission to reply - just do the work, reply with results, then report.
|
|
30
|
+
|
|
31
|
+
**No ack chains.** If the arriving message is a clear ack ("Noted", "Got it", "Thanks", "Keep running") — close it silently with `reply(id, resolve=true)`, no content, and keep listening. Never text-reply to an ack. Never send "Noted" + resolve=true — that creates a new pending message the other side will feel compelled to answer.
|
|
@@ -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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: patchcord:subscribe
|
|
3
3
|
description: >
|
|
4
|
-
Start a persistent background WebSocket listener
|
|
4
|
+
Start a persistent background WebSocket listener that
|
|
5
5
|
wakes Claude when new Patchcord messages arrive. Survives across turns
|
|
6
6
|
until the user kills it or closes the session. Use ONLY when the user
|
|
7
7
|
explicitly runs /patchcord:subscribe.
|
|
@@ -50,7 +50,7 @@ Read the output file. Scan the last ~15 lines for one of:
|
|
|
50
50
|
|
|
51
51
|
- `no .mcp.json in <cwd>` — session is not in a patchcord project dir
|
|
52
52
|
- `ticket: token rejected (HTTP 401|403)` — bad bearer; user regenerates from dashboard
|
|
53
|
-
- `ticket: server not configured for realtime` — self-hosted without
|
|
53
|
+
- `ticket: server not configured for realtime` — self-hosted without realtime configured
|
|
54
54
|
- `ticket: namespace not owned` — token lost its owner; regenerate
|
|
55
55
|
- `already running (pid N)` (exit 2) — another listener is active; report and stop
|
|
56
56
|
- `subscribe: fatal: ...` — surface the line verbatim
|