patchcord 0.5.79 → 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 +71 -27
- package/package.json +1 -1
- package/per-project-skills/kimi/inbox/SKILL.md +9 -147
- package/per-project-skills/kimi/subscribe/SKILL.md +6 -16
- package/per-project-skills/kimi/wait/SKILL.md +4 -6
- package/scripts/kimi-prompt-hook.sh +39 -0
- package/scripts/kimi-session-start.sh +28 -0
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
|
-
|
|
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
|
|
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
|
|
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;
|
|
@@ -473,12 +488,19 @@ if (cmd === "subscribe") {
|
|
|
473
488
|
// Kimi CLI uses polling instead of WebSocket realtime.
|
|
474
489
|
// Force Kimi mode with --kimi flag, or auto-detect from bearer config.
|
|
475
490
|
let isKimi = forceKimi;
|
|
491
|
+
let bearerInfo = null;
|
|
476
492
|
if (!isKimi) {
|
|
477
|
-
|
|
493
|
+
bearerInfo = await _resolveBearer();
|
|
478
494
|
isKimi = bearerInfo?.tool === "kimi";
|
|
479
495
|
}
|
|
480
496
|
|
|
481
497
|
if (isKimi) {
|
|
498
|
+
// Remove old combined skill so only patchcord:inbox/subscribe/wait appear
|
|
499
|
+
const oldKimiSkillDir = join(HOME, ".kimi", "skills", "patchcord");
|
|
500
|
+
if (existsSync(oldKimiSkillDir)) {
|
|
501
|
+
try { rmSync(oldKimiSkillDir, { recursive: true, force: true }); } catch {}
|
|
502
|
+
}
|
|
503
|
+
|
|
482
504
|
const kimiSubScript = join(HOME, ".kimi", "patchcord-subscribe.sh");
|
|
483
505
|
if (!existsSync(kimiSubScript)) {
|
|
484
506
|
console.error(`Kimi subscribe script not found at ${kimiSubScript}`);
|
|
@@ -1042,21 +1064,17 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1042
1064
|
const kimiSubDir = join(HOME, ".kimi", "skills", "patchcord:subscribe");
|
|
1043
1065
|
let kimiChanged = false;
|
|
1044
1066
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
mkdirSync(kimiSubDir, { recursive: true });
|
|
1057
|
-
cpSync(join(pluginRoot, "per-project-skills", "kimi", "subscribe", "SKILL.md"), join(kimiSubDir, "SKILL.md"));
|
|
1058
|
-
kimiChanged = true;
|
|
1059
|
-
}
|
|
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;
|
|
1060
1078
|
|
|
1061
1079
|
// Install/update stop hook — fires after each Kimi turn to check inbox
|
|
1062
1080
|
let hookChanged = false;
|
|
@@ -1078,8 +1096,34 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1078
1096
|
const kept = segments.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-stop-hook"));
|
|
1079
1097
|
kimiConfig = kept.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
|
|
1080
1098
|
|
|
1081
|
-
//
|
|
1082
|
-
|
|
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`;
|
|
1083
1127
|
|
|
1084
1128
|
mkdirSync(dirname(kimiConfigPath), { recursive: true });
|
|
1085
1129
|
writeFileSync(kimiConfigPath, kimiConfig);
|
|
@@ -1099,7 +1143,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1099
1143
|
|
|
1100
1144
|
const kimiParts = [];
|
|
1101
1145
|
if (kimiChanged) kimiParts.push("skills");
|
|
1102
|
-
if (hookChanged) kimiParts.push("
|
|
1146
|
+
if (hookChanged) kimiParts.push("hooks");
|
|
1103
1147
|
if (subChanged) kimiParts.push("subscribe script");
|
|
1104
1148
|
if (kimiParts.length > 0) {
|
|
1105
1149
|
globalChanges.push(`Kimi CLI ${kimiParts.join(" + ")} installed`);
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|