patchcord 0.5.65 → 0.5.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/patchcord.mjs CHANGED
@@ -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 Start the realtime listener
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
@@ -194,6 +194,8 @@ async function _resolveBearer() {
194
194
  (cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode"),
195
195
  (cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode"),
196
196
  (cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml")),
197
+ // Kimi can be per-project via .kimi/mcp.json + --mcp-config-file
198
+ (cwd) => readJsonAt(join(cwd, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi"),
197
199
  ];
198
200
  let dir = process.cwd();
199
201
  while (dir && dir !== "/") {
@@ -240,6 +242,7 @@ async function _resolveBearer() {
240
242
  () => readJsonAt(join(HOME, ".openclaw", "openclaw.json"), ["mcp", "servers", "patchcord"], "openclaw"),
241
243
  () => readJsonAt(join(HOME, ".gemini", "antigravity", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
242
244
  ...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
245
+ // Global Kimi fallback (only if no per-project .kimi/mcp.json was found)
243
246
  () => readJsonAt(join(HOME, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi"),
244
247
  ];
245
248
  for (const r of globalCandidates) {
@@ -463,12 +466,30 @@ if (cmd === "upload") {
463
466
  // env vars so subscribe.mjs works regardless of which MCP client is
464
467
  // running — OpenCode, Codex, Cursor, etc. — not just Claude Code.
465
468
  if (cmd === "subscribe") {
469
+ const bearerInfo = await _resolveBearer();
470
+
471
+ // Kimi CLI uses polling instead of WebSocket realtime
472
+ if (bearerInfo?.tool === "kimi") {
473
+ const kimiSubScript = join(HOME, ".kimi", "patchcord-subscribe.sh");
474
+ if (!existsSync(kimiSubScript)) {
475
+ console.error(`Kimi subscribe script not found at ${kimiSubScript}`);
476
+ console.error(`Run npx patchcord@latest to install it.`);
477
+ process.exit(1);
478
+ }
479
+ const { spawnSync } = await import("child_process");
480
+ const intervalArg = process.argv[3] || "30";
481
+ const result = spawnSync("bash", [kimiSubScript, intervalArg], {
482
+ stdio: "inherit",
483
+ env: { ...process.env, PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin" },
484
+ });
485
+ process.exit(result.status ?? (result.signal ? 1 : 0));
486
+ }
487
+
466
488
  const subscribeScript = join(pluginRoot, "scripts", "subscribe.mjs");
467
489
  if (!existsSync(subscribeScript)) {
468
490
  console.error(`subscribe.mjs not found at ${subscribeScript}`);
469
491
  process.exit(1);
470
492
  }
471
- const bearerInfo = await _resolveBearer();
472
493
  const spawnEnv = { ...process.env };
473
494
  if (bearerInfo) {
474
495
  spawnEnv.PATCHCORD_BASE_URL = bearerInfo.baseUrl;
@@ -955,15 +976,52 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
955
976
  let kimiChanged = false;
956
977
  if (!existsSync(kimiSkillDir)) {
957
978
  mkdirSync(kimiSkillDir, { recursive: true });
958
- cpSync(join(pluginRoot, "skills", "inbox", "SKILL.md"), join(kimiSkillDir, "SKILL.md"));
979
+ cpSync(join(pluginRoot, "per-project-skills", "kimi", "SKILL.md"), join(kimiSkillDir, "SKILL.md"));
959
980
  kimiChanged = true;
960
981
  }
961
982
  if (!existsSync(kimiWaitDir)) {
962
983
  mkdirSync(kimiWaitDir, { recursive: true });
963
- cpSync(join(pluginRoot, "skills", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
984
+ cpSync(join(pluginRoot, "per-project-skills", "kimi", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
964
985
  kimiChanged = true;
965
986
  }
966
987
  if (kimiChanged) globalChanges.push("Kimi CLI skills installed");
988
+
989
+ // Install/update stop hook — fires after each Kimi turn to check inbox
990
+ const kimiHookSrc = join(pluginRoot, "scripts", "kimi-stop-hook.sh");
991
+ const kimiHookDest = join(HOME, ".kimi", "patchcord-stop-hook.sh");
992
+ if (existsSync(kimiHookSrc)) {
993
+ const hookAlreadyExisted = existsSync(kimiHookDest);
994
+ copyFileSync(kimiHookSrc, kimiHookDest);
995
+ chmodSync(kimiHookDest, 0o755);
996
+
997
+ const kimiConfigPath = join(HOME, ".kimi", "config.toml");
998
+ let kimiConfig = existsSync(kimiConfigPath) ? readFileSync(kimiConfigPath, "utf-8") : "";
999
+
1000
+ // Remove old inline hooks = [] (conflicts with [[hooks]] array-of-tables)
1001
+ kimiConfig = kimiConfig.replace(/^hooks\s*=\s*\[\]\n?/gm, "");
1002
+
1003
+ // Remove existing patchcord stop hook blocks
1004
+ const segments = kimiConfig.split("[[hooks]]");
1005
+ const kept = segments.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-stop-hook"));
1006
+ kimiConfig = kept.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
1007
+
1008
+ // Append new hook
1009
+ kimiConfig = kimiConfig.trimEnd() + `\n\n[[hooks]]\nevent = "Stop"\ncommand = "${kimiHookDest}"\ntimeout = 10\n`;
1010
+
1011
+ mkdirSync(dirname(kimiConfigPath), { recursive: true });
1012
+ writeFileSync(kimiConfigPath, kimiConfig);
1013
+ globalChanges.push(`Kimi stop hook ${hookAlreadyExisted ? "updated" : "installed"}`);
1014
+ }
1015
+
1016
+ // Install/update Kimi subscribe script (background polling)
1017
+ const kimiSubSrc = join(pluginRoot, "scripts", "kimi-subscribe.sh");
1018
+ const kimiSubDest = join(HOME, ".kimi", "patchcord-subscribe.sh");
1019
+ if (existsSync(kimiSubSrc)) {
1020
+ const subAlreadyExisted = existsSync(kimiSubDest);
1021
+ copyFileSync(kimiSubSrc, kimiSubDest);
1022
+ chmodSync(kimiSubDest, 0o755);
1023
+ globalChanges.push(`Kimi subscribe script ${subAlreadyExisted ? "updated" : "installed"}`);
1024
+ }
967
1025
  }
968
1026
 
969
1027
  // Codex CLI — clean up old settings and install stop hook
@@ -1141,10 +1199,12 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1141
1199
  let existingConfigFile = "";
1142
1200
  const mcpJsonPath = join(cwd, ".mcp.json");
1143
1201
  const codexTomlPath = join(cwd, ".codex", "config.toml");
1202
+ const kimiJsonPath = join(cwd, ".kimi", "mcp.json");
1144
1203
 
1145
1204
  const slugForCheck = toolSlug ? toolSlug.replace(/-/g, "_") : "";
1146
1205
  const checkMcpJson = !slugForCheck || slugForCheck === "claude_code";
1147
1206
  const checkCodexToml = !slugForCheck || slugForCheck === "codex";
1207
+ const checkKimiJson = !slugForCheck || slugForCheck === "kimi";
1148
1208
 
1149
1209
  if (checkMcpJson && existsSync(mcpJsonPath)) {
1150
1210
  try {
@@ -1166,12 +1226,23 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1166
1226
  }
1167
1227
  } catch {}
1168
1228
  }
1229
+ if (!existingToken && checkKimiJson && existsSync(kimiJsonPath)) {
1230
+ try {
1231
+ const existing = JSON.parse(readFileSync(kimiJsonPath, "utf-8"));
1232
+ const pt = existing?.mcpServers?.patchcord;
1233
+ if (pt?.headers?.Authorization) {
1234
+ existingToken = pt.headers.Authorization.replace(/^Bearer\s+/i, "");
1235
+ existingConfigFile = kimiJsonPath;
1236
+ }
1237
+ } catch {}
1238
+ }
1169
1239
  // Global configs (Antigravity, OpenClaw, Gemini, Windsurf, Zed) are NOT
1170
1240
  // checked here. They're set up once globally and should not block new
1171
1241
  // project setup. Only per-project configs trigger "already configured."
1172
1242
  if (existingToken) {
1173
1243
  // Figure out which tool is already configured
1174
- const existingToolName = existingConfigFile.includes(".codex") ? "Codex"
1244
+ const existingToolName = existingConfigFile.includes(".kimi") ? "Kimi CLI"
1245
+ : existingConfigFile.includes(".codex") ? "Codex"
1175
1246
  : existingConfigFile.includes("antigravity") ? "Antigravity"
1176
1247
  : existingConfigFile.includes("openclaw") ? "OpenClaw"
1177
1248
  : existingConfigFile.includes(".cursor") ? "Cursor"
@@ -1646,8 +1717,9 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1646
1717
  console.log(` ${yellow}Global config — all Cline projects share this agent.${r}`);
1647
1718
  }
1648
1719
  } else if (isKimi) {
1649
- // Kimi CLI: global ~/.kimi/mcp.json
1650
- const kimiPath = join(HOME, ".kimi", "mcp.json");
1720
+ // Kimi CLI: per-project .kimi/mcp.json + shell wrapper for --mcp-config-file
1721
+ const kimiDir = join(cwd, ".kimi");
1722
+ const kimiPath = join(kimiDir, "mcp.json");
1651
1723
  const kimiOk = updateJsonConfig(kimiPath, (obj) => {
1652
1724
  obj.mcpServers = obj.mcpServers || {};
1653
1725
  obj.mcpServers.patchcord = {
@@ -1660,16 +1732,48 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1660
1732
  });
1661
1733
  if (kimiOk) {
1662
1734
  console.log(`\n ${green}✓${r} Kimi CLI configured: ${dim}${kimiPath}${r}`);
1663
- console.log(` ${yellow}Global config all Kimi CLI projects share this agent.${r}`);
1735
+ console.log(` ${dim}Per-projectuse the kimi-pc wrapper (see below) so Kimi loads this config.${r}`);
1664
1736
  }
1665
1737
  // Install/update global skills
1666
1738
  const kimiSkillDir = join(HOME, ".kimi", "skills", "patchcord");
1667
1739
  const kimiWaitDir = join(HOME, ".kimi", "skills", "patchcord-wait");
1668
1740
  mkdirSync(kimiSkillDir, { recursive: true });
1669
1741
  mkdirSync(kimiWaitDir, { recursive: true });
1670
- cpSync(join(pluginRoot, "skills", "inbox", "SKILL.md"), join(kimiSkillDir, "SKILL.md"));
1671
- cpSync(join(pluginRoot, "skills", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
1742
+ cpSync(join(pluginRoot, "per-project-skills", "kimi", "SKILL.md"), join(kimiSkillDir, "SKILL.md"));
1743
+ cpSync(join(pluginRoot, "per-project-skills", "kimi", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
1672
1744
  console.log(` ${green}✓${r} Skills installed: ${dim}patchcord${r}, ${dim}patchcord-wait${r}`);
1745
+
1746
+ // Install shell wrapper for per-project --mcp-config-file
1747
+ const wrapperPath = join(HOME, ".kimi", "patchcord-kimi-wrapper.sh");
1748
+ const wrapperAlreadyExisted = existsSync(wrapperPath);
1749
+ const wrapperContent = `#!/bin/bash
1750
+ # Patchcord Kimi wrapper — auto-detects per-project .kimi/mcp.json
1751
+ # Source this in your ~/.bashrc or ~/.zshrc:
1752
+ # source "${HOME}/.kimi/patchcord-kimi-wrapper.sh"
1753
+ #
1754
+ # Then launch Kimi in any project with:
1755
+ # kimi-pc
1756
+ #
1757
+ # Plain \`kimi\` still uses the global ~/.kimi/mcp.json.
1758
+
1759
+ kimi-pc() {
1760
+ local dir="$PWD"
1761
+ while [ "$dir" != "/" ]; do
1762
+ if [ -f "$dir/.kimi/mcp.json" ]; then
1763
+ kimi --mcp-config-file "$dir/.kimi/mcp.json" "$@"
1764
+ return
1765
+ fi
1766
+ dir="$(dirname "$dir")"
1767
+ done
1768
+ echo "No .kimi/mcp.json found in $PWD or any parent — falling back to global config" >&2
1769
+ kimi "$@"
1770
+ }
1771
+ `;
1772
+ writeFileSync(wrapperPath, wrapperContent);
1773
+ console.log(`\n ${green}✓${r} Shell wrapper installed: ${dim}${wrapperPath}${r}`);
1774
+ console.log(` ${dim}Add this to your ~/.bashrc or ~/.zshrc:${r}`);
1775
+ console.log(` ${cyan}source "${wrapperPath}"${r}`);
1776
+ console.log(` ${dim}Then run ${bold}kimi-pc${r}${dim} instead of ${bold}kimi${r}${dim} in project directories.${r}`);
1673
1777
  } else if (isVSCode) {
1674
1778
  // VS Code: write .vscode/mcp.json (per-project)
1675
1779
  const vscodeDir = join(cwd, ".vscode");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.65",
3
+ "version": "0.5.67",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -0,0 +1,166 @@
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` inside your project directory. Kimi normally uses a global
13
+ config, but Patchcord sets up **per-project agents** via `--mcp-config-file`.
14
+
15
+ **Launch Kimi in project directories with `kimi-pc`** (not plain `kimi`).
16
+ This wrapper auto-detects `.kimi/mcp.json` in the current project and loads
17
+ the right agent identity.
18
+
19
+ ```bash
20
+ # In any project with .kimi/mcp.json:
21
+ kimi-pc
22
+
23
+ # Falls back to global ~/.kimi/mcp.json if no project config found.
24
+ ```
25
+
26
+ Add this to your `~/.bashrc` or `~/.zshrc` (done once by the installer):
27
+ ```bash
28
+ source "$HOME/.kimi/patchcord-kimi-wrapper.sh"
29
+ ```
30
+
31
+ ## Tools available
32
+
33
+ - `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.
34
+ - `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.
35
+ - `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.
36
+ - `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.
37
+ - `attachment(...)` - upload, download, or relay files between agents (see File sharing below)
38
+ - `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.
39
+ - `unsend(message_id)` - take back a message before the recipient reads it
40
+
41
+ ## Notification mechanisms
42
+
43
+ Kimi has **two** complementary ways to learn about incoming messages:
44
+
45
+ 1. **Stop hook** (always active) — after every turn, Kimi runs `~/.kimi/patchcord-stop-hook.sh`. It detects the project from `cwd` in stdin, walks up looking for `.kimi/mcp.json`, then checks the inbox. If messages are pending, it injects `📬 Patchcord: You have N pending message(s)` into the next turn's context. No background task needed.
46
+
47
+ 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.
48
+
49
+ ### Re-arming the background poll
50
+
51
+ **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**:
52
+
53
+ ```
54
+ Run: patchcord subscribe
55
+ ```
56
+
57
+ 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).
58
+
59
+ ## Do the work, never just acknowledge
60
+
61
+ When you receive a message from another agent:
62
+
63
+ 1. Do the task described in the message first. Update the file. Write the code. Fix the bug. Whatever it asks - do it.
64
+ 2. Then reply with what you did. File paths, line numbers, concrete changes.
65
+ 3. Never reply with only an acknowledgment. "Got it", "Will do", "Ready" are not acceptable as standalone replies.
66
+
67
+ The user can undo any change in seconds. A wrong action costs nothing. A useless ack wastes everyone's time.
68
+
69
+ If you genuinely cannot act (missing file access, need credentials, ambiguous target): say specifically what's blocking you.
70
+
71
+ 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.
72
+
73
+ ## Startup
74
+
75
+ Call `inbox()` once at session start. **If inbox is empty, say nothing about it — proceed silently with the user's task.**
76
+
77
+ If there are pending actionable messages:
78
+
79
+ 1. Do the work described in each message
80
+ 2. Reply with what you did
81
+ 3. Tell the user what came in and what you did about it
82
+
83
+ Do not ask the user for permission to reply unless the requested action is destructive or requires secrets you do not have.
84
+
85
+ ## Threads
86
+
87
+ Named threads group related messages between a pair of agents. Use them for multi-turn tasks that need their own context.
88
+
89
+ - **Start**: `send_message("backend", "track this here", thread="deploy-review")`
90
+ - **Reply stays in thread automatically** — `reply()` inherits `thread_id` from the message you're replying to.
91
+ - **Close**: `reply(message_id, "done", resolve=true)` — closes the thread and notifies sender.
92
+ - **Filter history**: `recall(thread_id="<uuid>")` — only messages in that thread.
93
+
94
+ `inbox()` `groups` field clusters pending messages by thread. Each group: `{ thread_id, thread_title, messages }`. `thread_id: null` = pair-level.
95
+
96
+ ## Sending workflow
97
+
98
+ 1. `inbox()` - clear pending messages that block outbound sends. Note who's online (determines whether to wait after sending).
99
+ 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.
100
+ 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.
101
+
102
+ 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.
103
+
104
+ After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active - ask them to check their inbox."
105
+
106
+ If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
107
+
108
+ ## Receiving workflow
109
+
110
+ Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
111
+
112
+ 1. Read the message from `inbox()` or `wait_for_message()`. Check `message.thread` / `message.thread_id` if present.
113
+ 2. **Re-arm the background listener** — run `patchcord subscribe` so the next message will also wake you up.
114
+ 3. Do the work - use real code, real files, real results from your project
115
+ 4. Reply with the right flag:
116
+ - `reply(message_id, "done: [details]")` — work done, sender might follow up. Thread auto-inherited.
117
+ - `reply(message_id, "done: [details]", resolve=true)` — work done, thread closed.
118
+ - `reply(message_id, resolve=true)` — silently close without sending anything.
119
+ - `reply(message_id, "ack, prioritizing [other task] first", defer=true)` — acknowledged but work not done yet. Message stays in your inbox as a reminder.
120
+ 5. If sender is online: `wait_for_message()` for follow-ups
121
+
122
+ 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.
123
+
124
+ Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
125
+
126
+ ## Cross-user messaging (Gate)
127
+
128
+ 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).
129
+
130
+ ## File sharing
131
+
132
+ **Files on disk → `patchcord upload` (CLI, preferred):**
133
+ ```
134
+ patchcord upload /path/to/report.md --mime text/markdown
135
+ ```
136
+ Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat. 25MB cap.
137
+
138
+ **Public URLs → `attachment(relay=true, ...)`:**
139
+ ```
140
+ attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
141
+ ```
142
+ Server fetches and stores. Use when the file already lives at a public URL.
143
+
144
+ **Inline base64 last resort:**
145
+ ```
146
+ attachment(upload=true, filename="notes.txt", file_data="<base64>")
147
+ ```
148
+ Only if you cannot run shell commands. Wastes context tokens.
149
+
150
+ **Downloading:**
151
+ ```
152
+ attachment(path_or_url="namespace/agent/timestamp_file.md")
153
+ ```
154
+
155
+ Always send the storage path (not file content) to the other agent.
156
+
157
+ ## Rules
158
+
159
+ - Do the work first, reply second. Never reply before completing the task.
160
+ - **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.
161
+ - **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)`.
162
+ - **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
163
+ - Do not show raw JSON to the user unless they explicitly ask for it.
164
+ - Use `agent@namespace` when the online list shows multiple namespaces for the same agent name.
165
+ - MCP tools are cached at session start. New tools deployed after your session began are invisible until you start a new session.
166
+ - 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,82 @@
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
+ # Supports per-project .kimi/mcp.json (walks up from cwd) and
8
+ # falls back to global ~/.kimi/mcp.json.
9
+
10
+ command -v jq >/dev/null 2>&1 || exit 0
11
+
12
+ INPUT=$(cat)
13
+
14
+ # Guard: stop_hook_active prevents infinite loops
15
+ STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
16
+ if [ "$STOP_ACTIVE" = "true" ]; then
17
+ exit 0
18
+ fi
19
+
20
+ # Resolve MCP config: per-project first, then global
21
+ KIMI_MCP=""
22
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || echo "")
23
+
24
+ if [ -n "$CWD" ]; then
25
+ dir="$CWD"
26
+ while [ "$dir" != "/" ]; do
27
+ if [ -f "$dir/.kimi/mcp.json" ]; then
28
+ KIMI_MCP="$dir/.kimi/mcp.json"
29
+ break
30
+ fi
31
+ dir=$(dirname "$dir")
32
+ done
33
+ fi
34
+
35
+ # Fallback to global config
36
+ if [ -z "$KIMI_MCP" ]; then
37
+ KIMI_MCP="${HOME}/.kimi/mcp.json"
38
+ fi
39
+
40
+ TOKEN=""
41
+ URL=""
42
+
43
+ if [ -f "$KIMI_MCP" ]; then
44
+ TOKEN=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$KIMI_MCP" 2>/dev/null | sed 's/^Bearer //i' || true)
45
+ URL=$(jq -r '.mcpServers.patchcord.url // empty' "$KIMI_MCP" 2>/dev/null || true)
46
+ fi
47
+
48
+ if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
49
+ exit 0
50
+ fi
51
+
52
+ # Normalize URL to base
53
+ BASE_URL=$(echo "$URL" | sed 's|/mcp$||; s|/mcp/bearer$||')
54
+
55
+ # Check inbox
56
+ HTTP_CODE=$(curl -s -o /tmp/patchcord_kimi_inbox.json -w "%{http_code}" --max-time 5 \
57
+ -H "Authorization: Bearer ${TOKEN}" \
58
+ "${BASE_URL}/api/inbox?status=pending&limit=5&count_only=1" 2>/dev/null || echo "000")
59
+
60
+ if [ "$HTTP_CODE" != "200" ]; then
61
+ rm -f /tmp/patchcord_kimi_inbox.json
62
+ exit 0
63
+ fi
64
+
65
+ RESPONSE=$(cat /tmp/patchcord_kimi_inbox.json 2>/dev/null || echo '{"count":0}')
66
+ rm -f /tmp/patchcord_kimi_inbox.json
67
+
68
+ COUNT=$(echo "$RESPONSE" | jq -r '.count // .pending_count // 0' 2>/dev/null || echo "0")
69
+
70
+ if [ "$COUNT" -gt 0 ]; then
71
+ # Deduplicate: only notify once every 30 seconds
72
+ NOTIFY_LOCK="/tmp/patchcord_kimi_notify_lock"
73
+ if [ -f "$NOTIFY_LOCK" ]; then
74
+ LOCK_MTIME=$(stat -c %Y "$NOTIFY_LOCK" 2>/dev/null || stat -f %m "$NOTIFY_LOCK" 2>/dev/null || echo "0")
75
+ NOW=$(date +%s)
76
+ [ $(( NOW - LOCK_MTIME )) -lt 30 ] && exit 0
77
+ fi
78
+ touch "$NOTIFY_LOCK"
79
+ echo "📬 Patchcord: You have ${COUNT} pending message(s). Call inbox() to read and reply."
80
+ fi
81
+
82
+ exit 0
@@ -0,0 +1,102 @@
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
+ # Supports per-project .kimi/mcp.json (walks up from cwd) and
13
+ # falls back to global ~/.kimi/mcp.json.
14
+
15
+ command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 1; }
16
+
17
+ # Resolve MCP config: per-project first, then global
18
+ KIMI_MCP=""
19
+ dir="$PWD"
20
+ while [ "$dir" != "/" ]; do
21
+ if [ -f "$dir/.kimi/mcp.json" ]; then
22
+ KIMI_MCP="$dir/.kimi/mcp.json"
23
+ break
24
+ fi
25
+ dir=$(dirname "$dir")
26
+ done
27
+
28
+ # Fallback to global config
29
+ if [ -z "$KIMI_MCP" ]; then
30
+ KIMI_MCP="${HOME}/.kimi/mcp.json"
31
+ fi
32
+
33
+ if [ ! -f "$KIMI_MCP" ]; then
34
+ echo "Kimi MCP config not found at $KIMI_MCP or any parent — run npx patchcord@latest first" >&2
35
+ exit 1
36
+ fi
37
+
38
+ TOKEN=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$KIMI_MCP" 2>/dev/null | sed 's/^Bearer //i' || true)
39
+ URL=$(jq -r '.mcpServers.patchcord.url // empty' "$KIMI_MCP" 2>/dev/null || true)
40
+
41
+ if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
42
+ echo "Patchcord not configured in $KIMI_MCP" >&2
43
+ exit 1
44
+ fi
45
+
46
+ BASE_URL=$(echo "$URL" | sed 's|/mcp$||; s|/mcp/bearer$||')
47
+
48
+ # Derive agent identity for pidfile
49
+ IDENTITY_RESP=$(curl -s --max-time 5 \
50
+ -H "Authorization: Bearer ${TOKEN}" \
51
+ "${BASE_URL}/api/inbox?limit=0" 2>/dev/null || echo "{}")
52
+ NAMESPACE_ID=$(echo "$IDENTITY_RESP" | jq -r '.namespace_id // empty' 2>/dev/null || true)
53
+ AGENT_ID=$(echo "$IDENTITY_RESP" | jq -r '.agent_id // empty' 2>/dev/null || true)
54
+
55
+ if [ -z "$NAMESPACE_ID" ] || [ -z "$AGENT_ID" ]; then
56
+ echo "Could not determine agent identity — check your token" >&2
57
+ exit 1
58
+ fi
59
+
60
+ PIDFILE="/tmp/patchcord_subscribe_${NAMESPACE_ID}_${AGENT_ID}.pid"
61
+ NOTIFY_FILE="${HOME}/.kimi/patchcord-subscribe-notify.txt"
62
+
63
+ # Pidfile guard: exit if another instance is running
64
+ if [ -f "$PIDFILE" ]; then
65
+ OLD_PID=$(cat "$PIDFILE" 2>/dev/null || echo "")
66
+ if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
67
+ echo "Already running (pid $OLD_PID)" >&2
68
+ exit 2
69
+ fi
70
+ fi
71
+ echo $$ > "$PIDFILE"
72
+
73
+ # Cleanup pidfile on exit
74
+ cleanup() {
75
+ rm -f "$PIDFILE"
76
+ }
77
+ trap cleanup EXIT INT TERM
78
+
79
+ # Poll interval: first arg or default 30s
80
+ INTERVAL="${1:-30}"
81
+ if ! [[ "$INTERVAL" =~ ^[0-9]+$ ]] || [ "$INTERVAL" -lt 1 ]; then
82
+ INTERVAL=30
83
+ fi
84
+
85
+ echo "PATCHCORD: subscribe started (poll ${INTERVAL}s) for ${AGENT_ID}@${NAMESPACE_ID}"
86
+
87
+ while true; do
88
+ RESP=$(curl -s --max-time 10 \
89
+ -H "Authorization: Bearer ${TOKEN}" \
90
+ "${BASE_URL}/api/inbox?status=pending&limit=5" 2>/dev/null || echo "{}")
91
+
92
+ COUNT=$(echo "$RESP" | jq -r '.pending_count // 0' 2>/dev/null || echo "0")
93
+
94
+ if [ "$COUNT" -gt 0 ] 2>/dev/null; then
95
+ # Write notification file so the agent sees context on wake
96
+ echo "PATCHCORD: ${COUNT} message(s) waiting" > "$NOTIFY_FILE"
97
+ echo "PATCHCORD: ${COUNT} message(s) waiting — exiting to trigger auto-run"
98
+ exit 0
99
+ fi
100
+
101
+ sleep "$INTERVAL"
102
+ done