patchcord 0.3.62 → 0.3.63

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
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.62",
3
+ "version": "0.3.63",
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/",
@@ -15,51 +15,98 @@ project's MCP config.
15
15
 
16
16
  ## Tools available
17
17
 
18
- - `inbox()` read pending messages, current identity, and recent presence
19
- - `send_message(to_agent, content)` send a message. Comma-separated for multiple: `send_message("backend, frontend", "hello")`
20
- - `reply(message_id, content)` reply to a received message
21
- - `reply(message_id, content, defer=true)` reply but keep the original message visible as "deferred" in the inbox (use when the message needs later attention or another agent should handle it)
22
- - `wait_for_message()` block until any incoming message arrives
23
- - `attachment(...)` upload, download, or relay files between agents
24
- - `recall(limit)` view recent message history including already-read messages
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
- ## Startup rule
26
+ ## Do the work, never just acknowledge
28
27
 
29
- Call `inbox()` once at session start to orient.
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. Read them
34
- 2. Reply immediately
35
- 3. Tell the user what came in and what you answered
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()` orient on pending messages, identity, and recent presence
42
- 2. `send_message("agent", "specific question with paths and context")` or `"agent1, agent2"` for multiple recipients
43
- 3. `wait_for_message()` stay responsive for the response
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
- 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.
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 ask them to run `/patchcord` in their Claude Code session to pick it up."
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. Use the real code / files / results from your project
53
- 3. `reply(message_id, "concrete answer")`
54
- 4. `wait_for_message()` again when follow-up is expected
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
- - Reply immediately to actionable incoming messages.
59
- - Do not send ack-only replies to `ok`, `noted`, `seen`, `thanks`, or other conversation-ending signals.
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.
@@ -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
- ## CRITICAL: DO THE WORK, NEVER JUST ACKNOWLEDGE
13
+ ## Do the work, never just acknowledge
14
14
 
15
15
  When you receive a message from another agent:
16
16
 
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 FORBIDDEN as standalone replies. If your reply doesn't describe completed work, you failed.
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 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
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 SPECIFICALLY what's blocking you. "I need the path to the docs folder" not "Understood, I'll do it when ready."
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 RIGHT NOW** (busy with something else, need to finish current task first): 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.
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 inbox (full text of ALL unread messages) and online agents in one call.
35
+ Call inbox(). It returns pending messages and recently active agents.
36
36
 
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.
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() read pending mail and recent presence for routing
42
- 2. send_message("agent_name", "specific question with file paths and context") or "agent1, agent2" for multiple recipients
43
- 3. wait_for_message() — auto-wait for any response, don't ask human whether to wait. 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.
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
- ALWAYS send the message regardless of whether the recipient appears online or offline. Messages are stored and delivered when the recipient checks inbox. "Offline" just means not recently active NOT that they can't receive messages. Never refuse to send.
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 ask them to run `/patchcord` in their session to pick it up."
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. DO THE WORK described in the message using YOUR project's actual code, real files, real lines
53
- 3. reply(message_id, "here's what I did: [concrete changes with file paths]")
54
- 4. wait_for_message() stay responsive for follow-ups
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
- ## File sharing
59
+ ## Cross-user messaging (Gate)
58
60
 
59
- - attachment(upload=true, filename="report.md") returns presigned upload URL. PUT the file there.
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 the agent won't forget them.
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) view recent message history including already-read messages
77
- - unsend(message_id) take back a message before the recipient reads it
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
- - DO THE WORK FIRST, REPLY SECOND. Never reply before completing the task.
82
- - Never ask "want me to reply?" just do the work and reply with results.
83
- - Never ask "should I do this?" just do it. User can undo in 3 seconds.
84
- - Never ask "want me to wait?" just wait.
85
- - Never show raw JSON to the human summarize naturally.
86
- - One inbox() to orient. Don't call it repeatedly.
87
- - If user says "check" or "check patchcord" call inbox().
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
- - 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. 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.
@@ -1,21 +1,24 @@
1
1
  ---
2
2
  name: "patchcord:wait"
3
3
  description: >
4
- Enter listening mode wait for incoming patchcord messages. Use when user
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 (polls every 5s, up to 5 minutes).
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
- 1. Read it — the tool returns from, content, and message_id
15
- 2. Reply immediately: `reply(message_id, "your answer")`
16
- 3. Tell the human who wrote and what you answered
17
- 4. Call `wait_for_message()` again to keep listening
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
- Do NOT ask the human for permission to reply just reply, then report.
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.
@@ -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 cross-agent messaging
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 available via Patchcord connector
12
+ ## Tools
13
13
 
14
- - **inbox()** read pending messages + recent presence
15
- - **send_message(to_agent, content)** send a message. Comma-separated for multiple: `send_message("backend, frontend", "hello")`
16
- - **reply(message_id, content)** reply to a received message. Use `defer=true` to keep it visible in inbox for later.
17
- - **wait_for_message()** block until any incoming message arrives. Use the default timeout (300s) you get the message instantly when it arrives, not after the timeout.
18
- - **attachment(...)** upload, download, or relay files between agents
19
- - **recall(limit)** view recent message history including already-read messages
20
- - **unsend(message_id)** unsend if unread
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 pick a tag early and reuse it.
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 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
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 and recent presence.
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?" just reply, then tell the user what you received and what you answered.
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. **After sending or replying**, call wait_for_message() to stay responsive. Do not ask the user whether to wait.
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. **Never show raw JSON** summarize naturally.
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. **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.
52
+ 6. **Never show raw JSON** - summarize naturally.
53
53
 
54
- 7. **Presence is not a send/delivery gate**: an agent may still receive messages while absent from the online list. Use presence only as a recent-activity and routing hint.
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. **Blocked sends mean unread inbox**, not offline status. If send_message is blocked, clear actionable inbox items first.
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() review pending messages and recent presence
61
- 2. send_message("agent_name", "[your-chat-tag] your question with context") or "agent1, agent2" for multiple recipients
62
- 3. wait_for_message() block until response arrives
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 ask them to run `/patchcord` in their session to pick it up."
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 is this for your chat?
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. wait_for_message() stay responsive for follow-ups
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). Use the inline base64 mode instead:
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
- The server uploads for you. Send the returned `path` to the other agent in your message.
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
- **Limits**: your context window is the bottleneck. Base64 adds ~33% overhead. Keep files small — text files, configs, short docs. Don't try to send large binaries.
103
+ ## Agent names
87
104
 
88
- - Receiver uses `attachment(path)` to download
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.
@@ -1,11 +0,0 @@
1
- {
2
- "name": "patchcord-channel",
3
- "version": "0.1.0",
4
- "type": "module",
5
- "scripts": {
6
- "start": "bun install --no-summary && bun server.ts"
7
- },
8
- "dependencies": {
9
- "@modelcontextprotocol/sdk": "^1.0.0"
10
- }
11
- }
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)