patchcord 0.5.95 → 0.5.97
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
|
@@ -1225,16 +1225,19 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1225
1225
|
copyFileSync(hookScriptSrc, hookScriptDest);
|
|
1226
1226
|
chmodSync(hookScriptDest, 0o755);
|
|
1227
1227
|
|
|
1228
|
-
// Enable hooks feature flag
|
|
1229
|
-
//
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1228
|
+
// Enable the `hooks` feature flag exactly once. Older installs / Codex
|
|
1229
|
+
// versions may leave `codex_hooks = true`, `hooks=true` (no spaces), a
|
|
1230
|
+
// `hooks = false`, or even a duplicate `hooks = true` — any of which makes
|
|
1231
|
+
// config.toml fail to load with a "duplicate key" error. Strip every
|
|
1232
|
+
// feature-flag variant first, then add exactly one under [features].
|
|
1233
|
+
globalCodexContent = globalCodexContent
|
|
1234
|
+
.replace(/^[ \t]*(?:codex_)?hooks[ \t]*=[ \t]*(?:true|false)[ \t]*(?:#[^\n]*)?\r?\n?/gm, "")
|
|
1235
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1236
|
+
.trimEnd() + "\n";
|
|
1237
|
+
if (/^\[features\][ \t]*$/m.test(globalCodexContent)) {
|
|
1238
|
+
globalCodexContent = globalCodexContent.replace(/^(\[features\][ \t]*\r?\n)/m, "$1hooks = true\n");
|
|
1239
|
+
} else {
|
|
1240
|
+
globalCodexContent = globalCodexContent.trimEnd() + "\n\n[features]\nhooks = true\n";
|
|
1238
1241
|
}
|
|
1239
1242
|
|
|
1240
1243
|
// Remove any old patchcord stop hook entry from config.toml (moved to hooks.json)
|
|
@@ -1818,6 +1821,24 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1818
1821
|
if (ocOk) {
|
|
1819
1822
|
console.log(`\n ${green}✓${r} OpenCode configured: ${dim}${ocPath}${r}`);
|
|
1820
1823
|
}
|
|
1824
|
+
// Slash commands → .opencode/commands/
|
|
1825
|
+
try {
|
|
1826
|
+
const ocCmdDir = join(cwd, ".opencode", "commands");
|
|
1827
|
+
mkdirSync(ocCmdDir, { recursive: true });
|
|
1828
|
+
for (const f of ["patchcord-inbox.md", "patchcord-wait.md"]) {
|
|
1829
|
+
cpSync(join(pluginRoot, "commands", "opencode", f), join(ocCmdDir, f));
|
|
1830
|
+
}
|
|
1831
|
+
console.log(` ${green}✓${r} Commands installed: ${dim}/patchcord-inbox${r}, ${dim}/patchcord-wait${r}`);
|
|
1832
|
+
} catch {}
|
|
1833
|
+
// Realtime-wake plugin → .opencode/plugins/ (spawns `patchcord subscribe`
|
|
1834
|
+
// and injects a prompt via client.session.prompt on each new message).
|
|
1835
|
+
try {
|
|
1836
|
+
const ocPluginDir = join(cwd, ".opencode", "plugins");
|
|
1837
|
+
mkdirSync(ocPluginDir, { recursive: true });
|
|
1838
|
+
cpSync(join(pluginRoot, "plugins", "opencode", "patchcord.js"), join(ocPluginDir, "patchcord.js"));
|
|
1839
|
+
console.log(` ${green}✓${r} Realtime-wake plugin installed: ${dim}.opencode/plugins/patchcord.js${r}`);
|
|
1840
|
+
console.log(` ${dim}New Patchcord messages will wake the agent automatically.${r}`);
|
|
1841
|
+
} catch {}
|
|
1821
1842
|
} else if (isOpenClaw) {
|
|
1822
1843
|
// OpenClaw: global ~/.openclaw/openclaw.json → mcp.servers
|
|
1823
1844
|
// 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.97",
|
|
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
|
+
};
|