patchcord 0.5.95 → 0.5.96
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
|
@@ -1818,6 +1818,24 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1818
1818
|
if (ocOk) {
|
|
1819
1819
|
console.log(`\n ${green}✓${r} OpenCode configured: ${dim}${ocPath}${r}`);
|
|
1820
1820
|
}
|
|
1821
|
+
// Slash commands → .opencode/commands/
|
|
1822
|
+
try {
|
|
1823
|
+
const ocCmdDir = join(cwd, ".opencode", "commands");
|
|
1824
|
+
mkdirSync(ocCmdDir, { recursive: true });
|
|
1825
|
+
for (const f of ["patchcord-inbox.md", "patchcord-wait.md"]) {
|
|
1826
|
+
cpSync(join(pluginRoot, "commands", "opencode", f), join(ocCmdDir, f));
|
|
1827
|
+
}
|
|
1828
|
+
console.log(` ${green}✓${r} Commands installed: ${dim}/patchcord-inbox${r}, ${dim}/patchcord-wait${r}`);
|
|
1829
|
+
} catch {}
|
|
1830
|
+
// Realtime-wake plugin → .opencode/plugins/ (spawns `patchcord subscribe`
|
|
1831
|
+
// and injects a prompt via client.session.prompt on each new message).
|
|
1832
|
+
try {
|
|
1833
|
+
const ocPluginDir = join(cwd, ".opencode", "plugins");
|
|
1834
|
+
mkdirSync(ocPluginDir, { recursive: true });
|
|
1835
|
+
cpSync(join(pluginRoot, "plugins", "opencode", "patchcord.js"), join(ocPluginDir, "patchcord.js"));
|
|
1836
|
+
console.log(` ${green}✓${r} Realtime-wake plugin installed: ${dim}.opencode/plugins/patchcord.js${r}`);
|
|
1837
|
+
console.log(` ${dim}New Patchcord messages will wake the agent automatically.${r}`);
|
|
1838
|
+
} catch {}
|
|
1821
1839
|
} else if (isOpenClaw) {
|
|
1822
1840
|
// OpenClaw: global ~/.openclaw/openclaw.json → mcp.servers
|
|
1823
1841
|
// Try CLI first, fall back to direct file write
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Check the Patchcord inbox and reply to messages from other agents
|
|
3
|
+
---
|
|
4
|
+
Call the patchcord inbox tool. The first header line is YOUR identity (the recipient); each message's real sender is on its "From X" line — never confuse the header with the sender.
|
|
5
|
+
|
|
6
|
+
For each pending message, classify and act:
|
|
7
|
+
- ACK ("thanks", "noted", "ok", "works", "👍") → reply(message_id, resolve=true) with NO content. Never reply to an ack with text.
|
|
8
|
+
- BLOCKED (can't act right now) → reply(message_id, "<reason>", defer=true).
|
|
9
|
+
- ACTIONABLE → do the work first, THEN reply(message_id, "<concrete summary with file paths/results>").
|
|
10
|
+
|
|
11
|
+
Do the work before replying; never reply with a bare acknowledgement. Then tell me who wrote, what they asked, and what you did.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Block and wait for one incoming Patchcord message (up to ~5 minutes)
|
|
3
|
+
---
|
|
4
|
+
Call the patchcord wait_for_message tool — it blocks until a message arrives or ~5 minutes elapse.
|
|
5
|
+
|
|
6
|
+
If a message arrives, handle it the same way as /patchcord-inbox: ACK → reply(message_id, resolve=true) with no content; BLOCKED → reply(message_id, "<reason>", defer=true); ACTIONABLE → do the work first, then reply(message_id, "<concrete summary>").
|
|
7
|
+
|
|
8
|
+
If it times out with nothing, tell me nothing arrived.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchcord",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.96",
|
|
4
4
|
"description": "Cross-machine agent messaging for Claude Code and Codex",
|
|
5
5
|
"author": "ppravdin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"skills/",
|
|
29
29
|
"per-project-skills/",
|
|
30
30
|
"commands/",
|
|
31
|
-
"README.md"
|
|
31
|
+
"README.md",
|
|
32
|
+
"plugins/"
|
|
32
33
|
]
|
|
33
34
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Patchcord realtime-wake plugin for OpenCode.
|
|
2
|
+
//
|
|
3
|
+
// Spawns `patchcord subscribe` — the self-reconnecting Supabase realtime
|
|
4
|
+
// listener (shared with every other client) — and, on each incoming message,
|
|
5
|
+
// injects a prompt into the active session via client.session.prompt(). The
|
|
6
|
+
// agent wakes and checks its inbox. Real-time push, no re-arm.
|
|
7
|
+
//
|
|
8
|
+
// We prompt on the realtime event (not on session.idle) on purpose: re-prompting
|
|
9
|
+
// from session.idle has a documented teardown race in headless `opencode run`
|
|
10
|
+
// mode. Injecting on message arrival sidesteps it.
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
export const PatchcordPlugin = async ({ client, directory }) => {
|
|
14
|
+
let sessionId = null;
|
|
15
|
+
let proc = null;
|
|
16
|
+
|
|
17
|
+
const wake = (from) => {
|
|
18
|
+
if (!sessionId) return; // no session to inject into yet; the listener's
|
|
19
|
+
// 60s pending-heartbeat re-emits, so nothing is lost.
|
|
20
|
+
client.session
|
|
21
|
+
.prompt({
|
|
22
|
+
path: { id: sessionId },
|
|
23
|
+
body: {
|
|
24
|
+
parts: [
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
text:
|
|
28
|
+
`You have a new Patchcord message from ${from}. Call the patchcord ` +
|
|
29
|
+
`inbox tool now and handle each pending message: an ACK ("thanks", ` +
|
|
30
|
+
`"ok", "noted", "works") -> reply(message_id, resolve=true) with NO ` +
|
|
31
|
+
`content; BLOCKED (can't act now) -> reply(message_id, "reason", ` +
|
|
32
|
+
`defer=true); ACTIONABLE -> do the work first, then ` +
|
|
33
|
+
`reply(message_id, "concrete summary"). Then tell me who wrote and ` +
|
|
34
|
+
`what you did.`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
.catch(() => {});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const start = () => {
|
|
43
|
+
if (proc) return;
|
|
44
|
+
try {
|
|
45
|
+
proc = spawn("patchcord", ["subscribe"], {
|
|
46
|
+
cwd: directory,
|
|
47
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
proc = null;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let buf = "";
|
|
54
|
+
proc.stdout.on("data", (chunk) => {
|
|
55
|
+
buf += chunk.toString();
|
|
56
|
+
let nl;
|
|
57
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
58
|
+
const line = buf.slice(0, nl);
|
|
59
|
+
buf = buf.slice(nl + 1);
|
|
60
|
+
if (line.startsWith("PATCHCORD:")) {
|
|
61
|
+
const m = line.match(/from (\S+)/);
|
|
62
|
+
wake(m ? m[1] : "another agent");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
proc.on("error", () => {
|
|
67
|
+
proc = null;
|
|
68
|
+
});
|
|
69
|
+
proc.on("exit", () => {
|
|
70
|
+
proc = null;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Start the listener as soon as the plugin loads.
|
|
75
|
+
start();
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
event: async ({ event }) => {
|
|
79
|
+
const sid =
|
|
80
|
+
event?.session_id ||
|
|
81
|
+
event?.sessionID ||
|
|
82
|
+
event?.properties?.sessionID ||
|
|
83
|
+
event?.properties?.session_id ||
|
|
84
|
+
event?.properties?.info?.id;
|
|
85
|
+
if (sid) sessionId = sid;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|