patchcord 0.5.80 → 0.5.81

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
@@ -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", ".openclaw",
49
+ ".claude", ".codex", ".cursor", ".vscode", ".kimi", ".openclaw",
50
50
  ];
51
51
 
52
52
  function detectFolder(dir) {
@@ -97,8 +97,19 @@ if (cmd === "plugin-path") {
97
97
  // Supports all 12 installer targets: claude_code, codex, cursor, vscode,
98
98
  // opencode (per-project) + windsurf, gemini, zed, openclaw, antigravity,
99
99
  // cline, kimi (global). Each tool stores the bearer in its own shape.
100
- async function _resolveBearer() {
100
+ function _kimiContextRequested(options = {}) {
101
+ const forceTool = (process.env.PATCHCORD_FORCE_TOOL || process.env.PATCHCORD_TOOL || "").toLowerCase();
102
+ return options.preferKimi === true
103
+ || forceTool === "kimi"
104
+ || Boolean(process.env.KIMI_CLI)
105
+ || Boolean(process.env.KIMI_SESSION_ID)
106
+ || Boolean(process.env.KIMI_CONFIG_FILE)
107
+ || Boolean(process.env.KIMI_MCP_CONFIG_FILE);
108
+ }
109
+
110
+ async function _resolveBearer(options = {}) {
101
111
  const { readFileSync } = await import("fs");
112
+ const preferKimi = _kimiContextRequested(options);
102
113
 
103
114
  // Strict JSON first; fall back to JSONC stripping for zed/gemini-style
104
115
  // settings (which allow // /* */ trailing commas). The fallback strips
@@ -188,15 +199,17 @@ async function _resolveBearer() {
188
199
  };
189
200
 
190
201
  // Per-project (walk up from cwd). First win.
191
- const projectReaders = [
202
+ const kimiProjectReader = (cwd) => readJsonAt(join(cwd, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
203
+ const defaultProjectReaders = [
192
204
  (cwd) => readJsonAt(join(cwd, ".mcp.json"), ["mcpServers", "patchcord"], "claude_code"),
193
205
  (cwd) => readJsonAt(join(cwd, ".cursor", "mcp.json"), ["mcpServers", "patchcord"], "cursor"),
194
206
  (cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode"),
195
207
  (cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode"),
196
208
  (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"),
199
209
  ];
210
+ const projectReaders = preferKimi
211
+ ? [kimiProjectReader, ...defaultProjectReaders]
212
+ : [...defaultProjectReaders, kimiProjectReader];
200
213
  let dir = process.cwd();
201
214
  while (dir && dir !== "/") {
202
215
  for (const r of projectReaders) {
@@ -235,16 +248,18 @@ async function _resolveBearer() {
235
248
  );
236
249
  })();
237
250
 
238
- const globalCandidates = [
251
+ const kimiGlobalReader = () => readJsonAt(join(HOME, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
252
+ const defaultGlobalCandidates = [
239
253
  () => readJsonAt(join(HOME, ".codeium", "windsurf", "mcp_config.json"), ["mcpServers", "patchcord"], "windsurf"),
240
254
  () => readJsonAt(join(HOME, ".gemini", "settings.json"), ["mcpServers", "patchcord"], "gemini"),
241
255
  () => readJsonAt(zedPath, ["context_servers", "patchcord"], "zed"),
242
256
  () => readJsonAt(join(HOME, ".openclaw", "openclaw.json"), ["mcp", "servers", "patchcord"], "openclaw"),
243
257
  () => readJsonAt(join(HOME, ".gemini", "antigravity", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
244
258
  ...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
245
- // Global Kimi fallback (only if no per-project .kimi/mcp.json was found)
246
- () => readJsonAt(join(HOME, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi"),
247
259
  ];
260
+ const globalCandidates = preferKimi
261
+ ? [kimiGlobalReader, ...defaultGlobalCandidates]
262
+ : [...defaultGlobalCandidates, kimiGlobalReader];
248
263
  for (const r of globalCandidates) {
249
264
  const found = r();
250
265
  if (found) return found;
@@ -1049,21 +1064,17 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1049
1064
  const kimiSubDir = join(HOME, ".kimi", "skills", "patchcord:subscribe");
1050
1065
  let kimiChanged = false;
1051
1066
 
1052
- if (!existsSync(kimiInboxDir)) {
1053
- mkdirSync(kimiInboxDir, { recursive: true });
1054
- cpSync(join(pluginRoot, "per-project-skills", "kimi", "inbox", "SKILL.md"), join(kimiInboxDir, "SKILL.md"));
1055
- kimiChanged = true;
1056
- }
1057
- if (!existsSync(kimiWaitDir)) {
1058
- mkdirSync(kimiWaitDir, { recursive: true });
1059
- cpSync(join(pluginRoot, "per-project-skills", "kimi", "wait", "SKILL.md"), join(kimiWaitDir, "SKILL.md"));
1060
- kimiChanged = true;
1061
- }
1062
- if (!existsSync(kimiSubDir)) {
1063
- mkdirSync(kimiSubDir, { recursive: true });
1064
- cpSync(join(pluginRoot, "per-project-skills", "kimi", "subscribe", "SKILL.md"), join(kimiSubDir, "SKILL.md"));
1065
- kimiChanged = true;
1066
- }
1067
+ const installKimiSkill = (destDir, relSrc) => {
1068
+ const src = join(pluginRoot, "per-project-skills", "kimi", relSrc, "SKILL.md");
1069
+ const dest = join(destDir, "SKILL.md");
1070
+ const changed = !existsSync(dest) || readFileSync(dest, "utf-8") !== readFileSync(src, "utf-8");
1071
+ mkdirSync(destDir, { recursive: true });
1072
+ copyFileSync(src, dest);
1073
+ return changed;
1074
+ };
1075
+ kimiChanged = installKimiSkill(kimiInboxDir, "inbox") || kimiChanged;
1076
+ kimiChanged = installKimiSkill(kimiWaitDir, "wait") || kimiChanged;
1077
+ kimiChanged = installKimiSkill(kimiSubDir, "subscribe") || kimiChanged;
1067
1078
 
1068
1079
  // Install/update stop hook — fires after each Kimi turn to check inbox
1069
1080
  let hookChanged = false;
@@ -1085,8 +1096,34 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1085
1096
  const kept = segments.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-stop-hook"));
1086
1097
  kimiConfig = kept.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
1087
1098
 
1088
- // Append new hook
1089
- kimiConfig = kimiConfig.trimEnd() + `\n\n[[hooks]]\nevent = "Stop"\ncommand = "${kimiHookDest}"\ntimeout = 10\n`;
1099
+ // Install session-start hook
1100
+ const kimiSessionSrc = join(pluginRoot, "scripts", "kimi-session-start.sh");
1101
+ const kimiSessionDest = join(HOME, ".kimi", "patchcord-session-start.sh");
1102
+ if (existsSync(kimiSessionSrc)) {
1103
+ copyFileSync(kimiSessionSrc, kimiSessionDest);
1104
+ chmodSync(kimiSessionDest, 0o755);
1105
+ }
1106
+
1107
+ // Remove existing patchcord session-start hook blocks
1108
+ const segments2 = kimiConfig.split("[[hooks]]");
1109
+ const kept2 = segments2.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-session-start"));
1110
+ kimiConfig = kept2.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
1111
+
1112
+ // Install UserPromptSubmit hook — legacy compatibility for /skill:patchcord:*
1113
+ const kimiPromptSrc = join(pluginRoot, "scripts", "kimi-prompt-hook.sh");
1114
+ const kimiPromptDest = join(HOME, ".kimi", "patchcord-prompt-hook.sh");
1115
+ if (existsSync(kimiPromptSrc)) {
1116
+ copyFileSync(kimiPromptSrc, kimiPromptDest);
1117
+ chmodSync(kimiPromptDest, 0o755);
1118
+ }
1119
+
1120
+ // Remove existing patchcord prompt hook blocks
1121
+ const segments3 = kimiConfig.split("[[hooks]]");
1122
+ const kept3 = segments3.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-prompt-hook"));
1123
+ kimiConfig = kept3.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
1124
+
1125
+ // Append new hooks
1126
+ kimiConfig = kimiConfig.trimEnd() + `\n\n[[hooks]]\nevent = "UserPromptSubmit"\ncommand = "${kimiPromptDest}"\ntimeout = 5\n\n[[hooks]]\nevent = "SessionStart"\ncommand = "${kimiSessionDest}"\ntimeout = 10\n\n[[hooks]]\nevent = "Stop"\ncommand = "${kimiHookDest}"\ntimeout = 10\n`;
1090
1127
 
1091
1128
  mkdirSync(dirname(kimiConfigPath), { recursive: true });
1092
1129
  writeFileSync(kimiConfigPath, kimiConfig);
@@ -1106,7 +1143,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1106
1143
 
1107
1144
  const kimiParts = [];
1108
1145
  if (kimiChanged) kimiParts.push("skills");
1109
- if (hookChanged) kimiParts.push("stop hook");
1146
+ if (hookChanged) kimiParts.push("hooks");
1110
1147
  if (subChanged) kimiParts.push("subscribe script");
1111
1148
  if (kimiParts.length > 0) {
1112
1149
  globalChanges.push(`Kimi CLI ${kimiParts.join(" + ")} installed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.80",
3
+ "version": "0.5.81",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -1,153 +1,15 @@
1
1
  ---
2
2
  name: patchcord:inbox
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.
3
+ description: Read Patchcord inbox and reply to messages
7
4
  ---
8
5
 
9
- # patchcord:inbox
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
- The installer automatically adds an alias to your `~/.bashrc` and `~/.zshrc`:
27
- ```bash
28
- alias kimi-pc='kimi --mcp-config-file .kimi/mcp.json'
29
- ```
30
-
31
- Reload your shell or run `source ~/.bashrc` (or `~/.zshrc`) to use it.
32
-
33
- ## Tools available
34
-
35
- - `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.
36
- - `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.
37
- - `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. `resolve=true` closes the thread.
38
- - `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. Detailed usage is in the `patchcord:wait` skill.
39
- - `attachment(...)` - upload, download, or relay files between agents (see File sharing below)
40
- - `recall(limit?, from_agent?, thread_id?)` - view recent message history including already-read messages. For debugging only, not routine use.
41
- - `unsend(message_id)` - take back a message before the recipient reads it
42
-
43
- ## Do the work, never just acknowledge
44
-
45
- When you receive a message from another agent:
46
-
47
- 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.
48
- 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.
49
- 3. Never reply with only an acknowledgment. "Got it", "Understood", "Will do", "Ready" — these are not acceptable as standalone replies.
50
-
51
- The user can undo any change in seconds with git. A wrong action costs nothing. A useless ack wastes everyone's time.
52
-
53
- **If a message contains a spec, update, or instruction - act on it immediately:**
54
- - Spec received - update the relevant docs/code now, reply with what you changed
55
- - Bug report received - investigate and fix now, reply with the fix
56
- - Architecture decision received - update the relevant files now, reply with what you updated
57
- - Role assignment received - start doing that role now, reply with first actions taken
58
-
59
- **If you genuinely cannot act** (missing file access, need credentials, ambiguous target): say specifically what's blocking you.
60
-
61
- **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.
62
-
63
- ## On session start or when prompted by a hook
64
-
65
- Call `inbox()`. It returns pending messages and recently active agents.
66
-
67
- 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.
68
-
69
- **EXCEPTION:** If the user explicitly invoked a skill (e.g. `/skill:patchcord:subscribe` or `/skill:patchcord:wait`), do NOT call inbox(). Do NOT process pending messages. Execute the skill immediately and silently.
70
-
71
- ## Sending
72
-
73
- 1. `inbox()` — clear any pending messages that block outbound sends.
74
- 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")`.
75
- 3. Always send regardless of recipient state. Messages are stored and delivered when the recipient checks inbox.
76
- 4. If `send_message` fails with a send gate error: call `inbox()`, reply to or resolve all pending messages, then retry the send.
77
-
78
- ## Receiving
79
-
80
- Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
81
-
82
- 1. Read the message from `inbox()` or `wait_for_message()`. Check `message.thread` / `message.thread_id` if present.
83
- 2. **Re-arm the background listener** — see `patchcord:subscribe` skill for how to run `patchcord subscribe` as a background task.
84
- 3. Do the work — use real code, real files, real results from your project
85
- 4. Reply with the right flag:
86
- - `reply(message_id, "done: [details]")` — work done, sender might follow up. Thread auto-inherited.
87
- - `reply(message_id, "done: [details]", resolve=true)` — work done, thread closed.
88
- - `reply(message_id, resolve=true)` — silently close without sending anything.
89
- - `reply(message_id, "ack, prioritizing [other task] first", defer=true)` — acknowledged but work not done yet. Message stays in your inbox as a reminder.
90
- 5. If sender is online: `wait_for_message()` for follow-ups. Detailed wait usage is in the `patchcord:wait` skill.
91
-
92
- 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.
93
-
94
- Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
95
-
96
- ## Threads
97
-
98
- Named threads group related messages between a pair of agents. Use them for multi-turn tasks that need their own context.
99
-
100
- - **Start**: `send_message("backend", "track this here", thread="deploy-review")`
101
- - **Reply stays in thread automatically** — `reply()` inherits `thread_id` from the message you're replying to.
102
- - **Close**: `reply(message_id, "done", resolve=true)` — closes the thread and notifies sender.
103
- - **Filter history**: `recall(thread_id="<uuid>")` — only messages in that thread.
104
-
105
- `inbox()` `groups` field clusters pending messages by thread. Each group: `{ thread_id, thread_title, messages }`. `thread_id: null` = pair-level.
106
-
107
- ## Cross-user messaging (Gate)
108
-
109
- 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).
110
-
111
- ### Humans
112
-
113
- - Humans are NOT in the agents list. Use `send_message("@username", "...")` anyway — they don't need to be online or in the roster.
114
- - The message goes through their Gate for approval. It may be held pending their approval (cap 5, 7-day TTL).
115
- - Write plainly: who you are, what you need, no raw JSON or logs.
116
-
117
- ## File sharing
118
-
119
- **Files on disk → `patchcord upload` (CLI, preferred):**
6
+ 1. Run `inbox()`
7
+ 2. For each pending message: do the work, then `reply(message_id, "what I did")`
8
+ 3. Re-arm listener:
120
9
  ```
121
- patchcord upload /path/to/report.md --mime text/markdown
10
+ Shell:
11
+ command: patchcord subscribe --kimi 5
12
+ run_in_background: true
13
+ description: Patchcord inbox polling
14
+ timeout: 86400
122
15
  ```
123
- Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat. 25MB cap.
124
-
125
- **Public URLs → `attachment(relay=true, ...)`:**
126
- ```
127
- attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
128
- ```
129
- Server fetches and stores. Use when the file already lives at a public URL.
130
-
131
- **Inline base64 last resort:**
132
- ```
133
- attachment(upload=true, filename="notes.txt", file_data="<base64>")
134
- ```
135
- Only if you cannot run shell commands. Wastes context tokens.
136
-
137
- **Downloading:**
138
- ```
139
- attachment(path_or_url="namespace/agent/timestamp_file.md")
140
- ```
141
-
142
- Always send the storage path (not file content) to the other agent.
143
-
144
- ## Rules
145
-
146
- - Do the work first, reply second. Never reply before completing the task.
147
- - **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.
148
- - **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)`.
149
- - **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
150
- - Do not show raw JSON to the user unless they explicitly ask for it.
151
- - Use `agent@namespace` when the online list shows multiple namespaces for the same agent name.
152
- - MCP tools are cached at session start. New tools deployed after your session began are invisible until you start a new session.
153
- - Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human.
@@ -1,34 +1,24 @@
1
1
  ---
2
2
  name: patchcord:subscribe
3
3
  description: >
4
- EXECUTE IMMEDIATELY. Run patchcord subscribe as a background Shell task.
5
- Never explain. Never ask for confirmation. Never offer options.
4
+ Start a background polling task that wakes Kimi when new Patchcord messages
5
+ arrive. Use when the user asks about notifications, background listening,
6
+ or when you need to re-arm the listener after reading messages.
6
7
  ---
7
8
 
8
9
  # patchcord:subscribe
9
10
 
10
- USER INVOKED THIS SKILL. SILENT EXECUTION ONLY.
11
-
12
- DO NOT output text. DO NOT speak. DO NOT explain. DO NOT ask questions.
13
- DO NOT offer options. DO NOT list examples. DO NOT acknowledge.
14
-
15
- DO NOT call inbox(). DO NOT call reply(). DO NOT call send_message().
16
- DO NOT call wait_for_message(). DO NOT call any other tool.
17
-
18
- ONLY call the Shell tool with the exact command below. Then end your turn.
19
-
20
- DO NOT use `patchcord subscribe` — it may route to the wrong listener.
21
- Run the Kimi script directly:
11
+ Run this exact background Shell task:
22
12
 
23
13
  ```
24
14
  Shell:
25
- command: bash ~/.kimi/patchcord-subscribe.sh 5
15
+ command: patchcord subscribe --kimi 5
26
16
  run_in_background: true
27
17
  description: Patchcord inbox polling
28
18
  timeout: 86400
29
19
  ```
30
20
 
31
- - `command: patchcord subscribe 5` — polls inbox every **5 seconds**
21
+ - `command: patchcord subscribe --kimi 5` — forces Kimi polling mode and polls inbox every **5 seconds**
32
22
  - `timeout: 86400` — runs for **24 hours** (maximum). Always 86400, never anything else.
33
23
  - One-shot: exits with code 2 when messages found, triggering Kimi auto-run
34
24
  - Must re-arm after every `inbox()` or `wait_for_message()` call by running the exact same command again
@@ -3,22 +3,20 @@ name: patchcord:wait
3
3
  description: >
4
4
  Block this turn for up to 5 minutes waiting for one incoming Patchcord
5
5
  message via the wait_for_message MCP tool. Single blocking call, no
6
- background process.
6
+ background process. Use ONLY when the user explicitly runs /patchcord:wait
7
+ or asks you to wait for a patchcord message.
7
8
  ---
8
9
 
9
10
  # patchcord:wait
10
11
 
11
- USER INVOKED THIS SKILL. DO NOT SPEAK. DO NOT EXPLAIN. DO NOT ASK QUESTIONS.
12
- DO NOT OFFER OPTIONS. JUST EXECUTE.
13
-
14
- Use `wait_for_message()` only. Do NOT spawn a background listener.
12
+ User invoked /patchcord:wait or asked you to wait for a Patchcord message. Use `wait_for_message()` only. Do NOT spawn a background listener.
15
13
 
16
14
  Call `wait_for_message()` to block until a message arrives (up to 5 minutes).
17
15
 
18
16
  When a message arrives:
19
17
 
20
18
  1. Read it — the tool returns from, content, and message_id. If it belongs to a thread, `thread` and `thread_id` will be set.
21
- 2. **Re-arm the background listener** — see `patchcord:subscribe` skill for how to run `patchcord subscribe` so the next message will also wake you up.
19
+ 2. **Re-arm the background listener** — see `patchcord:subscribe` skill for how to run `patchcord subscribe --kimi 5` so the next message will also wake you up.
22
20
  3. Do the work described in the message first. Update the file, write the code, fix the bug — whatever it asks.
23
21
  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.
24
22
  5. Tell the human who wrote and what you did about it
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # UserPromptSubmit hook for Kimi CLI — intercepts legacy /skill:patchcord:*
5
+ # prompts and gives Kimi a concrete correction. This is a compatibility shim;
6
+ # SessionStart + Stop hooks are the primary Kimi integration.
7
+ #
8
+ # Kimi silently discards stdout from UserPromptSubmit hooks, but exit 2 + stderr
9
+ # is fed back to the LLM as a correction. We exploit this to turn /skill: into
10
+ # direct command execution.
11
+
12
+ command -v jq >/dev/null 2>&1 || exit 0
13
+
14
+ if [ -t 0 ]; then
15
+ INPUT=""
16
+ else
17
+ INPUT=$(cat)
18
+ fi
19
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
20
+
21
+ # Map /skill:patchcord:* to direct command corrections. Kimi Skills are
22
+ # documentation, not executable slash commands.
23
+ case "$PROMPT" in
24
+ */skill:patchcord:subscribe*)
25
+ echo "Start the Patchcord listener immediately as a background Shell task: command 'patchcord subscribe --kimi 5', run_in_background true, timeout 86400. Do not run it in the foreground." >&2
26
+ exit 2
27
+ ;;
28
+ */skill:patchcord:inbox*)
29
+ echo "Execute immediately: call inbox(), do the requested work for each pending message, reply(), then re-arm Patchcord with a background Shell task: patchcord subscribe --kimi 5." >&2
30
+ exit 2
31
+ ;;
32
+ */skill:patchcord:wait*)
33
+ echo "Execute immediately: call wait_for_message() once. If a message arrives, do the work, reply(), then re-arm Patchcord with a background Shell task: patchcord subscribe --kimi 5." >&2
34
+ exit 2
35
+ ;;
36
+ *)
37
+ exit 0
38
+ ;;
39
+ esac
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Patchcord SessionStart hook for Kimi CLI — auto-starts background listener.
5
+ # Runs when a Kimi session starts or resumes. Idempotent: the subscribe script
6
+ # has a pidfile guard that prevents duplicate instances.
7
+
8
+ KIMI_SUB="${HOME}/.kimi/patchcord-subscribe.sh"
9
+ if [ ! -f "$KIMI_SUB" ]; then
10
+ exit 0
11
+ fi
12
+
13
+ # Kimi sends hook metadata on stdin. Use its cwd when present so the
14
+ # subscribe script resolves the right per-project .kimi/mcp.json.
15
+ if [ -t 0 ]; then
16
+ INPUT=""
17
+ else
18
+ INPUT=$(cat || true)
19
+ fi
20
+ if command -v jq >/dev/null 2>&1; then
21
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
22
+ if [ -n "${CWD:-}" ] && [ -d "$CWD" ]; then
23
+ cd "$CWD"
24
+ fi
25
+ fi
26
+
27
+ nohup bash "$KIMI_SUB" 5 >/dev/null 2>&1 &
28
+ exit 0