patchcord 0.5.94 → 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
@@ -1949,7 +1967,10 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1949
1967
  const kcOk = updateJsonConfig(kcPath, (obj) => {
1950
1968
  obj.mcpServers = obj.mcpServers || {};
1951
1969
  obj.mcpServers.patchcord = {
1952
- url: `${serverUrl}/mcp`,
1970
+ // Kimi Code does RFC-9728 OAuth discovery on /mcp and drops the static
1971
+ // bearer header. The /mcp/bearer path serves resource metadata with no
1972
+ // authorization_servers, so the client skips OAuth and sends the header.
1973
+ url: `${serverUrl}/mcp/bearer`,
1953
1974
  headers: {
1954
1975
  Authorization: `Bearer ${token}`,
1955
1976
  "X-Patchcord-Machine": hostname,
@@ -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.94",
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
+ };