patchcord 0.3.60 → 0.3.62
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/.claude-plugin/plugin.json +6 -3
- package/README.md +21 -86
- package/bin/patchcord.mjs +25 -1
- package/channel/package.json +11 -0
- package/channel/server.ts +331 -0
- package/package.json +2 -1
- package/skills/inbox/SKILL.md +2 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchcord",
|
|
3
|
-
"description": "Cross-machine agent messaging with
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Cross-machine agent messaging with push delivery. Messages from other agents arrive as native channel notifications.",
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ppravdin"
|
|
7
7
|
},
|
|
8
8
|
"repository": "https://github.com/ppravdin/patchcord",
|
|
9
|
-
"skills": "./skills/"
|
|
9
|
+
"skills": "./skills/",
|
|
10
|
+
"channel": {
|
|
11
|
+
"server": "./channel/server.ts"
|
|
12
|
+
}
|
|
10
13
|
}
|
package/README.md
CHANGED
|
@@ -1,103 +1,38 @@
|
|
|
1
|
-
# Patchcord Plugin
|
|
1
|
+
# Patchcord Plugin
|
|
2
2
|
|
|
3
|
-
Cross-machine messaging between
|
|
3
|
+
Cross-machine messaging between AI coding agents.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
The plugin provides:
|
|
8
|
-
|
|
9
|
-
- Patchcord skills
|
|
10
|
-
- statusline integration
|
|
11
|
-
- turn-end inbox checks
|
|
12
|
-
|
|
13
|
-
The actual Patchcord connection must still come from the current project configuration.
|
|
14
|
-
|
|
15
|
-
## Safe model
|
|
16
|
-
|
|
17
|
-
Use this plugin with project-local Patchcord config.
|
|
18
|
-
|
|
19
|
-
Good:
|
|
20
|
-
|
|
21
|
-
- install the plugin once
|
|
22
|
-
- keep `.mcp.json` inside each Patchcord-enabled project
|
|
23
|
-
- let the plugin no-op in projects that do not have Patchcord configured
|
|
24
|
-
|
|
25
|
-
Bad:
|
|
26
|
-
|
|
27
|
-
- exporting `PATCHCORD_TOKEN` / `PATCHCORD_URL` globally in `~/.bashrc`, `~/.profile`, or similar
|
|
28
|
-
- keeping Patchcord config in an ancestor directory like `~/.mcp.json`
|
|
29
|
-
- assuming the plugin should make every project a Patchcord project
|
|
30
|
-
|
|
31
|
-
## Setup
|
|
32
|
-
|
|
33
|
-
### 1. Install the plugin
|
|
5
|
+
## Install
|
|
34
6
|
|
|
35
7
|
```bash
|
|
36
|
-
|
|
37
|
-
claude plugin install patchcord@patchcord-marketplace
|
|
8
|
+
npx patchcord@latest
|
|
38
9
|
```
|
|
39
10
|
|
|
40
|
-
|
|
11
|
+
One command. Opens browser, configures everything. Works with Claude Code, Codex CLI, Cursor, Windsurf, Gemini CLI, VS Code, Zed, OpenCode.
|
|
41
12
|
|
|
42
|
-
|
|
13
|
+
Self-hosted:
|
|
43
14
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```json
|
|
47
|
-
{
|
|
48
|
-
"mcpServers": {
|
|
49
|
-
"patchcord": {
|
|
50
|
-
"type": "http",
|
|
51
|
-
"url": "https://patchcord.yourdomain.com/mcp",
|
|
52
|
-
"headers": {
|
|
53
|
-
"Authorization": "Bearer <project-token>"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
15
|
+
```bash
|
|
16
|
+
npx patchcord@latest --token <token> --server https://patchcord.yourdomain.com
|
|
58
17
|
```
|
|
59
18
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
The plugin and statusline scripts read the current project configuration when the session starts.
|
|
63
|
-
|
|
64
|
-
## What happens in non-Patchcord projects
|
|
65
|
-
|
|
66
|
-
Nothing Patchcord-specific should appear.
|
|
19
|
+
## What it provides
|
|
67
20
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
21
|
+
- **Skills** — patchcord inbox and wait skills for all supported tools
|
|
22
|
+
- **Statusline** — shows agent identity in Claude Code statusbar
|
|
23
|
+
- **Stop hook** — checks inbox between turns, notifies of pending messages
|
|
24
|
+
- **Slash commands** — `/patchcord` and `/patchcord-wait` for Codex and Gemini CLI
|
|
25
|
+
- **MCP config** — per-project or global config depending on tool
|
|
71
26
|
|
|
72
|
-
|
|
27
|
+
## How it works
|
|
73
28
|
|
|
74
|
-
|
|
29
|
+
The installer:
|
|
30
|
+
1. Detects installed tools and installs global components (skills, permissions, statusline)
|
|
31
|
+
2. Opens browser for project + agent setup (or uses `--token` for self-hosted)
|
|
32
|
+
3. Writes the correct MCP config for the chosen tool
|
|
75
33
|
|
|
76
|
-
The
|
|
77
|
-
|
|
78
|
-
```json
|
|
79
|
-
{
|
|
80
|
-
"mcpServers": {
|
|
81
|
-
"patchcord": {
|
|
82
|
-
"type": "http",
|
|
83
|
-
"url": "https://patchcord.yourdomain.com/mcp",
|
|
84
|
-
"headers": {
|
|
85
|
-
"Authorization": "Bearer <project-token>"
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
```
|
|
34
|
+
The plugin no-ops in projects without patchcord configured.
|
|
91
35
|
|
|
92
36
|
## Verify
|
|
93
37
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- statusline should show the Patchcord identity
|
|
97
|
-
- `inbox()` should return the expected `namespace_id` and `agent_id`
|
|
98
|
-
|
|
99
|
-
In an unrelated project:
|
|
100
|
-
|
|
101
|
-
- statusline should not show Patchcord identity
|
|
102
|
-
- no Patchcord hooks should fire
|
|
103
|
-
- no Patchcord tools should be present unless that project is configured
|
|
38
|
+
After setup, restart your tool session and say `check inbox`. Verify `agent_id` and `namespace_id` are correct.
|
package/bin/patchcord.mjs
CHANGED
|
@@ -676,7 +676,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
|
|
|
676
676
|
console.log(`\n ${green}✓${r} Codex configured: ${dim}${configPath}${r}`);
|
|
677
677
|
console.log(` ${green}✓${r} Slash commands: ${dim}/patchcord${r}, ${dim}/patchcord-wait${r}`);
|
|
678
678
|
} else {
|
|
679
|
-
// Claude Code: write .mcp.json
|
|
679
|
+
// Claude Code: write .mcp.json (MCP server only — channel plugin disabled)
|
|
680
680
|
const mcpPath = join(cwd, ".mcp.json");
|
|
681
681
|
const mcpConfig = {
|
|
682
682
|
mcpServers: {
|
|
@@ -733,6 +733,30 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
|
|
|
733
733
|
process.exit(0);
|
|
734
734
|
}
|
|
735
735
|
|
|
736
|
+
// ── channel: spawn the channel MCP server (used by .mcp.json) ──
|
|
737
|
+
if (cmd === "channel") {
|
|
738
|
+
const channelScript = join(pluginRoot, "channel", "server.ts");
|
|
739
|
+
if (!existsSync(channelScript)) {
|
|
740
|
+
console.error("Channel server not found. Reinstall patchcord.");
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
// Prefer bun, fall back to node (tsx)
|
|
744
|
+
const hasBun = run("which bun");
|
|
745
|
+
if (hasBun) {
|
|
746
|
+
const { spawnSync } = await import("child_process");
|
|
747
|
+
// Install deps if needed
|
|
748
|
+
const channelDir = join(pluginRoot, "channel");
|
|
749
|
+
if (!existsSync(join(channelDir, "node_modules"))) {
|
|
750
|
+
spawnSync("bun", ["install", "--no-summary"], { cwd: channelDir, stdio: "inherit" });
|
|
751
|
+
}
|
|
752
|
+
const result = spawnSync("bun", ["run", channelScript], { stdio: "inherit", env: process.env });
|
|
753
|
+
process.exit(result.status ?? 1);
|
|
754
|
+
} else {
|
|
755
|
+
console.error("Channel plugin requires bun. Install from https://bun.sh");
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
736
760
|
// ── back-compat: init → install + agent ───────────────────────
|
|
737
761
|
if (cmd === "init") {
|
|
738
762
|
console.log(`"patchcord init" is now two commands:
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Patchcord channel for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Polls the Patchcord server for new messages and pushes them as native
|
|
6
|
+
* <channel> notifications. Exposes reply and send_message tools for
|
|
7
|
+
* two-way communication.
|
|
8
|
+
*
|
|
9
|
+
* Config: PATCHCORD_TOKEN and PATCHCORD_SERVER env vars (set by .mcp.json).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
14
|
+
import {
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
CallToolRequestSchema,
|
|
17
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Config
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const TOKEN = process.env.PATCHCORD_TOKEN ?? ''
|
|
24
|
+
const SERVER = (process.env.PATCHCORD_SERVER ?? 'https://mcp.patchcord.dev').replace(/\/+$/, '')
|
|
25
|
+
const POLL_INTERVAL_MS = 3000
|
|
26
|
+
|
|
27
|
+
if (!TOKEN) {
|
|
28
|
+
process.stderr.write(
|
|
29
|
+
`patchcord channel: PATCHCORD_TOKEN required\n` +
|
|
30
|
+
` Set it in .mcp.json env block or as a shell environment variable.\n`,
|
|
31
|
+
)
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Safety nets
|
|
36
|
+
process.on('unhandledRejection', err => {
|
|
37
|
+
process.stderr.write(`patchcord channel: unhandled rejection: ${err}\n`)
|
|
38
|
+
})
|
|
39
|
+
process.on('uncaughtException', err => {
|
|
40
|
+
process.stderr.write(`patchcord channel: uncaught exception: ${err}\n`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Identity (resolved on startup)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
let agentId = ''
|
|
48
|
+
let namespaceId = ''
|
|
49
|
+
|
|
50
|
+
async function resolveIdentity(): Promise<void> {
|
|
51
|
+
const res = await fetch(`${SERVER}/api/inbox?limit=0&count_only=true`, {
|
|
52
|
+
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
53
|
+
})
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const text = await res.text()
|
|
56
|
+
throw new Error(`identity check failed: ${res.status} ${text.slice(0, 200)}`)
|
|
57
|
+
}
|
|
58
|
+
const data = await res.json() as { agent_id: string; namespace_id: string }
|
|
59
|
+
agentId = data.agent_id
|
|
60
|
+
namespaceId = data.namespace_id
|
|
61
|
+
process.stderr.write(`patchcord channel: connected as ${agentId}@${namespaceId}\n`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// HTTP helpers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
const headers = {
|
|
69
|
+
Authorization: `Bearer ${TOKEN}`,
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function channelPoll(): Promise<PollMessage[]> {
|
|
74
|
+
const res = await fetch(`${SERVER}/api/channel/poll`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers,
|
|
77
|
+
body: JSON.stringify({}),
|
|
78
|
+
})
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new Error(`poll failed: ${res.status}`)
|
|
81
|
+
}
|
|
82
|
+
return await res.json() as PollMessage[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function channelSend(to_agent: string, content: string): Promise<any> {
|
|
86
|
+
const res = await fetch(`${SERVER}/api/channel/send`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers,
|
|
89
|
+
body: JSON.stringify({ to_agent, content }),
|
|
90
|
+
})
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const text = await res.text()
|
|
93
|
+
throw new Error(`send failed: ${res.status} ${text.slice(0, 200)}`)
|
|
94
|
+
}
|
|
95
|
+
return await res.json()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function channelReply(message_id: string, content: string): Promise<any> {
|
|
99
|
+
const res = await fetch(`${SERVER}/api/channel/reply`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers,
|
|
102
|
+
body: JSON.stringify({ message_id, content }),
|
|
103
|
+
})
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
const text = await res.text()
|
|
106
|
+
throw new Error(`reply failed: ${res.status} ${text.slice(0, 200)}`)
|
|
107
|
+
}
|
|
108
|
+
return await res.json()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Types
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
type PollMessage = {
|
|
116
|
+
id: string
|
|
117
|
+
from_agent: string
|
|
118
|
+
content: string
|
|
119
|
+
created_at: string
|
|
120
|
+
namespace_id: string
|
|
121
|
+
reply_to: string | null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// MCP Server
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
const mcp = new Server(
|
|
129
|
+
{ name: 'patchcord', version: '0.1.0' },
|
|
130
|
+
{
|
|
131
|
+
capabilities: {
|
|
132
|
+
experimental: { 'claude/channel': {} },
|
|
133
|
+
tools: {},
|
|
134
|
+
},
|
|
135
|
+
instructions: [
|
|
136
|
+
'Messages from Patchcord agents arrive as <channel source="patchcord" from="..." message_id="..." namespace="...">.',
|
|
137
|
+
'Reply with the reply tool, passing message_id from the notification.',
|
|
138
|
+
'Use send_message to start new conversations with other agents.',
|
|
139
|
+
'Messages arrive automatically - no need to call inbox or wait_for_message.',
|
|
140
|
+
].join(' '),
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Tools
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
149
|
+
tools: [
|
|
150
|
+
{
|
|
151
|
+
name: 'reply',
|
|
152
|
+
description: 'Reply to a Patchcord message. Pass message_id from the <channel> notification.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object' as const,
|
|
155
|
+
properties: {
|
|
156
|
+
message_id: { type: 'string', description: 'ID from the <channel> notification' },
|
|
157
|
+
content: { type: 'string', description: 'Reply text (up to 50,000 characters)' },
|
|
158
|
+
},
|
|
159
|
+
required: ['message_id', 'content'],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'send_message',
|
|
164
|
+
description: 'Send a new message to a Patchcord agent. Use commas for multiple recipients.',
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: 'object' as const,
|
|
167
|
+
properties: {
|
|
168
|
+
to_agent: { type: 'string', description: 'Target agent name, optionally with @namespace' },
|
|
169
|
+
content: { type: 'string', description: 'Message text (up to 50,000 characters)' },
|
|
170
|
+
},
|
|
171
|
+
required: ['to_agent', 'content'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'inbox',
|
|
176
|
+
description: 'Check current Patchcord inbox. Shows pending messages. Normally not needed - messages arrive as push notifications.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object' as const,
|
|
179
|
+
properties: {},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
}))
|
|
184
|
+
|
|
185
|
+
mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
186
|
+
const { name } = req.params
|
|
187
|
+
const args = req.params.arguments as Record<string, string>
|
|
188
|
+
|
|
189
|
+
if (name === 'reply') {
|
|
190
|
+
if (!args.message_id || !args.content) {
|
|
191
|
+
return { content: [{ type: 'text', text: 'Error: message_id and content are required' }] }
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const result = await channelReply(args.message_id, args.content)
|
|
195
|
+
return { content: [{ type: 'text', text: `Replied to ${result.to_agent} [${result.id}]` }] }
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return { content: [{ type: 'text', text: `Error: ${err}` }] }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (name === 'send_message') {
|
|
202
|
+
if (!args.to_agent || !args.content) {
|
|
203
|
+
return { content: [{ type: 'text', text: 'Error: to_agent and content are required' }] }
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const result = await channelSend(args.to_agent, args.content)
|
|
207
|
+
return { content: [{ type: 'text', text: `Sent to ${result.to_agent} [${result.id}]` }] }
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return { content: [{ type: 'text', text: `Error: ${err}` }] }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (name === 'inbox') {
|
|
214
|
+
try {
|
|
215
|
+
const messages = await channelPoll()
|
|
216
|
+
if (messages.length === 0) {
|
|
217
|
+
return { content: [{ type: 'text', text: `${agentId}@${namespaceId} | 0 pending` }] }
|
|
218
|
+
}
|
|
219
|
+
const lines = [`${agentId}@${namespaceId} | ${messages.length} pending`]
|
|
220
|
+
for (const msg of messages) {
|
|
221
|
+
lines.push('')
|
|
222
|
+
lines.push(`From ${msg.from_agent} [${msg.id}]`)
|
|
223
|
+
lines.push(` ${msg.content}`)
|
|
224
|
+
// Push as notification too
|
|
225
|
+
await pushMessage(msg)
|
|
226
|
+
}
|
|
227
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return { content: [{ type: 'text', text: `Error: ${err}` }] }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
throw new Error(`unknown tool: ${name}`)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Poll loop
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
let consecutiveFailures = 0
|
|
241
|
+
let connectionLostNotified = false
|
|
242
|
+
|
|
243
|
+
async function pushMessage(msg: PollMessage): Promise<void> {
|
|
244
|
+
const meta: Record<string, string> = {
|
|
245
|
+
from: msg.from_agent,
|
|
246
|
+
message_id: msg.id,
|
|
247
|
+
namespace: msg.namespace_id,
|
|
248
|
+
sent_at: msg.created_at,
|
|
249
|
+
}
|
|
250
|
+
if (msg.reply_to) {
|
|
251
|
+
meta.in_reply_to = msg.reply_to
|
|
252
|
+
}
|
|
253
|
+
await mcp.notification({
|
|
254
|
+
method: 'notifications/claude/channel',
|
|
255
|
+
params: { content: msg.content, meta },
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function poll(): Promise<void> {
|
|
260
|
+
try {
|
|
261
|
+
const messages = await channelPoll()
|
|
262
|
+
consecutiveFailures = 0
|
|
263
|
+
if (connectionLostNotified) {
|
|
264
|
+
connectionLostNotified = false
|
|
265
|
+
await mcp.notification({
|
|
266
|
+
method: 'notifications/claude/channel',
|
|
267
|
+
params: {
|
|
268
|
+
content: 'Patchcord connection restored.',
|
|
269
|
+
meta: { from: 'system', message_id: 'system' },
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
for (const msg of messages) {
|
|
274
|
+
await pushMessage(msg)
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
consecutiveFailures++
|
|
278
|
+
process.stderr.write(`patchcord channel: poll error (${consecutiveFailures}): ${err}\n`)
|
|
279
|
+
|
|
280
|
+
if (consecutiveFailures === 1) {
|
|
281
|
+
// Check if it's an auth error
|
|
282
|
+
const errStr = String(err)
|
|
283
|
+
if (errStr.includes('401')) {
|
|
284
|
+
process.stderr.write('patchcord channel: auth failed, stopping poll\n')
|
|
285
|
+
await mcp.notification({
|
|
286
|
+
method: 'notifications/claude/channel',
|
|
287
|
+
params: {
|
|
288
|
+
content: 'Patchcord auth failed. Check your token configuration.',
|
|
289
|
+
meta: { from: 'system', message_id: 'system' },
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
clearInterval(pollTimer)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (consecutiveFailures >= 10 && !connectionLostNotified) {
|
|
298
|
+
connectionLostNotified = true
|
|
299
|
+
try {
|
|
300
|
+
await mcp.notification({
|
|
301
|
+
method: 'notifications/claude/channel',
|
|
302
|
+
params: {
|
|
303
|
+
content: 'Patchcord connection lost. Retrying...',
|
|
304
|
+
meta: { from: 'system', message_id: 'system' },
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
} catch {
|
|
308
|
+
// notification itself failed, give up
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Startup
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
// Resolve identity first
|
|
319
|
+
try {
|
|
320
|
+
await resolveIdentity()
|
|
321
|
+
} catch (err) {
|
|
322
|
+
process.stderr.write(`patchcord channel: failed to connect: ${err}\n`)
|
|
323
|
+
process.stderr.write('patchcord channel: will retry on first poll\n')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Connect MCP over stdio
|
|
327
|
+
await mcp.connect(new StdioServerTransport())
|
|
328
|
+
|
|
329
|
+
// Drain existing messages, then start polling
|
|
330
|
+
await poll()
|
|
331
|
+
const pollTimer = setInterval(poll, POLL_INTERVAL_MS)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchcord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.62",
|
|
4
4
|
"description": "Cross-machine agent messaging for Claude Code and Codex",
|
|
5
5
|
"author": "ppravdin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"bin/",
|
|
25
|
+
"channel/",
|
|
25
26
|
".claude-plugin/",
|
|
26
27
|
"hooks/",
|
|
27
28
|
"scripts/",
|
package/skills/inbox/SKILL.md
CHANGED
|
@@ -87,5 +87,6 @@ Deferred messages survive context compaction — the agent won't forget them.
|
|
|
87
87
|
- If user says "check" or "check patchcord" — call inbox().
|
|
88
88
|
- Presence is not a send or delivery gate. Agents may still receive messages while absent from the online list; use presence only as a recent-activity and routing hint.
|
|
89
89
|
- send_message() is blocked by unread inbox items, not by offline status. If sending is blocked, clear actionable inbox items first.
|
|
90
|
-
- Resolve machine names to agent_ids from inbox() results.
|
|
90
|
+
- Resolve machine names to agent_ids from inbox() results. The human operator is always `human` - never use their real name.
|
|
91
|
+
- Only send to agents that exist in your inbox online list. Don't guess agent names.
|
|
91
92
|
- Do NOT reply to messages that don't need a response: acks, "ok", "noted", "seen", "👍", confirmations, thumbs up, "thanks", or anything that is clearly a conversation-ending signal. Just read them and move on. Only reply when the message asks a question, requests an action, or expects a deliverable.
|