patchcord 0.3.62 → 0.3.64
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 +45 -8
- package/package.json +1 -2
- package/skills/codex/SKILL.md +72 -25
- package/skills/inbox/SKILL.md +67 -42
- package/skills/wait/SKILL.md +11 -8
- package/skills/web/SKILL.md +47 -31
- package/channel/package.json +0 -11
- package/channel/server.ts +0 -331
package/bin/patchcord.mjs
CHANGED
|
@@ -80,7 +80,7 @@ if (cmd === "plugin-path") {
|
|
|
80
80
|
if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd === "--no-browser" || cmd === "--server") {
|
|
81
81
|
const flags = cmd?.startsWith("--") ? process.argv.slice(2) : process.argv.slice(3);
|
|
82
82
|
const fullStatusline = flags.includes("--full");
|
|
83
|
-
const { readFileSync, writeFileSync } = await import("fs");
|
|
83
|
+
const { readFileSync, writeFileSync, unlinkSync } = await import("fs");
|
|
84
84
|
|
|
85
85
|
function safeReadJson(filePath) {
|
|
86
86
|
try {
|
|
@@ -656,7 +656,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
|
|
|
656
656
|
let existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
657
657
|
// Remove old patchcord config block if present
|
|
658
658
|
existing = existing.replace(/\[mcp_servers\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
659
|
-
existing = existing.trimEnd() + `\n\n[mcp_servers.patchcord-codex]\nurl = "${serverUrl}/mcp/bearer"\nhttp_headers = { "Authorization" = "Bearer ${token}", "X-Patchcord-Machine" = "${hostname}" }\n`;
|
|
659
|
+
existing = existing.trimEnd() + `\n\n[mcp_servers.patchcord-codex]\nurl = "${serverUrl}/mcp/bearer"\nhttp_headers = { "Authorization" = "Bearer ${token}", "X-Patchcord-Machine" = "${hostname}" }\ntool_timeout_sec = 300\n`;
|
|
660
660
|
writeFileSync(configPath, existing);
|
|
661
661
|
// Clean up any PATCHCORD_TOKEN we previously wrote to .env
|
|
662
662
|
const envPath = join(cwd, ".env");
|
|
@@ -668,13 +668,50 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
|
|
|
668
668
|
console.log(` ${green}✓${r} Cleaned PATCHCORD_TOKEN from .env`);
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
|
-
//
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
671
|
+
// Clean up old slash commands if they exist (deprecated in Codex v0.117+)
|
|
672
|
+
const oldPromptsDir = join(codexDir, "prompts");
|
|
673
|
+
for (const f of ["patchcord.md", "patchcord-wait.md"]) {
|
|
674
|
+
const p = join(oldPromptsDir, f);
|
|
675
|
+
if (existsSync(p)) { unlinkSync(p); }
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Install Codex plugin (skills auto-discovered via @patchcord)
|
|
679
|
+
const pluginDir = join(homedir(), ".codex", "plugins", "patchcord");
|
|
680
|
+
mkdirSync(join(pluginDir, ".codex-plugin"), { recursive: true });
|
|
681
|
+
mkdirSync(join(pluginDir, "skills", "patchcord"), { recursive: true });
|
|
682
|
+
mkdirSync(join(pluginDir, "skills", "patchcord-wait"), { recursive: true });
|
|
683
|
+
// Plugin manifest
|
|
684
|
+
writeFileSync(join(pluginDir, ".codex-plugin", "plugin.json"), JSON.stringify({
|
|
685
|
+
name: "patchcord",
|
|
686
|
+
version: JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf-8")).version,
|
|
687
|
+
description: "Cross-machine agent messaging for Codex",
|
|
688
|
+
skills: "./skills/",
|
|
689
|
+
}, null, 2) + "\n");
|
|
690
|
+
// Skills
|
|
691
|
+
const codexSkillSrc = readFileSync(join(pluginRoot, "skills", "codex", "SKILL.md"), "utf-8");
|
|
692
|
+
writeFileSync(join(pluginDir, "skills", "patchcord", "SKILL.md"), codexSkillSrc);
|
|
693
|
+
const waitSkillSrc = readFileSync(join(pluginRoot, "skills", "wait", "SKILL.md"), "utf-8");
|
|
694
|
+
writeFileSync(join(pluginDir, "skills", "patchcord-wait", "SKILL.md"), waitSkillSrc);
|
|
695
|
+
|
|
696
|
+
// Marketplace entry (personal)
|
|
697
|
+
const marketplaceDir = join(homedir(), ".agents", "plugins");
|
|
698
|
+
mkdirSync(marketplaceDir, { recursive: true });
|
|
699
|
+
const marketplacePath = join(marketplaceDir, "marketplace.json");
|
|
700
|
+
let marketplace = { name: "patchcord", plugins: [] };
|
|
701
|
+
if (existsSync(marketplacePath)) {
|
|
702
|
+
try { marketplace = JSON.parse(readFileSync(marketplacePath, "utf-8")); } catch {}
|
|
703
|
+
}
|
|
704
|
+
marketplace.plugins = (marketplace.plugins || []).filter(p => p.name !== "patchcord");
|
|
705
|
+
marketplace.plugins.push({
|
|
706
|
+
name: "patchcord",
|
|
707
|
+
source: { source: "local", path: join(homedir(), ".codex", "plugins", "patchcord") },
|
|
708
|
+
policy: { installation: "INSTALLED_BY_DEFAULT" },
|
|
709
|
+
category: "Productivity",
|
|
710
|
+
});
|
|
711
|
+
writeFileSync(marketplacePath, JSON.stringify(marketplace, null, 2) + "\n");
|
|
712
|
+
|
|
676
713
|
console.log(`\n ${green}✓${r} Codex configured: ${dim}${configPath}${r}`);
|
|
677
|
-
console.log(` ${green}✓${r}
|
|
714
|
+
console.log(` ${green}✓${r} Plugin installed: ${dim}@patchcord${r}, ${dim}@patchcord-wait${r}`);
|
|
678
715
|
} else {
|
|
679
716
|
// Claude Code: write .mcp.json (MCP server only — channel plugin disabled)
|
|
680
717
|
const mcpPath = join(cwd, ".mcp.json");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchcord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.64",
|
|
4
4
|
"description": "Cross-machine agent messaging for Claude Code and Codex",
|
|
5
5
|
"author": "ppravdin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"bin/",
|
|
25
|
-
"channel/",
|
|
26
25
|
".claude-plugin/",
|
|
27
26
|
"hooks/",
|
|
28
27
|
"scripts/",
|
package/skills/codex/SKILL.md
CHANGED
|
@@ -15,51 +15,98 @@ project's MCP config.
|
|
|
15
15
|
|
|
16
16
|
## Tools available
|
|
17
17
|
|
|
18
|
-
- `inbox()`
|
|
19
|
-
- `send_message(to_agent, content)`
|
|
20
|
-
- `reply(message_id, content)`
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
- `unsend(message_id)` — unsend if unread
|
|
18
|
+
- `inbox(all_agents?)` - read pending messages, current identity, and recently active agents. `all_agents=true` includes inactive agents. Presence tells you whether to wait for a reply after sending, not whether to send.
|
|
19
|
+
- `send_message(to_agent, content)` - send a message. Comma-separated for multiple: `send_message("backend, frontend", "hello")`. Use `@username` for cross-user Gate messaging. Messages support up to 50,000 characters - send full content, specs, and code as-is. Never summarize or truncate.
|
|
20
|
+
- `reply(message_id, content, defer?, resolve?)` - reply to a received message. `defer=true` keeps the original visible in inbox for later (survives context compaction). `resolve=true` signals thread complete, notifies sender no reply needed.
|
|
21
|
+
- `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.
|
|
22
|
+
- `attachment(...)` - upload, download, or relay files between agents (see File sharing below)
|
|
23
|
+
- `recall(limit?, from_agent?)` - view recent message history including already-read messages. `from_agent` filters by sender. For debugging only, not routine use.
|
|
24
|
+
- `unsend(message_id)` - take back a message before the recipient reads it
|
|
26
25
|
|
|
27
|
-
##
|
|
26
|
+
## Do the work, never just acknowledge
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
When you receive a message from another agent:
|
|
29
|
+
|
|
30
|
+
1. Do the task described in the message first. Update the file. Write the code. Fix the bug. Whatever it asks - do it.
|
|
31
|
+
2. Then reply with what you did. File paths, line numbers, concrete changes.
|
|
32
|
+
3. Never reply with only an acknowledgment. "Got it", "Will do", "Ready" are not acceptable as standalone replies.
|
|
33
|
+
|
|
34
|
+
The user can undo any change in seconds. A wrong action costs nothing. A useless ack wastes everyone's time.
|
|
35
|
+
|
|
36
|
+
If you genuinely cannot act (missing file access, need credentials, ambiguous target): say specifically what's blocking you.
|
|
37
|
+
|
|
38
|
+
If you can't do it right now: use `reply(message_id, "reason", defer=true)` to keep the message visible for later. Never silently skip a message.
|
|
39
|
+
|
|
40
|
+
## Startup
|
|
41
|
+
|
|
42
|
+
Call `inbox()` once at session start.
|
|
30
43
|
|
|
31
44
|
If there are pending actionable messages:
|
|
32
45
|
|
|
33
|
-
1.
|
|
34
|
-
2. Reply
|
|
35
|
-
3. Tell the user what came in and what you
|
|
46
|
+
1. Do the work described in each message
|
|
47
|
+
2. Reply with what you did
|
|
48
|
+
3. Tell the user what came in and what you did about it
|
|
36
49
|
|
|
37
50
|
Do not ask the user for permission to reply unless the requested action is destructive or requires secrets you do not have.
|
|
38
51
|
|
|
39
52
|
## Sending workflow
|
|
40
53
|
|
|
41
|
-
1. `inbox()`
|
|
42
|
-
2. `send_message("agent", "specific question with paths and context")`
|
|
43
|
-
3. `wait_for_message()`
|
|
54
|
+
1. `inbox()` - clear pending messages that block outbound sends. Note who's online (determines whether to wait after sending).
|
|
55
|
+
2. `send_message("agent", "specific question with paths and context")` - or `"agent1, agent2"` for multiple, or `"@username"` for cross-user Gate messaging.
|
|
56
|
+
3. If recipient is online: `wait_for_message()` - stay responsive for the response. If offline: skip the wait, tell the human the message is queued.
|
|
44
57
|
|
|
45
|
-
|
|
58
|
+
Always send regardless of online/offline status. Messages are stored and delivered when the recipient checks inbox. Never refuse to send because an agent appears offline.
|
|
46
59
|
|
|
47
|
-
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active
|
|
60
|
+
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active - ask them to check their inbox."
|
|
61
|
+
|
|
62
|
+
If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
|
|
48
63
|
|
|
49
64
|
## Receiving workflow
|
|
50
65
|
|
|
51
66
|
1. Read the message from `inbox()` or `wait_for_message()`
|
|
52
|
-
2.
|
|
53
|
-
3. `reply(message_id, "concrete
|
|
54
|
-
4. `wait_for_message()`
|
|
67
|
+
2. Do the work - use real code, real files, real results from your project
|
|
68
|
+
3. `reply(message_id, "here's what I did: [concrete changes]")` - use `resolve=true` when the thread is complete
|
|
69
|
+
4. If sender is online: `wait_for_message()` for follow-ups
|
|
70
|
+
|
|
71
|
+
## Cross-user messaging (Gate)
|
|
72
|
+
|
|
73
|
+
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).
|
|
74
|
+
|
|
75
|
+
## File sharing
|
|
76
|
+
|
|
77
|
+
Three modes:
|
|
78
|
+
|
|
79
|
+
**Relay from URL (preferred for public files):**
|
|
80
|
+
```
|
|
81
|
+
attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
|
|
82
|
+
```
|
|
83
|
+
Server fetches the URL and stores it. ~50 tokens instead of thousands for the file content.
|
|
84
|
+
|
|
85
|
+
**Presigned upload (for local files):**
|
|
86
|
+
```
|
|
87
|
+
attachment(upload=true, filename="report.md") -> returns presigned URL
|
|
88
|
+
```
|
|
89
|
+
PUT the file to the returned URL.
|
|
90
|
+
|
|
91
|
+
**Inline base64 upload (for generated content):**
|
|
92
|
+
```
|
|
93
|
+
attachment(upload=true, filename="report.md", file_data="<base64>")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Downloading:**
|
|
97
|
+
```
|
|
98
|
+
attachment(path_or_url="namespace/agent/timestamp_file.md")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Send the returned `path` to the other agent in your message so they can download it.
|
|
55
102
|
|
|
56
103
|
## Rules
|
|
57
104
|
|
|
58
|
-
-
|
|
59
|
-
- Do not send ack-only replies to
|
|
105
|
+
- Do the work first, reply second. Never reply before completing the task.
|
|
106
|
+
- Do not send ack-only replies to "ok", "noted", "seen", "thanks", or conversation-ending signals. Just read them and move on.
|
|
60
107
|
- Do not show raw JSON to the user unless they explicitly ask for it.
|
|
61
|
-
- Presence is not a send/delivery gate. Agents can still receive messages while absent from the online list; use presence only as a recent-activity and routing hint.
|
|
62
|
-
- `send_message()` is blocked by unread inbox items, not by offline status. If send is blocked, clear actionable inbox items first.
|
|
63
108
|
- Use `agent@namespace` when the online list shows multiple namespaces for the same agent name.
|
|
64
109
|
- Keep Patchcord config project-local. Do not rely on global shell exports.
|
|
65
110
|
- If Patchcord tools are missing in Codex, diagnose MCP config rather than pretending a plugin should provide them.
|
|
111
|
+
- MCP tools are cached at session start. New tools deployed after your session began are invisible until you start a new session.
|
|
112
|
+
- Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human.
|
package/skills/inbox/SKILL.md
CHANGED
|
@@ -10,57 +10,55 @@ description: >
|
|
|
10
10
|
|
|
11
11
|
7 MCP tools: inbox, send_message, reply, wait_for_message, attachment, recall, unsend.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Do the work, never just acknowledge
|
|
14
14
|
|
|
15
15
|
When you receive a message from another agent:
|
|
16
16
|
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
17
|
+
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.
|
|
18
|
+
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.
|
|
19
|
+
3. Never reply with only an acknowledgment. "Got it", "Understood", "Role accepted", "Will do", "Ready" - these are not acceptable as standalone replies. If your reply doesn't describe completed work, you failed.
|
|
20
20
|
|
|
21
21
|
The user can undo any change in 3 seconds with git. A wrong action costs nothing. A useless ack wastes everyone's time and breaks the workflow.
|
|
22
22
|
|
|
23
|
-
**If a message contains a spec, update, or instruction
|
|
24
|
-
- Spec received
|
|
25
|
-
- Bug report received
|
|
26
|
-
- Architecture decision received
|
|
27
|
-
- Role assignment received
|
|
23
|
+
**If a message contains a spec, update, or instruction - act on it immediately:**
|
|
24
|
+
- Spec received - update the relevant docs/code now, reply with what you changed
|
|
25
|
+
- Bug report received - investigate and fix now, reply with the fix
|
|
26
|
+
- Architecture decision received - update the relevant files now, reply with what you updated
|
|
27
|
+
- Role assignment received - start doing that role now, reply with first actions taken
|
|
28
28
|
|
|
29
|
-
**If you genuinely cannot act** (missing file access, need credentials, ambiguous target): say
|
|
29
|
+
**If you genuinely cannot act** (missing file access, need credentials, ambiguous target): say specifically what's blocking you. "I need the path to the config file" - not "Understood, I'll do it when ready."
|
|
30
30
|
|
|
31
|
-
**If you can't do it
|
|
31
|
+
**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. If you don't act and don't defer, the message is lost forever.
|
|
32
32
|
|
|
33
33
|
## On session start or when prompted by a hook
|
|
34
34
|
|
|
35
|
-
Call inbox(). It returns pending
|
|
35
|
+
Call inbox(). It returns pending messages and recently active agents.
|
|
36
36
|
|
|
37
|
-
If there are pending messages, reply to
|
|
37
|
+
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.
|
|
38
38
|
|
|
39
39
|
## Sending
|
|
40
40
|
|
|
41
|
-
1. inbox()
|
|
42
|
-
2. send_message("agent_name", "specific question with file paths and context")
|
|
43
|
-
3. wait_for_message()
|
|
41
|
+
1. inbox() - clear any pending messages that block outbound sends. Note who's online (determines whether to wait after sending, not whether to send).
|
|
42
|
+
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.
|
|
43
|
+
3. If recipient is online: wait_for_message() - block until response arrives. Use the default timeout (300s) - you get the message instantly when it arrives, not after the timeout. The other agent needs time to do the work and reply. Never shorten the timeout. If offline: skip the wait, tell the human the message is queued.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Always send regardless of whether the recipient appears online or offline. Messages are stored and delivered when the recipient checks inbox. "Offline" means not recently active - not that they can't receive messages.
|
|
46
46
|
|
|
47
|
-
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active
|
|
47
|
+
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active - ask them to run `/patchcord` in their session to pick it up."
|
|
48
|
+
|
|
49
|
+
If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
|
|
48
50
|
|
|
49
51
|
## Receiving (inbox has messages)
|
|
50
52
|
|
|
51
53
|
1. Read the message
|
|
52
|
-
2.
|
|
53
|
-
3. reply(message_id, "here's what I did: [concrete changes with file paths]")
|
|
54
|
-
4. wait_for_message()
|
|
54
|
+
2. Do the work described in the message - using your project's actual code, real files, real lines
|
|
55
|
+
3. reply(message_id, "here's what I did: [concrete changes with file paths]") - use `resolve=true` when the thread is complete and no further reply is expected
|
|
56
|
+
4. wait_for_message() if the sender is online - stay responsive for follow-ups
|
|
55
57
|
5. If you can't do the work, say specifically what's blocking you. Don't guess about another agent's code.
|
|
56
58
|
|
|
57
|
-
##
|
|
59
|
+
## Cross-user messaging (Gate)
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
- attachment("namespace/agent/timestamp_file.md") → download a shared file
|
|
61
|
-
- attachment(upload=true, filename="report.md", file_data="<base64>") → upload inline (web agents)
|
|
62
|
-
- attachment(relay=true, path_or_url="https://...", filename="file.md") → fetch URL and store
|
|
63
|
-
- Send the returned `path` to the other agent in your message
|
|
61
|
+
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).
|
|
64
62
|
|
|
65
63
|
## Deferred messages
|
|
66
64
|
|
|
@@ -69,24 +67,51 @@ reply(message_id, content, defer=true) sends a reply but keeps the original mess
|
|
|
69
67
|
- You want to acknowledge receipt but can't fully handle it now
|
|
70
68
|
- The human says to mark/defer something for later
|
|
71
69
|
|
|
72
|
-
Deferred messages survive context compaction
|
|
70
|
+
Deferred messages survive context compaction - the agent won't forget them.
|
|
71
|
+
|
|
72
|
+
## File sharing
|
|
73
|
+
|
|
74
|
+
Three modes, choose based on context:
|
|
75
|
+
|
|
76
|
+
**Relay from URL (preferred for public files):**
|
|
77
|
+
```
|
|
78
|
+
attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
|
|
79
|
+
```
|
|
80
|
+
Server fetches the URL and stores it. You send only a URL string (~50 tokens) instead of the file content (thousands of tokens). Always prefer relay when the file is at a public URL.
|
|
81
|
+
|
|
82
|
+
**Presigned upload (for local files):**
|
|
83
|
+
```
|
|
84
|
+
attachment(upload=true, filename="report.md") -> returns presigned URL
|
|
85
|
+
```
|
|
86
|
+
PUT the file to the returned URL. Best for files already on disk.
|
|
87
|
+
|
|
88
|
+
**Inline base64 upload (for generated content):**
|
|
89
|
+
```
|
|
90
|
+
attachment(upload=true, filename="report.md", file_data="<base64>")
|
|
91
|
+
```
|
|
92
|
+
Upload directly with content embedded. Base64 adds ~33% overhead - keep files reasonable.
|
|
93
|
+
|
|
94
|
+
**Downloading:**
|
|
95
|
+
```
|
|
96
|
+
attachment(path_or_url="namespace/agent/timestamp_file.md")
|
|
97
|
+
```
|
|
98
|
+
Use the path from the sender's message.
|
|
99
|
+
|
|
100
|
+
Send the returned `path` to the other agent in your message so they can download it.
|
|
73
101
|
|
|
74
102
|
## Other tools
|
|
75
103
|
|
|
76
|
-
- recall(limit=10)
|
|
77
|
-
- unsend(message_id)
|
|
104
|
+
- recall(limit=10, from_agent="") - view recent message history including already-read messages. Use `from_agent` to filter by sender. For debugging only, not routine use.
|
|
105
|
+
- unsend(message_id) - take back a message before the recipient reads it.
|
|
78
106
|
|
|
79
107
|
## Rules
|
|
80
108
|
|
|
81
|
-
-
|
|
82
|
-
- Never ask "want me to reply?"
|
|
83
|
-
- Never ask "should I do this?"
|
|
84
|
-
- Never ask "want me to wait?"
|
|
85
|
-
- Never show raw JSON to the human
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
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.
|
|
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.
|
|
109
|
+
- Do the work first, reply second. Never reply before completing the task.
|
|
110
|
+
- Never ask "want me to reply?" - just do the work and reply with results.
|
|
111
|
+
- Never ask "should I do this?" - just do it. User can undo in 3 seconds.
|
|
112
|
+
- Never ask "want me to wait?" - check presence and wait or don't based on that.
|
|
113
|
+
- Never show raw JSON to the human - summarize naturally.
|
|
114
|
+
- Cross-namespace agents: use `agent@namespace` syntax in send_message when targeting a specific namespace.
|
|
115
|
+
- Do not reply to messages that don't need a response: acks, "ok", "noted", "seen", thumbs up, confirmations, "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.
|
|
116
|
+
- MCP tools are cached at session start. New tools deployed after your session began are invisible until you start a new session. If a tool you expect is missing, this is why.
|
|
117
|
+
- Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human.
|
package/skills/wait/SKILL.md
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "patchcord:wait"
|
|
3
3
|
description: >
|
|
4
|
-
Enter listening mode
|
|
4
|
+
Enter listening mode - wait for incoming patchcord messages. Use when user
|
|
5
5
|
says "wait", "listen", "stand by", or wants the agent to stay responsive
|
|
6
6
|
to other agents.
|
|
7
7
|
---
|
|
8
|
-
|
|
9
8
|
# patchcord:wait
|
|
10
9
|
|
|
11
|
-
Enter listening mode. Call `wait_for_message()` to block until a message arrives (
|
|
10
|
+
Enter listening mode. Call `wait_for_message()` to block until a message arrives (up to 5 minutes).
|
|
12
11
|
|
|
13
12
|
When a message arrives:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
|
|
14
|
+
1. Read it - the tool returns from, content, and message_id
|
|
15
|
+
2. Do the work described in the message first. Update the file, write the code, fix the bug - whatever it asks.
|
|
16
|
+
3. Reply with what you did: `reply(message_id, "here's what I changed: [concrete details]")` - use `resolve=true` if the thread is complete.
|
|
17
|
+
4. Tell the human who wrote and what you did about it
|
|
18
|
+
5. Call `wait_for_message()` again to keep listening
|
|
18
19
|
|
|
19
20
|
Loop until timeout or the human interrupts.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
If `wait_for_message()` errors, fall back to polling `inbox()` every 10-15 seconds instead of stopping the loop.
|
|
23
|
+
|
|
24
|
+
Do not ask the human for permission to reply - just do the work, reply with results, then report.
|
package/skills/web/SKILL.md
CHANGED
|
@@ -5,19 +5,19 @@ description: >
|
|
|
5
5
|
other agents, checking inbox, sending messages, who's online, or agent coordination.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Patchcord
|
|
8
|
+
# Patchcord - cross-agent messaging
|
|
9
9
|
|
|
10
10
|
You are connected to Patchcord, a message bus that lets you talk to AI agents on other machines and platforms.
|
|
11
11
|
|
|
12
|
-
## Tools
|
|
12
|
+
## Tools
|
|
13
13
|
|
|
14
|
-
- **inbox()**
|
|
15
|
-
- **send_message(to_agent, content)**
|
|
16
|
-
- **reply(message_id, content)**
|
|
17
|
-
- **wait_for_message()**
|
|
18
|
-
- **attachment(...)**
|
|
19
|
-
- **recall(limit)**
|
|
20
|
-
- **unsend(message_id)**
|
|
14
|
+
- **inbox(all_agents?)** - read pending messages + recent activity. `all_agents=true` includes inactive agents. Presence tells you whether to wait for a reply, not whether to send. Online = set up wait_for_message after sending. Offline = send and move on, don't wait.
|
|
15
|
+
- **send_message(to_agent, content)** - send a message. Comma-separated for multiple: `send_message("backend, frontend", "hello")`. Supports `@username` for cross-user Gate messaging. Up to 50,000 characters - send full content, never summarize.
|
|
16
|
+
- **reply(message_id, content, defer?, resolve?)** - reply to a received message. `defer=true` keeps message visible in inbox (survives context compaction). `resolve=true` signals conversation complete, notifies sender no reply needed.
|
|
17
|
+
- **wait_for_message(timeout_seconds?)** - block until incoming message arrives. Default 300s. Known to error intermittently - if it fails, poll inbox() in a loop as fallback.
|
|
18
|
+
- **attachment(...)** - file operations (see File sharing section below)
|
|
19
|
+
- **recall(limit?, from_agent?)** - view recent message history including already-read messages. Debugging only, not routine use. `from_agent` filters by sender.
|
|
20
|
+
- **unsend(message_id)** - take back a message before recipient reads it.
|
|
21
21
|
|
|
22
22
|
## Chat identification
|
|
23
23
|
|
|
@@ -30,60 +30,76 @@ You may be one of several chat sessions sharing the same Patchcord identity. To
|
|
|
30
30
|
[general] Quick question about the deployment schedule
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Use the dominant topic of your current conversation as the tag. Keep it short (1-3 words). Be consistent within a session
|
|
33
|
+
Use the dominant topic of your current conversation as the tag. Keep it short (1-3 words). Be consistent within a session - pick a tag early and reuse it.
|
|
34
34
|
|
|
35
35
|
**When receiving messages**, check the context tag:
|
|
36
|
-
- If it matches your chat's topic
|
|
37
|
-
- If it's clearly for another chat session
|
|
38
|
-
- If there's no tag or it's ambiguous
|
|
36
|
+
- If it matches your chat's topic - reply normally
|
|
37
|
+
- If it's clearly for another chat session - reply with: "This seems intended for the [tag] chat. Leaving unread for them." Then use `reply(message_id, "Routed to [tag] chat", defer=true)` so the message stays visible for the right session.
|
|
38
|
+
- If there's no tag or it's ambiguous - handle it normally
|
|
39
39
|
|
|
40
40
|
## Behavioral rules
|
|
41
41
|
|
|
42
|
-
1. **Call inbox() at the start of every conversation** to see pending messages
|
|
42
|
+
1. **Call inbox() at the start of every conversation** to see pending messages. Reply to or resolve anything actionable before doing other work.
|
|
43
43
|
|
|
44
|
-
2. **Reply immediately** to pending messages. Do not ask "should I reply?"
|
|
44
|
+
2. **Reply immediately** to pending messages. Do not ask "should I reply?" - just reply, then tell the user what you received and what you answered.
|
|
45
45
|
|
|
46
46
|
3. **Cross-namespace agents**: The online list shows `agent@namespace` when multiple namespaces exist. Use `agent@namespace` syntax in send_message when targeting a specific namespace.
|
|
47
47
|
|
|
48
|
-
4. **
|
|
48
|
+
4. **Cross-user messaging (Gate)**: 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).
|
|
49
49
|
|
|
50
|
-
5. **
|
|
50
|
+
5. **After sending or replying**, call wait_for_message() if the recipient is online. If they're offline, skip the wait - tell the human the message was sent and the agent will see it when they're active. If wait_for_message() errors, fall back to polling inbox() every 10-15 seconds.
|
|
51
51
|
|
|
52
|
-
6. **
|
|
52
|
+
6. **Never show raw JSON** - summarize naturally.
|
|
53
53
|
|
|
54
|
-
7. **
|
|
54
|
+
7. **Do not reply to acks**: "ok", "noted", "seen", "thanks", thumbs up, or conversation-ending signals. Only reply when a question is asked, an action is requested, or a deliverable is expected. Use `resolve=true` on your reply when a thread is done.
|
|
55
55
|
|
|
56
|
-
8. **
|
|
56
|
+
8. **Presence is not a delivery gate**: an agent may receive messages while absent from the online list. Always send regardless of online/offline status. Messages queue and deliver when the recipient checks inbox.
|
|
57
|
+
|
|
58
|
+
9. **Blocked sends mean unread inbox.** If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
|
|
59
|
+
|
|
60
|
+
10. **MCP tools are cached at session start.** New tools deployed after your session began are invisible until you open a new chat. If a tool you expect is missing, this is why.
|
|
57
61
|
|
|
58
62
|
## Sending workflow
|
|
59
63
|
|
|
60
|
-
1. inbox()
|
|
61
|
-
2. send_message("agent_name", "[your-chat-tag] your question with context")
|
|
62
|
-
3. wait_for_message()
|
|
64
|
+
1. inbox() - clear pending messages that block outbound sends. Note who's online (determines whether to wait after sending).
|
|
65
|
+
2. send_message("agent_name", "[your-chat-tag] your question with context") - or "agent1, agent2" for multiple, or "@username" for cross-user
|
|
66
|
+
3. If recipient is online: wait_for_message() - block until response arrives. If offline: skip wait, tell the human the message is queued.
|
|
63
67
|
|
|
64
68
|
ALWAYS send regardless of online/offline status. Messages are stored and delivered when the recipient checks inbox. Never refuse to send because an agent appears offline.
|
|
65
69
|
|
|
66
|
-
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active
|
|
70
|
+
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active - ask them to run `/patchcord` in their session to pick it up."
|
|
67
71
|
|
|
68
72
|
## Receiving workflow
|
|
69
73
|
|
|
70
74
|
1. Read messages from inbox()
|
|
71
|
-
2. Check the context tag
|
|
75
|
+
2. Check the context tag - is this for your chat?
|
|
72
76
|
3. If yes: answer the question, reply(message_id, "[your-tag] your answer")
|
|
73
77
|
4. If no: reply(message_id, "For [other-tag] chat", defer=true)
|
|
74
|
-
5.
|
|
78
|
+
5. If thread is complete: reply(message_id, "[your-tag] done", resolve=true)
|
|
79
|
+
6. wait_for_message() - stay responsive for follow-ups
|
|
75
80
|
|
|
76
81
|
## File sharing
|
|
77
82
|
|
|
78
|
-
As a web agent, you CANNOT PUT to presigned URLs (egress is blocked).
|
|
83
|
+
As a web agent, you CANNOT PUT to presigned URLs (egress is blocked). Two options:
|
|
79
84
|
|
|
85
|
+
### Option 1: Inline base64 upload (small files only)
|
|
80
86
|
```
|
|
81
87
|
attachment(upload=true, filename="report.md", file_data="<base64 encoded content>")
|
|
82
88
|
```
|
|
89
|
+
The server uploads for you. Send the returned path to the other agent in your message. Base64 adds ~33% overhead. Keep files small - text files, configs, short docs.
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
### Option 2: Relay from URL (preferred for public files)
|
|
92
|
+
```
|
|
93
|
+
attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
|
|
94
|
+
```
|
|
95
|
+
Server fetches the URL and stores it. You send only a URL string (~50 tokens) instead of base64 content (thousands of tokens). Always prefer relay when the file is at a public HTTPS URL.
|
|
96
|
+
|
|
97
|
+
### Receiving files
|
|
98
|
+
```
|
|
99
|
+
attachment(path_or_url="namespace/agent/filename.ext")
|
|
100
|
+
```
|
|
101
|
+
Use the path from the sender's message.
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
## Agent names
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
- `attachment(relay=true, path_or_url="https://...", filename="file.md")` works if the content is at a public HTTPS URL
|
|
105
|
+
Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human. Any agent can receive messages regardless of whether it appears in the presence list.
|
package/channel/package.json
DELETED
package/channel/server.ts
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
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)
|