patchcord 0.5.64 → 0.5.66
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/README.md +2 -1
- package/bin/patchcord.mjs +118 -18
- package/package.json +1 -1
- package/per-project-skills/kimi/SKILL.md +152 -0
- package/per-project-skills/kimi/wait/SKILL.md +30 -0
- package/scripts/kimi-stop-hook.sh +62 -0
- package/scripts/kimi-subscribe.sh +89 -0
- package/scripts/subscribe.mjs +1 -1
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.
|
|
11
|
+
One command. Opens browser, configures everything. Works with Claude Code, Codex CLI, Cursor, Windsurf, Gemini CLI, VS Code, Zed, OpenCode, Kimi CLI.
|
|
12
12
|
|
|
13
13
|
Self-hosted:
|
|
14
14
|
|
|
@@ -23,6 +23,7 @@ npx patchcord@latest --token <token> --server https://patchcord.yourdomain.com
|
|
|
23
23
|
- **Stop hook** — checks inbox between turns, notifies of pending messages
|
|
24
24
|
- **Slash commands** — `/patchcord` and `/patchcord-wait` for Codex and Gemini CLI
|
|
25
25
|
- **MCP config** — per-project or global config depending on tool
|
|
26
|
+
- **Kimi CLI** — global MCP config plus skills in `~/.kimi/skills/`
|
|
26
27
|
|
|
27
28
|
## How it works
|
|
28
29
|
|
package/bin/patchcord.mjs
CHANGED
|
@@ -79,7 +79,7 @@ Usage:
|
|
|
79
79
|
patchcord agents List every agent (with whoami)
|
|
80
80
|
patchcord agents <name> Show one agent's whoami
|
|
81
81
|
patchcord upload <file> [--mime <type>] [--as <name>] Upload a file as a patchcord attachment (prints the storage path)
|
|
82
|
-
patchcord subscribe
|
|
82
|
+
patchcord subscribe [interval] Start the realtime listener (Kimi: background poll, default 30s)
|
|
83
83
|
patchcord update Update to the latest version
|
|
84
84
|
patchcord --version Show installed version
|
|
85
85
|
patchcord --help Show this help
|
|
@@ -94,9 +94,9 @@ if (cmd === "plugin-path") {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// Shared bearer/base-url resolver. Project-local configs win over globals.
|
|
97
|
-
// Supports all
|
|
97
|
+
// Supports all 12 installer targets: claude_code, codex, cursor, vscode,
|
|
98
98
|
// opencode (per-project) + windsurf, gemini, zed, openclaw, antigravity,
|
|
99
|
-
// cline (global). Each tool stores the bearer in its own shape.
|
|
99
|
+
// cline, kimi (global). Each tool stores the bearer in its own shape.
|
|
100
100
|
async function _resolveBearer() {
|
|
101
101
|
const { readFileSync } = await import("fs");
|
|
102
102
|
|
|
@@ -240,6 +240,7 @@ async function _resolveBearer() {
|
|
|
240
240
|
() => readJsonAt(join(HOME, ".openclaw", "openclaw.json"), ["mcp", "servers", "patchcord"], "openclaw"),
|
|
241
241
|
() => readJsonAt(join(HOME, ".gemini", "antigravity", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
|
|
242
242
|
...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
|
|
243
|
+
() => readJsonAt(join(HOME, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi"),
|
|
243
244
|
];
|
|
244
245
|
for (const r of globalCandidates) {
|
|
245
246
|
const found = r();
|
|
@@ -458,16 +459,34 @@ if (cmd === "upload") {
|
|
|
458
459
|
// (exits with code 2 + "already running" if another listener is up),
|
|
459
460
|
// so this command needs zero pre-checks.
|
|
460
461
|
//
|
|
461
|
-
// We resolve the bearer here (all
|
|
462
|
+
// We resolve the bearer here (all 12 tool configs) and inject it as
|
|
462
463
|
// env vars so subscribe.mjs works regardless of which MCP client is
|
|
463
464
|
// running — OpenCode, Codex, Cursor, etc. — not just Claude Code.
|
|
464
465
|
if (cmd === "subscribe") {
|
|
466
|
+
const bearerInfo = await _resolveBearer();
|
|
467
|
+
|
|
468
|
+
// Kimi CLI uses polling instead of WebSocket realtime
|
|
469
|
+
if (bearerInfo?.tool === "kimi") {
|
|
470
|
+
const kimiSubScript = join(HOME, ".kimi", "patchcord-subscribe.sh");
|
|
471
|
+
if (!existsSync(kimiSubScript)) {
|
|
472
|
+
console.error(`Kimi subscribe script not found at ${kimiSubScript}`);
|
|
473
|
+
console.error(`Run npx patchcord@latest to install it.`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
const { spawnSync } = await import("child_process");
|
|
477
|
+
const intervalArg = process.argv[3] || "30";
|
|
478
|
+
const result = spawnSync("bash", [kimiSubScript, intervalArg], {
|
|
479
|
+
stdio: "inherit",
|
|
480
|
+
env: { ...process.env, PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin" },
|
|
481
|
+
});
|
|
482
|
+
process.exit(result.status ?? (result.signal ? 1 : 0));
|
|
483
|
+
}
|
|
484
|
+
|
|
465
485
|
const subscribeScript = join(pluginRoot, "scripts", "subscribe.mjs");
|
|
466
486
|
if (!existsSync(subscribeScript)) {
|
|
467
487
|
console.error(`subscribe.mjs not found at ${subscribeScript}`);
|
|
468
488
|
process.exit(1);
|
|
469
489
|
}
|
|
470
|
-
const bearerInfo = await _resolveBearer();
|
|
471
490
|
const spawnEnv = { ...process.env };
|
|
472
491
|
if (bearerInfo) {
|
|
473
492
|
spawnEnv.PATCHCORD_BASE_URL = bearerInfo.baseUrl;
|
|
@@ -946,6 +965,62 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
946
965
|
if (geminiChanged) globalChanges.push("Gemini CLI skills + commands installed");
|
|
947
966
|
}
|
|
948
967
|
|
|
968
|
+
// Kimi CLI
|
|
969
|
+
const hasKimi = run("which kimi");
|
|
970
|
+
if (hasKimi) {
|
|
971
|
+
const kimiSkillDir = join(HOME, ".kimi", "skills", "patchcord");
|
|
972
|
+
const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord-wait");
|
|
973
|
+
let kimiChanged = false;
|
|
974
|
+
if (!existsSync(kimiSkillDir)) {
|
|
975
|
+
mkdirSync(kimiSkillDir, { recursive: true });
|
|
976
|
+
cpSync(join(pluginRoot, "per-project-skills", "kimi", "SKILL.md"), join(kimiSkillDir, "SKILL.md"));
|
|
977
|
+
kimiChanged = true;
|
|
978
|
+
}
|
|
979
|
+
if (!existsSync(kimiWaitDir)) {
|
|
980
|
+
mkdirSync(kimiWaitDir, { recursive: true });
|
|
981
|
+
cpSync(join(pluginRoot, "per-project-skills", "kimi", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
|
|
982
|
+
kimiChanged = true;
|
|
983
|
+
}
|
|
984
|
+
if (kimiChanged) globalChanges.push("Kimi CLI skills installed");
|
|
985
|
+
|
|
986
|
+
// Install/update stop hook — fires after each Kimi turn to check inbox
|
|
987
|
+
const kimiHookSrc = join(pluginRoot, "scripts", "kimi-stop-hook.sh");
|
|
988
|
+
const kimiHookDest = join(HOME, ".kimi", "patchcord-stop-hook.sh");
|
|
989
|
+
if (existsSync(kimiHookSrc)) {
|
|
990
|
+
const hookAlreadyExisted = existsSync(kimiHookDest);
|
|
991
|
+
copyFileSync(kimiHookSrc, kimiHookDest);
|
|
992
|
+
chmodSync(kimiHookDest, 0o755);
|
|
993
|
+
|
|
994
|
+
const kimiConfigPath = join(HOME, ".kimi", "config.toml");
|
|
995
|
+
let kimiConfig = existsSync(kimiConfigPath) ? readFileSync(kimiConfigPath, "utf-8") : "";
|
|
996
|
+
|
|
997
|
+
// Remove old inline hooks = [] (conflicts with [[hooks]] array-of-tables)
|
|
998
|
+
kimiConfig = kimiConfig.replace(/^hooks\s*=\s*\[\]\n?/gm, "");
|
|
999
|
+
|
|
1000
|
+
// Remove existing patchcord stop hook blocks
|
|
1001
|
+
const segments = kimiConfig.split("[[hooks]]");
|
|
1002
|
+
const kept = segments.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-stop-hook"));
|
|
1003
|
+
kimiConfig = kept.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
|
|
1004
|
+
|
|
1005
|
+
// Append new hook
|
|
1006
|
+
kimiConfig = kimiConfig.trimEnd() + `\n\n[[hooks]]\nevent = "Stop"\ncommand = "${kimiHookDest}"\ntimeout = 10\n`;
|
|
1007
|
+
|
|
1008
|
+
mkdirSync(dirname(kimiConfigPath), { recursive: true });
|
|
1009
|
+
writeFileSync(kimiConfigPath, kimiConfig);
|
|
1010
|
+
globalChanges.push(`Kimi stop hook ${hookAlreadyExisted ? "updated" : "installed"}`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Install/update Kimi subscribe script (background polling)
|
|
1014
|
+
const kimiSubSrc = join(pluginRoot, "scripts", "kimi-subscribe.sh");
|
|
1015
|
+
const kimiSubDest = join(HOME, ".kimi", "patchcord-subscribe.sh");
|
|
1016
|
+
if (existsSync(kimiSubSrc)) {
|
|
1017
|
+
const subAlreadyExisted = existsSync(kimiSubDest);
|
|
1018
|
+
copyFileSync(kimiSubSrc, kimiSubDest);
|
|
1019
|
+
chmodSync(kimiSubDest, 0o755);
|
|
1020
|
+
globalChanges.push(`Kimi subscribe script ${subAlreadyExisted ? "updated" : "installed"}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
949
1024
|
// Codex CLI — clean up old settings and install stop hook
|
|
950
1025
|
const codexConfig = join(HOME, ".codex", "config.toml");
|
|
951
1026
|
if (existsSync(codexConfig)) {
|
|
@@ -1006,8 +1081,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1006
1081
|
}
|
|
1007
1082
|
}
|
|
1008
1083
|
|
|
1009
|
-
if (!hasClaude && !existsSync(codexConfig)) {
|
|
1010
|
-
console.log(`${dim}No Claude Code or
|
|
1084
|
+
if (!hasClaude && !existsSync(codexConfig) && !hasKimi) {
|
|
1085
|
+
console.log(`${dim}No Claude Code, Codex CLI, or Kimi CLI detected — skipping global setup.${r}`);
|
|
1011
1086
|
}
|
|
1012
1087
|
|
|
1013
1088
|
if (updateOnly) process.exit(0);
|
|
@@ -1025,7 +1100,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1025
1100
|
const CLIENT_TYPE_MAP = {
|
|
1026
1101
|
"claude_code": "1", "codex": "2", "cursor": "3", "windsurf": "4",
|
|
1027
1102
|
"gemini": "5", "vscode": "6", "zed": "7", "opencode": "8", "openclaw": "9", "antigravity": "10",
|
|
1028
|
-
"cline": "11",
|
|
1103
|
+
"cline": "11", "kimi": "12",
|
|
1029
1104
|
};
|
|
1030
1105
|
|
|
1031
1106
|
|
|
@@ -1077,13 +1152,12 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1077
1152
|
// already pre-selected one for us.
|
|
1078
1153
|
if (!choice) {
|
|
1079
1154
|
console.log(`\n${bold}Which tool are you setting up?${r}\n`);
|
|
1080
|
-
console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI`);
|
|
1081
|
-
console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code`);
|
|
1082
|
-
console.log(` ${cyan}3.${r} Cursor ${cyan}7.${r} Zed`);
|
|
1083
|
-
console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode`);
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (!["1","2","3","4","5","6","7","8","9","11"].includes(choice)) {
|
|
1155
|
+
console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI ${cyan}9.${r} OpenClaw`);
|
|
1156
|
+
console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code ${cyan}10.${r} Antigravity`);
|
|
1157
|
+
console.log(` ${cyan}3.${r} Cursor ${cyan}7.${r} Zed ${cyan}11.${r} Cline`);
|
|
1158
|
+
console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode ${cyan}12.${r} Kimi CLI\n`);
|
|
1159
|
+
choice = (await ask(`${dim}Choose (1-12):${r} `)).trim();
|
|
1160
|
+
if (!["1","2","3","4","5","6","7","8","9","10","11","12"].includes(choice)) {
|
|
1087
1161
|
console.error("Invalid choice.");
|
|
1088
1162
|
rl.close();
|
|
1089
1163
|
process.exit(1);
|
|
@@ -1393,6 +1467,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1393
1467
|
const isOpenClaw = choice === "9";
|
|
1394
1468
|
const isAntigravity = choice === "10";
|
|
1395
1469
|
const isCline = choice === "11";
|
|
1470
|
+
const isKimi = choice === "12";
|
|
1396
1471
|
|
|
1397
1472
|
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
1398
1473
|
|
|
@@ -1625,6 +1700,31 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1625
1700
|
console.log(`\n ${green}✓${r} Cline configured: ${dim}${clinePath}${r}`);
|
|
1626
1701
|
console.log(` ${yellow}Global config — all Cline projects share this agent.${r}`);
|
|
1627
1702
|
}
|
|
1703
|
+
} else if (isKimi) {
|
|
1704
|
+
// Kimi CLI: global ~/.kimi/mcp.json
|
|
1705
|
+
const kimiPath = join(HOME, ".kimi", "mcp.json");
|
|
1706
|
+
const kimiOk = updateJsonConfig(kimiPath, (obj) => {
|
|
1707
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
1708
|
+
obj.mcpServers.patchcord = {
|
|
1709
|
+
url: `${serverUrl}/mcp`,
|
|
1710
|
+
headers: {
|
|
1711
|
+
Authorization: `Bearer ${token}`,
|
|
1712
|
+
"X-Patchcord-Machine": hostname,
|
|
1713
|
+
},
|
|
1714
|
+
};
|
|
1715
|
+
});
|
|
1716
|
+
if (kimiOk) {
|
|
1717
|
+
console.log(`\n ${green}✓${r} Kimi CLI configured: ${dim}${kimiPath}${r}`);
|
|
1718
|
+
console.log(` ${yellow}Global config — all Kimi CLI projects share this agent.${r}`);
|
|
1719
|
+
}
|
|
1720
|
+
// Install/update global skills
|
|
1721
|
+
const kimiSkillDir = join(HOME, ".kimi", "skills", "patchcord");
|
|
1722
|
+
const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord-wait");
|
|
1723
|
+
mkdirSync(kimiSkillDir, { recursive: true });
|
|
1724
|
+
mkdirSync(kimiWaitDir, { recursive: true });
|
|
1725
|
+
cpSync(join(pluginRoot, "per-project-skills", "kimi", "SKILL.md"), join(kimiSkillDir, "SKILL.md"));
|
|
1726
|
+
cpSync(join(pluginRoot, "per-project-skills", "kimi", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
|
|
1727
|
+
console.log(` ${green}✓${r} Skills installed: ${dim}patchcord${r}, ${dim}patchcord-wait${r}`);
|
|
1628
1728
|
} else if (isVSCode) {
|
|
1629
1729
|
// VS Code: write .vscode/mcp.json (per-project)
|
|
1630
1730
|
const vscodeDir = join(cwd, ".vscode");
|
|
@@ -1851,7 +1951,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1851
1951
|
}
|
|
1852
1952
|
|
|
1853
1953
|
// Warn about gitignore for per-project configs with tokens
|
|
1854
|
-
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isAntigravity && !isCline) {
|
|
1954
|
+
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isAntigravity && !isCline && !isKimi) {
|
|
1855
1955
|
const gitignorePath = join(cwd, ".gitignore");
|
|
1856
1956
|
const configFile = isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : ".mcp.json";
|
|
1857
1957
|
let needsWarning = true;
|
|
@@ -1869,9 +1969,9 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1869
1969
|
}
|
|
1870
1970
|
}
|
|
1871
1971
|
|
|
1872
|
-
const toolName = isAntigravity ? "Antigravity" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
|
|
1972
|
+
const toolName = isKimi ? "Kimi CLI" : isAntigravity ? "Antigravity" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
|
|
1873
1973
|
|
|
1874
|
-
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isAntigravity && !isCline) {
|
|
1974
|
+
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isAntigravity && !isCline && !isKimi) {
|
|
1875
1975
|
console.log(`\n ${dim}To connect a second agent:${r}`);
|
|
1876
1976
|
console.log(` ${dim}cd into another project and run${r} ${bold}npx patchcord@latest${r} ${dim}there.${r}`);
|
|
1877
1977
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord
|
|
3
|
+
description: >
|
|
4
|
+
Cross-agent messaging for Kimi CLI via the Patchcord MCP server. Use when
|
|
5
|
+
the user mentions other agents, inbox state, sending messages, who's online,
|
|
6
|
+
or cross-machine coordination.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Patchcord for Kimi CLI
|
|
10
|
+
|
|
11
|
+
You are connected to Patchcord through the MCP server configured in
|
|
12
|
+
`~/.kimi/mcp.json`. There is no Patchcord plugin for Kimi — behavior comes
|
|
13
|
+
from MCP tools + this skill.
|
|
14
|
+
|
|
15
|
+
Your identity is **global** (all Kimi projects share the same agent) because
|
|
16
|
+
Kimi stores MCP config in `~/.kimi/mcp.json`.
|
|
17
|
+
|
|
18
|
+
## Tools available
|
|
19
|
+
|
|
20
|
+
- `inbox(all_agents?)` - read pending messages, current identity, and recently active agents. `all_agents=true` includes inactive agents. Returns a `groups` list (messages grouped by thread) alongside the legacy `pending` flat list. Presence tells you whether to wait for a reply after sending, not whether to send.
|
|
21
|
+
- `send_message(to_agent, content, thread?)` - send a message. Comma-separated for multiple: `send_message("backend, frontend", "hello")`. Use `@username` for cross-user Gate messaging. `thread` is an optional slug to start or join a named thread: `send_message("backend", "...", thread="auth-migration")`. Messages support up to 50,000 characters - send full content, specs, and code as-is. Never summarize or truncate.
|
|
22
|
+
- `reply(message_id, content?, defer?, resolve?)` - reply to a received message. Auto-inherits the thread of the original. `defer=true` keeps the original visible in inbox for later (survives context compaction). `resolve=true` closes the thread — stamps `thread_resolved_at` and notifies sender. Content is optional: use `reply(message_id, resolve=true)` to silently close without sending.
|
|
23
|
+
- `wait_for_message(timeout_seconds?)` - block until incoming message arrives. Default 5 minutes. Known to error intermittently - if it fails, poll inbox() every 10-15 seconds as fallback.
|
|
24
|
+
- `attachment(...)` - upload, download, or relay files between agents (see File sharing below)
|
|
25
|
+
- `recall(limit?, 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.
|
|
26
|
+
- `unsend(message_id)` - take back a message before the recipient reads it
|
|
27
|
+
|
|
28
|
+
## Notification mechanisms
|
|
29
|
+
|
|
30
|
+
Kimi has **two** complementary ways to learn about incoming messages:
|
|
31
|
+
|
|
32
|
+
1. **Stop hook** (always active) — after every turn, Kimi runs `~/.kimi/patchcord-stop-hook.sh`. If messages are pending, it injects `📬 Patchcord: You have N pending message(s)` into the next turn's context. No background task needed.
|
|
33
|
+
|
|
34
|
+
2. **Background polling** (optional, proactive) — run `patchcord subscribe` to start a background task that polls the inbox every 30 seconds (or pass a custom interval: `patchcord subscribe 10`). When messages arrive, the task exits with code 0, triggering Kimi's auto-run. This wakes Kimi even during idle periods.
|
|
35
|
+
|
|
36
|
+
### Re-arming the background poll
|
|
37
|
+
|
|
38
|
+
**Critical:** the background poll is **one-shot** — it exits on first detection and must be re-launched. Every time you read messages from `inbox()` or `wait_for_message()`, **re-arm the listener before doing any work**:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Run: patchcord subscribe
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This ensures the next incoming message will also wake you up. If you forget, you will only be notified via the stop hook (which only fires at the end of your next turn).
|
|
45
|
+
|
|
46
|
+
## Do the work, never just acknowledge
|
|
47
|
+
|
|
48
|
+
When you receive a message from another agent:
|
|
49
|
+
|
|
50
|
+
1. Do the task described in the message first. Update the file. Write the code. Fix the bug. Whatever it asks - do it.
|
|
51
|
+
2. Then reply with what you did. File paths, line numbers, concrete changes.
|
|
52
|
+
3. Never reply with only an acknowledgment. "Got it", "Will do", "Ready" are not acceptable as standalone replies.
|
|
53
|
+
|
|
54
|
+
The user can undo any change in seconds. A wrong action costs nothing. A useless ack wastes everyone's time.
|
|
55
|
+
|
|
56
|
+
If you genuinely cannot act (missing file access, need credentials, ambiguous target): say specifically what's blocking you.
|
|
57
|
+
|
|
58
|
+
If you can't do it right now: use `reply(message_id, "reason", defer=true)` to keep the message visible for later. Never silently skip a message.
|
|
59
|
+
|
|
60
|
+
## Startup
|
|
61
|
+
|
|
62
|
+
Call `inbox()` once at session start. **If inbox is empty, say nothing about it — proceed silently with the user's task.**
|
|
63
|
+
|
|
64
|
+
If there are pending actionable messages:
|
|
65
|
+
|
|
66
|
+
1. Do the work described in each message
|
|
67
|
+
2. Reply with what you did
|
|
68
|
+
3. Tell the user what came in and what you did about it
|
|
69
|
+
|
|
70
|
+
Do not ask the user for permission to reply unless the requested action is destructive or requires secrets you do not have.
|
|
71
|
+
|
|
72
|
+
## Threads
|
|
73
|
+
|
|
74
|
+
Named threads group related messages between a pair of agents. Use them for multi-turn tasks that need their own context.
|
|
75
|
+
|
|
76
|
+
- **Start**: `send_message("backend", "track this here", thread="deploy-review")`
|
|
77
|
+
- **Reply stays in thread automatically** — `reply()` inherits `thread_id` from the message you're replying to.
|
|
78
|
+
- **Close**: `reply(message_id, "done", resolve=true)` — closes the thread and notifies sender.
|
|
79
|
+
- **Filter history**: `recall(thread_id="<uuid>")` — only messages in that thread.
|
|
80
|
+
|
|
81
|
+
`inbox()` `groups` field clusters pending messages by thread. Each group: `{ thread_id, thread_title, messages }`. `thread_id: null` = pair-level.
|
|
82
|
+
|
|
83
|
+
## Sending workflow
|
|
84
|
+
|
|
85
|
+
1. `inbox()` - clear pending messages that block outbound sends. Note who's online (determines whether to wait after sending).
|
|
86
|
+
2. `send_message("agent", "specific question with paths and context")` - or `"agent1, agent2"` for multiple, or `"@username"` for cross-user Gate messaging. Add `thread="slug"` to group messages in a named thread.
|
|
87
|
+
3. If recipient is online: `wait_for_message()` - stay responsive for the response. If offline: skip the wait, tell the human the message is queued.
|
|
88
|
+
|
|
89
|
+
Always send regardless of online/offline status. Messages are stored and delivered when the recipient checks inbox. Never refuse to send because an agent appears offline.
|
|
90
|
+
|
|
91
|
+
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active - ask them to check their inbox."
|
|
92
|
+
|
|
93
|
+
If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
|
|
94
|
+
|
|
95
|
+
## Receiving workflow
|
|
96
|
+
|
|
97
|
+
Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
|
|
98
|
+
|
|
99
|
+
1. Read the message from `inbox()` or `wait_for_message()`. Check `message.thread` / `message.thread_id` if present.
|
|
100
|
+
2. Do the work - use real code, real files, real results from your project
|
|
101
|
+
3. Reply with the right flag:
|
|
102
|
+
- `reply(message_id, "done: [details]")` — work done, sender might follow up. Thread auto-inherited.
|
|
103
|
+
- `reply(message_id, "done: [details]", resolve=true)` — work done, thread closed.
|
|
104
|
+
- `reply(message_id, resolve=true)` — silently close without sending anything.
|
|
105
|
+
- `reply(message_id, "ack, prioritizing [other task] first", defer=true)` — acknowledged but work not done yet. Message stays in your inbox as a reminder.
|
|
106
|
+
4. If sender is online: `wait_for_message()` for follow-ups
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
|
|
111
|
+
|
|
112
|
+
## Cross-user messaging (Gate)
|
|
113
|
+
|
|
114
|
+
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).
|
|
115
|
+
|
|
116
|
+
## File sharing
|
|
117
|
+
|
|
118
|
+
**Files on disk → `patchcord upload` (CLI, preferred):**
|
|
119
|
+
```
|
|
120
|
+
patchcord upload /path/to/report.md --mime text/markdown
|
|
121
|
+
```
|
|
122
|
+
Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat. 25MB cap.
|
|
123
|
+
|
|
124
|
+
**Public URLs → `attachment(relay=true, ...)`:**
|
|
125
|
+
```
|
|
126
|
+
attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
|
|
127
|
+
```
|
|
128
|
+
Server fetches and stores. Use when the file already lives at a public URL.
|
|
129
|
+
|
|
130
|
+
**Inline base64 last resort:**
|
|
131
|
+
```
|
|
132
|
+
attachment(upload=true, filename="notes.txt", file_data="<base64>")
|
|
133
|
+
```
|
|
134
|
+
Only if you cannot run shell commands. Wastes context tokens.
|
|
135
|
+
|
|
136
|
+
**Downloading:**
|
|
137
|
+
```
|
|
138
|
+
attachment(path_or_url="namespace/agent/timestamp_file.md")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Always send the storage path (not file content) to the other agent.
|
|
142
|
+
|
|
143
|
+
## Rules
|
|
144
|
+
|
|
145
|
+
- Do the work first, reply second. Never reply before completing the task.
|
|
146
|
+
- **Do not reply to acks.** "ok", "noted", "seen", "thanks", "good progress", "keep running" — read them and move on. If you must close the thread: `reply(id, resolve=true)` with NO content.
|
|
147
|
+
- **resolve=true with ack-only content is an anti-pattern.** `reply(id, "Noted", resolve=true)` creates a new pending message the other side feels compelled to answer — producing ack chains. Omit content when there's nothing substantive to add: `reply(id, resolve=true)`.
|
|
148
|
+
- **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
|
|
149
|
+
- Do not show raw JSON to the user unless they explicitly ask for it.
|
|
150
|
+
- Use `agent@namespace` when the online list shows multiple namespaces for the same agent name.
|
|
151
|
+
- MCP tools are cached at session start. New tools deployed after your session began are invisible until you start a new session.
|
|
152
|
+
- 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,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord-wait
|
|
3
|
+
description: >
|
|
4
|
+
Block this turn waiting for one incoming Patchcord message via the
|
|
5
|
+
wait_for_message MCP tool. Use when the user asks you to wait for a
|
|
6
|
+
patchcord message.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# patchcord-wait
|
|
10
|
+
|
|
11
|
+
User asked you to wait for a Patchcord message. Use `wait_for_message()` only.
|
|
12
|
+
|
|
13
|
+
Call `wait_for_message()` to block until a message arrives (up to 5 minutes).
|
|
14
|
+
|
|
15
|
+
When a message arrives:
|
|
16
|
+
|
|
17
|
+
1. Read it — the tool returns from, content, and message_id. If it belongs to a thread, `thread` and `thread_id` will be set.
|
|
18
|
+
2. **Re-arm the background listener** — run `patchcord subscribe` so the next message will also wake you up.
|
|
19
|
+
3. Do the work described in the message first. Update the file, write the code, fix the bug - whatever it asks.
|
|
20
|
+
4. 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.
|
|
21
|
+
5. Tell the human who wrote and what you did about it
|
|
22
|
+
6. Call `wait_for_message()` again to keep listening
|
|
23
|
+
|
|
24
|
+
Loop until timeout or the human interrupts.
|
|
25
|
+
|
|
26
|
+
If `wait_for_message()` errors, fall back to polling `inbox()` every 10-15 seconds instead of stopping the loop.
|
|
27
|
+
|
|
28
|
+
Do not ask the human for permission to reply - just do the work, reply with results, then report.
|
|
29
|
+
|
|
30
|
+
**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,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Kimi Stop hook — checks patchcord inbox after each turn.
|
|
5
|
+
# Installed automatically by `npx patchcord` when Kimi CLI is detected.
|
|
6
|
+
|
|
7
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Guard: stop_hook_active prevents infinite loops
|
|
12
|
+
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
|
13
|
+
if [ "$STOP_ACTIVE" = "true" ]; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Resolve bearer from ~/.kimi/mcp.json
|
|
18
|
+
HOME_DIR="${HOME}"
|
|
19
|
+
KIMI_MCP="${HOME_DIR}/.kimi/mcp.json"
|
|
20
|
+
TOKEN=""
|
|
21
|
+
URL=""
|
|
22
|
+
|
|
23
|
+
if [ -f "$KIMI_MCP" ]; then
|
|
24
|
+
TOKEN=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$KIMI_MCP" 2>/dev/null | sed 's/^Bearer //i' || true)
|
|
25
|
+
URL=$(jq -r '.mcpServers.patchcord.url // empty' "$KIMI_MCP" 2>/dev/null || true)
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Normalize URL to base
|
|
33
|
+
BASE_URL=$(echo "$URL" | sed 's|/mcp$||; s|/mcp/bearer$||')
|
|
34
|
+
|
|
35
|
+
# Check inbox
|
|
36
|
+
HTTP_CODE=$(curl -s -o /tmp/patchcord_kimi_inbox.json -w "%{http_code}" --max-time 5 \
|
|
37
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
38
|
+
"${BASE_URL}/api/inbox?status=pending&limit=5&count_only=1" 2>/dev/null || echo "000")
|
|
39
|
+
|
|
40
|
+
if [ "$HTTP_CODE" != "200" ]; then
|
|
41
|
+
rm -f /tmp/patchcord_kimi_inbox.json
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
RESPONSE=$(cat /tmp/patchcord_kimi_inbox.json 2>/dev/null || echo '{"count":0}')
|
|
46
|
+
rm -f /tmp/patchcord_kimi_inbox.json
|
|
47
|
+
|
|
48
|
+
COUNT=$(echo "$RESPONSE" | jq -r '.count // .pending_count // 0' 2>/dev/null || echo "0")
|
|
49
|
+
|
|
50
|
+
if [ "$COUNT" -gt 0 ]; then
|
|
51
|
+
# Deduplicate: only notify once every 30 seconds
|
|
52
|
+
NOTIFY_LOCK="/tmp/patchcord_kimi_notify_lock"
|
|
53
|
+
if [ -f "$NOTIFY_LOCK" ]; then
|
|
54
|
+
LOCK_MTIME=$(stat -c %Y "$NOTIFY_LOCK" 2>/dev/null || stat -f %m "$NOTIFY_LOCK" 2>/dev/null || echo "0")
|
|
55
|
+
NOW=$(date +%s)
|
|
56
|
+
[ $(( NOW - LOCK_MTIME )) -lt 30 ] && exit 0
|
|
57
|
+
fi
|
|
58
|
+
touch "$NOTIFY_LOCK"
|
|
59
|
+
echo "📬 Patchcord: You have ${COUNT} pending message(s). Call inbox() to read and reply."
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
exit 0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Patchcord subscribe for Kimi CLI — background polling task.
|
|
5
|
+
# Kimi has no native push/WebSocket listener, so we poll the inbox
|
|
6
|
+
# and exit when messages arrive. Exiting triggers Kimi's auto-run
|
|
7
|
+
# (if the session is armed), waking the agent to read and reply.
|
|
8
|
+
#
|
|
9
|
+
# Usage: patchcord subscribe (starts with default 30s interval)
|
|
10
|
+
# patchcord subscribe 10 (starts with 10s interval)
|
|
11
|
+
#
|
|
12
|
+
# Installed to ~/.kimi/patchcord-subscribe.sh by `npx patchcord@latest`.
|
|
13
|
+
|
|
14
|
+
command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
# Resolve auth from ~/.kimi/mcp.json
|
|
17
|
+
HOME_DIR="${HOME}"
|
|
18
|
+
KIMI_MCP="${HOME_DIR}/.kimi/mcp.json"
|
|
19
|
+
|
|
20
|
+
if [ ! -f "$KIMI_MCP" ]; then
|
|
21
|
+
echo "Kimi MCP config not found at $KIMI_MCP — run npx patchcord@latest first" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
TOKEN=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$KIMI_MCP" 2>/dev/null | sed 's/^Bearer //i' || true)
|
|
26
|
+
URL=$(jq -r '.mcpServers.patchcord.url // empty' "$KIMI_MCP" 2>/dev/null || true)
|
|
27
|
+
|
|
28
|
+
if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
|
|
29
|
+
echo "Patchcord not configured in $KIMI_MCP" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
BASE_URL=$(echo "$URL" | sed 's|/mcp$||; s|/mcp/bearer$||')
|
|
34
|
+
|
|
35
|
+
# Derive agent identity for pidfile
|
|
36
|
+
IDENTITY_RESP=$(curl -s --max-time 5 \
|
|
37
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
38
|
+
"${BASE_URL}/api/inbox?limit=0" 2>/dev/null || echo "{}")
|
|
39
|
+
NAMESPACE_ID=$(echo "$IDENTITY_RESP" | jq -r '.namespace_id // empty' 2>/dev/null || true)
|
|
40
|
+
AGENT_ID=$(echo "$IDENTITY_RESP" | jq -r '.agent_id // empty' 2>/dev/null || true)
|
|
41
|
+
|
|
42
|
+
if [ -z "$NAMESPACE_ID" ] || [ -z "$AGENT_ID" ]; then
|
|
43
|
+
echo "Could not determine agent identity — check your token" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
PIDFILE="/tmp/patchcord_subscribe_${NAMESPACE_ID}_${AGENT_ID}.pid"
|
|
48
|
+
NOTIFY_FILE="${HOME_DIR}/.kimi/patchcord-subscribe-notify.txt"
|
|
49
|
+
|
|
50
|
+
# Pidfile guard: exit if another instance is running
|
|
51
|
+
if [ -f "$PIDFILE" ]; then
|
|
52
|
+
OLD_PID=$(cat "$PIDFILE" 2>/dev/null || echo "")
|
|
53
|
+
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
|
|
54
|
+
echo "Already running (pid $OLD_PID)" >&2
|
|
55
|
+
exit 2
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
echo $$ > "$PIDFILE"
|
|
59
|
+
|
|
60
|
+
# Cleanup pidfile on exit
|
|
61
|
+
cleanup() {
|
|
62
|
+
rm -f "$PIDFILE"
|
|
63
|
+
}
|
|
64
|
+
trap cleanup EXIT INT TERM
|
|
65
|
+
|
|
66
|
+
# Poll interval: first arg or default 30s
|
|
67
|
+
INTERVAL="${1:-30}"
|
|
68
|
+
if ! [[ "$INTERVAL" =~ ^[0-9]+$ ]] || [ "$INTERVAL" -lt 1 ]; then
|
|
69
|
+
INTERVAL=30
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
echo "PATCHCORD: subscribe started (poll ${INTERVAL}s) for ${AGENT_ID}@${NAMESPACE_ID}"
|
|
73
|
+
|
|
74
|
+
while true; do
|
|
75
|
+
RESP=$(curl -s --max-time 10 \
|
|
76
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
77
|
+
"${BASE_URL}/api/inbox?status=pending&limit=5" 2>/dev/null || echo "{}")
|
|
78
|
+
|
|
79
|
+
COUNT=$(echo "$RESP" | jq -r '.pending_count // 0' 2>/dev/null || echo "0")
|
|
80
|
+
|
|
81
|
+
if [ "$COUNT" -gt 0 ] 2>/dev/null; then
|
|
82
|
+
# Write notification file so the agent sees context on wake
|
|
83
|
+
echo "PATCHCORD: ${COUNT} message(s) waiting" > "$NOTIFY_FILE"
|
|
84
|
+
echo "PATCHCORD: ${COUNT} message(s) waiting — exiting to trigger auto-run"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
sleep "$INTERVAL"
|
|
89
|
+
done
|
package/scripts/subscribe.mjs
CHANGED
|
@@ -55,7 +55,7 @@ function die(msg, code = 1) {
|
|
|
55
55
|
|
|
56
56
|
function readMcpConfig(cwd) {
|
|
57
57
|
// Prefer env vars injected by patchcord.mjs, which already ran _resolveBearer()
|
|
58
|
-
// and supports all
|
|
58
|
+
// and supports all 12 tool configs (Claude Code, OpenCode, Codex, Cursor, Kimi, etc.).
|
|
59
59
|
if (process.env.PATCHCORD_BASE_URL && process.env.PATCHCORD_BEARER_TOKEN) {
|
|
60
60
|
return { baseUrl: process.env.PATCHCORD_BASE_URL, token: process.env.PATCHCORD_BEARER_TOKEN };
|
|
61
61
|
}
|