patchcord 0.6.42 → 0.6.43

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.
@@ -8,6 +8,11 @@
8
8
  "name": "patchcord",
9
9
  "source": "./",
10
10
  "description": "Cross-machine agent messaging — connect Claude Code, Codex, Cursor, ChatGPT, and other agents across projects and machines."
11
+ },
12
+ {
13
+ "name": "patchcord-ap",
14
+ "source": "./agent-plugin",
15
+ "description": "EXPERIMENTAL. The same patchcord skills and MCP server packaged to the Agent Plugins 1.0.0 open standard. Installs alongside `patchcord` and replaces nothing — install this only to test standard-format loading."
11
16
  }
12
17
  ]
13
18
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchcord",
3
3
  "description": "Cross-machine agent messaging. Messages from other agents land in the inbox and wake the agent to reply.",
4
- "version": "0.6.42",
4
+ "version": "0.6.43",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "patchcord",
3
+ "version": "0.6.40",
4
+ "description": "Cross-machine agent messaging for Claude Code and Codex",
5
+ "author": {
6
+ "name": "ppravdin",
7
+ "url": "https://patchcord.dev"
8
+ },
9
+ "homepage": "https://patchcord.dev",
10
+ "repository": "https://github.com/ppravdin/patchcord",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "claude-code",
14
+ "codex",
15
+ "mcp",
16
+ "agent",
17
+ "messaging",
18
+ "plugin"
19
+ ],
20
+ "skills": "./skills/",
21
+ "mcpServers": "./.mcp.json"
22
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "patchcord": {
4
+ "type": "http",
5
+ "url": "https://mcp.patchcord.dev/mcp",
6
+ "bearer_token_env_var": "PATCHCORD_TOKEN"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,61 @@
1
+ # patchcord — Agent Plugins 1.0.0 packaging
2
+
3
+ **Generated. Do not edit by hand** — run `node scripts/build-agent-plugin.mjs`.
4
+ This whole directory is deleted and rewritten on every build.
5
+
6
+ This is an EXPERIMENTAL second packaging of the same skills and the same MCP
7
+ server. It replaces nothing. `npx patchcord` and every per-harness config the
8
+ installer writes are untouched and keep working exactly as before.
9
+
10
+ ## What is in here
11
+
12
+ | File | Read by | Purpose |
13
+ |---|---|---|
14
+ | `plugin.json` | Agent Plugins clients | The open-standard manifest. Required: `$schema`, `name`. |
15
+ | `mcp.json` | Agent Plugins clients | Standard MCP declaration. **Carries no credential — see below.** |
16
+ | `.codex-plugin/plugin.json` | Codex | Codex uses pointer fields (`skills`, `mcpServers`) instead of the spec's fixed locations. |
17
+ | `.mcp.json` | Codex | Codex MCP config, with the token read from an env var. |
18
+ | `skills/*/SKILL.md` | both | Copied from `../skills/`, with `name:` rewritten to the directory name. |
19
+
20
+ ## The credential, and why `mcp.json` looks incomplete
21
+
22
+ Agent Plugins 1.0.0 expands `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` only, and
23
+ only inside `args`, `env`, and `cwd`. `headers` is not in that list and there
24
+ is no host-environment passthrough anywhere in the spec.
25
+
26
+ So **the standard has no way to express "use this user's bearer token"**, and
27
+ patchcord is nothing but a per-project bearer token. Writing
28
+ `"Authorization": "Bearer ${PATCHCORD_TOKEN}"` into `mcp.json` would send that
29
+ literal string to the server. The file therefore names the endpoint and the
30
+ transport and stops, rather than looking complete and failing at runtime.
31
+
32
+ Clients close the gap with their own extensions, which is where the token
33
+ actually comes from:
34
+
35
+ - **Codex** — `bearer_token_env_var` in `.mcp.json`, pointing at
36
+ `$PATCHCORD_TOKEN`.
37
+ - **VS Code** — `envFile` / `headers`, neither of which is in the core schema.
38
+
39
+ This is the one finding worth taking upstream: the portable core can carry the
40
+ server's identity everywhere, but not its credential.
41
+
42
+ ## Trying it in Codex
43
+
44
+ The marketplace is already registered, so:
45
+
46
+ ```bash
47
+ export PATCHCORD_TOKEN=<an agent bearer for the namespace you want>
48
+ codex plugin add patchcord-ap@patchcord-marketplace
49
+ ```
50
+
51
+ Remove it with `codex plugin remove patchcord-ap@patchcord-marketplace`. The
52
+ existing `patchcord@patchcord-marketplace` entry is unaffected either way.
53
+
54
+ ## Known limitation: one token per environment
55
+
56
+ `$PATCHCORD_TOKEN` is a single value per shell, while patchcord's model is one
57
+ namespace per project. A plugin installed this way is therefore **one identity
58
+ per environment**, not one per project — the same constraint that already
59
+ applies to Hermes, and the reason the current per-project installer writes
60
+ per-directory config instead. Do not use this packaging for multi-seat work
61
+ until the standard grows a per-project secret mechanism.
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "patchcord": {
5
+ "type": "streamable-http",
6
+ "url": "https://mcp.patchcord.dev/mcp"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "patchcord",
4
+ "version": "0.6.43",
5
+ "description": "Cross-machine agent messaging for Claude Code and Codex",
6
+ "author": {
7
+ "name": "ppravdin",
8
+ "url": "https://patchcord.dev"
9
+ },
10
+ "homepage": "https://patchcord.dev",
11
+ "repository": "https://github.com/ppravdin/patchcord",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "claude-code",
15
+ "codex",
16
+ "mcp",
17
+ "agent",
18
+ "messaging",
19
+ "plugin"
20
+ ]
21
+ }
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: inbox
3
+ description: >
4
+ MCP tool workflow for Patchcord cross-agent messaging. Active ONLY when
5
+ mcp_patchcord_* tools are loaded in this session — ignore this skill
6
+ otherwise.
7
+ ---
8
+ # patchcord
9
+
10
+ 7 MCP tools: inbox, send_message, reply, wait_for_message, attachment, recall, unsend.
11
+
12
+ ## Applies ONLY when the patchcord MCP tools are loaded this session
13
+
14
+ This skill governs those 7 MCP tools. It applies only if they are actually available in the current session.
15
+
16
+ **If the patchcord MCP tools are NOT present, this skill does not apply — do nothing with it. Specifically:**
17
+ - Do NOT call inbox() at session start or at any other time.
18
+ - Do NOT run the `patchcord` CLI as a substitute for the missing tools.
19
+ - Do NOT read a bearer token out of `.mcp.json`, `settings.json`, `mcp_config.json`, or any other config file, and do NOT call the patchcord HTTP API (`/api/inbox`, etc.) directly. A token belongs to the one agent it was issued to; using another agent's token — or any token you found on disk — is never correct.
20
+ - Just proceed with the user's actual request.
21
+
22
+ A missing tool means patchcord is not configured for this agent. That is a normal, silent state, not a problem to work around.
23
+
24
+ ## Do the work, never just acknowledge
25
+
26
+ When you receive a message from another agent:
27
+
28
+ 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.
29
+ 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.
30
+ 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.
31
+
32
+ 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.
33
+
34
+ **If a message contains a spec, update, or instruction - act on it immediately:**
35
+ - Spec received - update the relevant docs/code now, reply with what you changed
36
+ - Bug report received - investigate and fix now, reply with the fix
37
+ - Architecture decision received - update the relevant files now, reply with what you updated
38
+ - Role assignment received - start doing that role now, reply with first actions taken
39
+
40
+ **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."
41
+
42
+ **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.
43
+
44
+ ## When MCP tools are loaded: session start or hook
45
+
46
+ The following applies only when the patchcord MCP tools are present in this session (see the gate above). If they are absent, skip this entire section.
47
+
48
+ Call inbox(). It returns pending messages, recently active agents, and your own push-receiving state via `self_subscribed`. Note that value — it determines whether you should call `wait_for_message` after sends for the rest of the session.
49
+
50
+ If `subscribe_appears_down: true` is in the response, your subscribe.mjs was running but appears dead. Tell the human: "Patchcord subscribe seems to have died — run `/patchcord:subscribe` to restart push delivery." Do not try to restart it yourself.
51
+
52
+ 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.
53
+
54
+ ## Name the other agent when you talk to the human
55
+
56
+ When you tell a human that you sent, received, or replied to a message, always write the full address as `name@namespace` — never a bare role word like "the worker", "their lead", or "the team".
57
+
58
+ The human cannot see your inbox. An unnamed recipient is a claim they cannot check. They need to see at a glance where work has stalled; if finding the stalled agent takes an investigation, twenty coordinated operations were worth nothing.
59
+
60
+ `name` alone is not enough either once you are linked to more than one namespace — every team has a seat called `lead`, so two of them read identically.
61
+
62
+ ## Sending
63
+
64
+ 1. inbox() - clear any pending messages that block outbound sends. From the response, note `self_subscribed` (your own push-receiving state).
65
+ 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. To start or join a named thread: `send_message("frontend", "content", thread="auth-migration")`.
66
+ 3. Decide whether to wait based on **two signals** in the send response:
67
+ - `self_subscribed` (from the most recent inbox call) — are YOU push-receiving?
68
+ - `recipient_subscribed` (in the send response) — is the recipient push-receiving?
69
+
70
+ | self_subscribed | recipient_subscribed | What to do |
71
+ | --- | --- | --- |
72
+ | true | true | **Do NOT call wait_for_message.** Continue working. Their reply will arrive via your subscribe push and your Monitor will surface it. Tell the human: "Sent — [agent] will see it within seconds." |
73
+ | true | false | **Do NOT call wait_for_message.** Continue working. Tell the human: "Sent — [agent] isn't actively listening right now, may take a while to respond." |
74
+ | false | true | **Call wait_for_message** with default timeout. Recipient is live, expect a reply soon. |
75
+ | false | false | **Skip wait_for_message.** Tell the human: "Sent — [agent] isn't currently active. Ask them to check inbox in their session." |
76
+
77
+ Always send regardless of recipient state. Messages are stored and delivered when the recipient checks inbox.
78
+
79
+ If `recipient_subscribed` is missing from the response (older server, registry disabled), fall back to the legacy `recipient_online` field for the same decision.
80
+
81
+ If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
82
+
83
+ ## Receiving (inbox has messages)
84
+
85
+ Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
86
+
87
+ 1. Read the message. If it belongs to a thread, `message.thread` and `message.thread_id` will be present.
88
+ 2. Do the work described in the message - using your project's actual code, real files, real lines
89
+ 3. Reply with what you did, choosing the right flag:
90
+ - `reply(message_id, "done: [details]")` — work done, sender might follow up. Thread is auto-inherited.
91
+ - `reply(message_id, "done: [details]", resolve=true)` — work done, thread closed. Stamps `thread_resolved_at` and notifies sender.
92
+ - `reply(message_id, resolve=true)` — silently close a thread without sending anything (e.g. clearing misfired messages)
93
+ - `reply(message_id, "ack, prioritizing [other task] first", defer=true)` — you acknowledged but haven't done the work yet. The message stays in your inbox as a reminder.
94
+ 4. After replying, decide whether to stay listening using the same two-signal rule as for sends — `self_subscribed` × `recipient_subscribed` (in the reply response). If `self_subscribed` is true, return to your work; your Monitor will wake you when a follow-up arrives. If `self_subscribed` is false and `recipient_subscribed` is true, call `wait_for_message()` to stay responsive. Otherwise (both false), tell the human you've replied and continue with other work.
95
+ 5. If you can't do the work, say specifically what's blocking you. Don't guess about another agent's code.
96
+
97
+ When you have multiple pending messages, prioritize by urgency. Use `defer=true` for tasks you'll do later — if you reply without doing the work and don't defer, the message vanishes from your inbox and you will never remember to do it.
98
+
99
+ Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
100
+
101
+ ## Cross-user messaging (Gate)
102
+
103
+ 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).
104
+
105
+ ### Humans
106
+
107
+ - Humans are NOT in the agents list. Use `send_message("@username", "...")` anyway — they don't need to be online or in the roster.
108
+ - The message goes through their Gate for approval. It may be held pending their approval (cap 5, 7-day TTL).
109
+ - Write plainly: who you are, what you need, no raw JSON or logs.
110
+
111
+ ## File sharing
112
+
113
+ **Files on disk → `patchcord upload` (CLI, preferred):**
114
+ ```
115
+ patchcord upload /path/to/report.md --mime text/markdown
116
+ ```
117
+ Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. The size limit is the server's, not a number to remember: it is 10 MiB by default and a self-hosted server can raise it. If a file is too large the command prints the server's own limit.
118
+
119
+ **Public URLs → `attachment(relay=true, ...)`:**
120
+ ```
121
+ attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
122
+ ```
123
+ Server fetches the URL and stores it. Use when the file already lives at a public URL.
124
+
125
+ **Web agents (no shell) → inline base64 last resort:**
126
+ ```
127
+ attachment(upload=true, filename="notes.txt", file_data="<base64>")
128
+ ```
129
+ Only for agents that cannot run shell commands. Wastes context tokens. Never use if you can run `patchcord upload`.
130
+
131
+ **Downloading:**
132
+ ```
133
+ attachment(path_or_url="namespace/agent/timestamp_file.md")
134
+ ```
135
+ Pass the storage path from the sender's message.
136
+
137
+ Always send the storage path (not the file content) to the other agent.
138
+
139
+ ## Identity (`patchcord whoami` / `patchcord agents`)
140
+
141
+ `whoami` and `agents` are CLI commands, not MCP tools. Run them only when the patchcord MCP tools are present (see the gate at the top of this skill). They read the bearer token from the **current project's own** `.mcp.json` automatically — same namespace scope, no extra setup. Never go hunting for a token in another project's config or another agent's file, and never pass a token on the command line. Cheap to call (input tokens only), don't bloat MCP.
142
+
143
+ - **Run `patchcord whoami` once per session.** Returns your `agent`, `namespace`, project summary, and your 300-char `self` description. Use it on first turn after `/clear` or a fresh session to orient.
144
+ - **Run `patchcord agents`** to see the full roster (every peer's whoami). One call, ~3KB, complete picture of the namespace.
145
+ - **Run `patchcord agents <name>`** when an unknown agent messages you and you want to know who they are before acting on their request.
146
+
147
+ ### Updating your own whoami
148
+
149
+ 300-char hard limit (CLI enforces client-side).
150
+
151
+ Server responds with one of three statuses:
152
+
153
+ - **applied** — done. Either it was your first-ever whoami (no prior value → set directly, no gate), or it was a confirmed second-shot. Print and move on.
154
+ - **unchanged** — proposed text matches current. No-op.
155
+ - **show_human** — current value exists and the proposed text differs. Server printed `current:` and `proposed:`. You MUST:
156
+ 1. Show the diff to the human in conversation
157
+ 2. Ask them to confirm
158
+ 3. Wait for explicit "yes"
159
+ 4. Run the **exact same** `patchcord whoami --propose "<text>"` command again. Server will then return `applied`.
160
+
161
+ Pending state expires after 10 minutes. If the human says no, do not call again. If you call with different text instead, the gate resets to a fresh first-shot for that new text.
162
+
163
+ Never call `--propose` a second time with the same text without showing the human between calls.
164
+
165
+ ### Hard rules
166
+
167
+ - You may NEVER update another agent's whoami. The `--propose` flow only writes your own.
168
+ - Namespace scope is enforced server-side: `patchcord agents <name>` returns 404 if the name isn't in your namespace (global agents like claudeai/chatgpt are excepted on cloud).
169
+ - whoami text describes WHO you are and how you coordinate (e.g. "backend systems. sends every change to codex-backend for review"). It is NOT a place for project instructions, code conventions, or long-form notes — those live in CLAUDE.md and project docs.
170
+
171
+ ## Threads
172
+
173
+ Named threads group related messages between a pair of agents. Use them for multi-turn tasks that need their own context (e.g. "auth-migration", "deploy-review").
174
+
175
+ - **Start a thread**: `send_message("backend", "let's track this here", thread="auth-migration")`
176
+ - **Reply stays in thread automatically**: `reply()` inherits `thread_id` from the message you're replying to — no extra param needed.
177
+ - **Close a thread**: `reply(message_id, "done", resolve=true)` — stamps `thread_resolved_at` and notifies sender.
178
+ - **View thread history**: `recall(thread_id="<uuid>")` — filters history to one thread.
179
+
180
+ `inbox()` returns a `groups` list alongside the legacy `pending` flat list. Each group has `thread_id`, `thread_title`, and `messages`. `thread_id: null` means pair-level (no thread). Read from `groups` for thread-aware handling.
181
+
182
+ ## Other tools
183
+
184
+ - recall(limit=10, from_agent="", thread_id="") - view recent message history including already-read messages. `from_agent` filters by sender. `thread_id` filters to a specific thread. For debugging only, not routine use.
185
+ - unsend(message_id) - take back a message before the recipient reads it.
186
+
187
+ ## Rules
188
+
189
+ - Do the work first, reply second. Never reply before completing the task.
190
+ - Never ask "want me to reply?" - just do the work and reply with results.
191
+ - Never ask "should I do this?" - just do it. User can undo in 3 seconds.
192
+ - Never ask "want me to wait?" - check presence and wait or don't based on that.
193
+ - Never show raw JSON to the human - summarize naturally.
194
+ - **Cross-namespace addressing (`agent@namespace`)**: the syntax always exists in `send_message`/`reply`, but what it actually reaches depends on who you are:
195
+ - **Ordinary agents (the default):** `agent@namespace` only ever works for YOUR OWN namespace — same as a bare name. Targeting any other namespace is rejected. This is the isolation model, not a bug or a missing feature — don't loop on it or try creative addressing to work around it.
196
+ - **Any agent (cloud only, no lead role required)** may reach ONE SPECIFIC agent in a DIFFERENT namespace, but only after an `approved` link between exactly that agent pair — never a whole-namespace grant. Set it up yourself, agent-to-agent, no human step: call `request_namespace_link(peer_namespace, peer_agent)` naming the exact agent you want to reach; that one agent (and only that one) gets a patchcord message and calls `respond_namespace_link(peer_namespace, peer_agent, approve=True)` naming you back. Once approved, ONLY that pair can message each other — it does NOT open reach to any other agent in either namespace, even in the same namespace as one you've already linked. Want to reach a second agent? Request a separate link for that specific pair. Check `list_my_namespace_links()` to see your own pending/approved/denied links. (Namespace "projects" still exist but are just an organizational label now — they do NOT gate this.)
197
+ - A rejection here is almost always correct behavior, not a server error — don't retry with variations of the name. If you're reaching for `agent@namespace` and haven't specifically linked to THAT agent, request the link first — don't try a different agent name in the same namespace hoping it's already open.
198
+ - **Do not reply to acks.** "ok", "noted", "seen", "thanks", "good progress", "keep running", thumbs up — anything that is clearly a conversation-ending signal. Just read them and move on. If you must close the thread, use `reply(id, resolve=true)` with NO content. Never send a text reply to an ack.
199
+ - **resolve=true with ack-only content is an anti-pattern.** `reply(id, "Noted, thanks", resolve=true)` creates a new pending message the other side feels compelled to answer — producing ack chains. If you have nothing substantive to add, omit content entirely: `reply(id, resolve=true)`. Only include content with resolve when it carries new information the recipient needs.
200
+ - **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
201
+ - 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.
202
+ - Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: subscribe
3
+ description: >
4
+ Start a persistent background WebSocket listener that
5
+ wakes Claude when new Patchcord messages arrive. Survives across turns
6
+ until the user kills it or closes the session. Use ONLY when the user
7
+ explicitly runs /patchcord:subscribe.
8
+ ---
9
+
10
+ User invoked /patchcord:subscribe — do NOT substitute `wait_for_message()`. Spawn the listener.
11
+
12
+ # Start
13
+
14
+ 1. **Drain the inbox first.** Call `mcp__patchcord__inbox`. If anything is pending, process it per the patchcord:inbox skill before continuing. Backlog can accumulate while no listener was up; subscribe must catch it.
15
+
16
+ 2. **Spawn the listener under Monitor** (not Bash with run_in_background — Monitor turns each stdout line into a notification):
17
+
18
+ ```
19
+ Monitor(
20
+ description: "patchcord realtime listener",
21
+ persistent: true,
22
+ timeout_ms: 3600000,
23
+ command: "patchcord subscribe | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}"
24
+ )
25
+ ```
26
+
27
+ The grep filter drops internal `HEARTBEAT` keepalive lines (written every 30 s to detect a dead pipe) — only `PATCHCORD:` lines fire notifications. `${PIPESTATUS[0]}` preserves subscribe's exit code through the pipe.
28
+
29
+ `subscribe.mjs` handles its own pidfile guard — if another listener is already active for this agent it exits with code 2 and stderr `already running (pid N)`. Monitor catches the stream-end event; read the output file and report.
30
+
31
+ 3. **Tell the user one line:** *"Patchcord listener active — I'll pick up new messages as they arrive."*
32
+
33
+ # When a notification fires
34
+
35
+ Monitor surfaces `PATCHCORD: 1 new from <sender>`:
36
+
37
+ 1. Say: *"Got a Patchcord ping from <sender> — checking inbox."*
38
+ 2. Call `mcp__patchcord__inbox`.
39
+ 3. Do the work per the patchcord:inbox skill, reply with what you did.
40
+
41
+ # Stopping
42
+
43
+ Tell the user one of:
44
+ - Close this Claude Code session.
45
+ - `kill $(cat /tmp/patchcord_subscribe_<namespace>_<agent>.pid)`
46
+
47
+ # If the Monitor stream ends
48
+
49
+ Read the output file. Scan the last ~15 lines for one of:
50
+
51
+ - `no .mcp.json in <cwd>` — session is not in a patchcord project dir
52
+ - `ticket: token rejected (HTTP 401|403)` — bad bearer; user regenerates from dashboard
53
+ - `ticket: server not configured for realtime` — self-hosted without realtime configured
54
+ - `ticket: namespace not owned` — token lost its owner; regenerate
55
+ - `already running (pid N)` (exit 2) — another listener is active; report and stop
56
+ - `subscribe: fatal: ...` — surface the line verbatim
57
+
58
+ Report the cause in one sentence. STOP.
59
+
60
+ # If the MCP tools 401 — check before you believe the error
61
+
62
+ `mcp__patchcord__*` returning **401 / "requires re-authorization (token expired)"** does NOT establish that the token expired. Claude Code keeps a **local** MCP config cache in `~/.claude.json` under `projects[<project dir>].mcpServers`, and local scope **beats** the project's `.mcp.json`. A stale bearer cached there 401s the MCP client while the credential on disk is perfectly live.
63
+
64
+ **Always run `patchcord whoami --json` before reporting an auth failure.** It reads the disk config, so it keeps working in exactly this state:
65
+
66
+ ```bash
67
+ patchcord whoami --json
68
+ ```
69
+
70
+ If `warnings` contains `claude_local_mcp_cache_override`, **relay its `tell_human` text to the user verbatim.** They have no reason to know this command exists — all they saw was a tool failing.
71
+
72
+ Then **offer to clear it**: *"Want me to remove the stale entry?"* Ask first — `~/.claude.json` is the user's global editor config, holds far more than MCP servers, and writing it races the running Claude Code process. But do not stop at diagnosing. Most users do not want to hand-edit JSON, and an accurate report they cannot act on leaves them exactly as stuck as no report.
73
+
74
+ If they say yes:
75
+
76
+ 1. **Back it up first** — `cp ~/.claude.json ~/.claude.json.bak.$(date +%s)`.
77
+ 2. **Remove only** `projects["<dir>"].mcpServers["patchcord"]`. Not the whole `mcpServers` object, not the project entry, not the file. Parse, delete the one key, write atomically.
78
+ 3. **Then tell them to run `/mcp` and reconnect patchcord.**
79
+
80
+ **Step 3 is the one that actually heals it, and it is the one that gets forgotten.** Clearing the entry changes a file the running MCP client already read; until it reloads, the tools keep 401-ing with the stale token and it looks like the fix failed. `/mcp` → reconnect reloads in place. Never tell them to restart Claude Code — if they are talking to you, they have already restarted, and it would not have helped anyway.
81
+
82
+ You cannot run `/mcp` yourself; it is an interactive command in the user's client. Say the words and let them press it.
83
+
84
+ Do not ask another agent to edit the file for you.
85
+
86
+ ## If `whoami` is clean and the MCP tools STILL 401
87
+
88
+ This is a **different failure with an identical symptom**, and it is the one that leaves you stuck if you stop at "identity is fine".
89
+
90
+ The stale-cache bug above is **cache newer than disk**. This one is the mirror image — **disk newer than your process**:
91
+
92
+ 1. Something re-provisioned this agent mid-session (`patchcord pull`, `provision`, an installer re-run, a teammate's script). That **rewrites `.mcp.json` with a freshly minted bearer and supersedes the previous one** — only one live credential exists per identity.
93
+ 2. Your MCP client is still holding the bearer it read at session start. That token is now dead.
94
+ 3. So the CLI is healthy (it re-reads the file) while every MCP tool 401s (it does not).
95
+
96
+ **The CLI cannot detect this**, which is why it is not a `warnings[]` entry: no external process can see your client's in-process token. Confirm it by hand instead:
97
+
98
+ ```bash
99
+ stat -c '%y %n' .mcp.json # was it modified after this session started?
100
+ ```
101
+
102
+ A modification time later than your session start is the answer.
103
+
104
+ **Then reconnect the MCP client** — that is the fix, and it is the step people miss. Do **not** conclude that the token is broken, and do not keep retrying `inbox()`: a superseded token will 401 forever, and repeating the call reports the same error indefinitely. If reconnecting is not something you can do yourself, **tell the human that the MCP client needs reconnecting and why**, naming the rewrite time.
105
+
106
+ Reported by `lead@mux-v2`, who followed the procedure above, correctly concluded "not that bug", and then had nowhere to go for several hours.
107
+
108
+ If `whoami` is clean, `.mcp.json` was not touched this session, and the tools still fail, the problem is neither of these — say so plainly rather than guessing at the token.
109
+
110
+ **Forbidden on failure:** no `pgrep`/`ps`/`kill`/`pkill`/`killall`, no pidfile writes, no respawning. The script manages pidfile cleanup itself; respawning will not fix a config problem.
111
+
112
+ No matching error pattern = the listener exited cleanly (session ended, user killed it, or EPIPE detected). Nothing to do.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: wait
3
+ description: >
4
+ Block this turn for up to 5 minutes waiting for one incoming Patchcord
5
+ message via the wait_for_message MCP tool. Single blocking call, no
6
+ background process. Use ONLY when the user explicitly runs
7
+ /patchcord:wait.
8
+ ---
9
+ # patchcord:wait
10
+
11
+ Applies ONLY when the patchcord MCP tools are loaded this session. If `wait_for_message` is not available, this skill does not apply — do nothing, do not substitute the CLI or direct HTTP calls, and never read a bearer token out of a config file. Proceed with the user's request.
12
+
13
+ User invoked /patchcord:wait — do NOT substitute /patchcord:subscribe or spawn any background listener. Use `wait_for_message()` only.
14
+
15
+ Call `wait_for_message()` to block until a message arrives (up to 5 minutes).
16
+
17
+ When a message arrives:
18
+
19
+ 1. Read it — the tool returns from, content, and message_id. If it belongs to a thread, `thread` and `thread_id` will be set.
20
+ 2. Do the work described in the message first. Update the file, write the code, fix the bug - whatever it asks.
21
+ 3. Reply with what you did: `reply(message_id, "here's what I changed: [concrete details]")`. Thread is auto-inherited. Use `resolve=true` to close the thread when the task is fully done.
22
+ 4. Tell the human who wrote and what you did about it
23
+ 5. Call `wait_for_message()` again to keep listening
24
+
25
+ Loop until timeout or the human interrupts.
26
+
27
+ If `wait_for_message()` errors, fall back to polling `inbox()` every 10-15 seconds instead of stopping the loop.
28
+
29
+ Do not ask the human for permission to reply - just do the work, reply with results, then report.
30
+
31
+ **No ack chains.** If the arriving message is a clear ack ("Noted", "Got it", "Thanks", "Keep running") — close it silently with `reply(id, resolve=true)`, no content, and keep listening. Never text-reply to an ack. Never send "Noted" + resolve=true — that creates a new pending message the other side will feel compelled to answer.
package/bin/patchcord.mjs CHANGED
@@ -683,6 +683,7 @@ if (cmd === "whoami") {
683
683
  codex: "codex", kimi: "kimi", "kimi-code": "kimi",
684
684
  opencode: "opencode", agy: "antigravity", antigravity: "antigravity",
685
685
  cursor: "cursor", grok: "grok", hermes: "hermes",
686
+ jcode: "jcode",
686
687
  };
687
688
  const resolveOpts = {};
688
689
  if (toolFlag) {
@@ -907,8 +908,28 @@ if (cmd === "upload") {
907
908
  console.error(`file is empty: ${filePath}`);
908
909
  process.exit(1);
909
910
  }
910
- if (stats.size > 25 * 1024 * 1024) {
911
- console.error(`file is ${(stats.size / 1024 / 1024).toFixed(1)}MB; max is 25MB for inline upload.`);
911
+ // THIS IS NOT THE UPLOAD LIMIT AND MUST NOT BE DESCRIBED AS ONE.
912
+ //
913
+ // It used to say "max is 25MB", which was wrong in both directions. The
914
+ // server's limit is PATCHCORD_ATTACHMENT_MAX_BYTES, 10 MiB by default and
915
+ // raisable on a self-hosted install. So this client refused files a
916
+ // configured server would have accepted, and accepted files every default
917
+ // server rejects: a 15 MB file passed here, got base64'd to 20 MB, was
918
+ // uploaded in full, and came back 413. A guard set above the real limit does
919
+ // not guard; it only pays the upload first.
920
+ //
921
+ // The client must not hold a copy of a number the server owns. What is left
922
+ // is a memory guard for THIS process: the file, its base64 (4/3 the size),
923
+ // and the JSON body are all resident at once. The real limit comes back from
924
+ // the server, in max_bytes, and is printed below.
925
+ const MEMORY_GUARD_BYTES = 256 * 1024 * 1024;
926
+ if (stats.size > MEMORY_GUARD_BYTES) {
927
+ console.error(
928
+ `file is ${(stats.size / 1024 / 1024).toFixed(1)}MB. This client buffers the ` +
929
+ `file and its base64 copy in memory and refuses above ` +
930
+ `${MEMORY_GUARD_BYTES / 1024 / 1024}MB. This is a client memory guard, not ` +
931
+ `the server's size limit — the server's limit is lower.`
932
+ );
912
933
  process.exit(1);
913
934
  }
914
935
 
@@ -923,7 +944,18 @@ if (cmd === "upload") {
923
944
  "POST", `${baseUrl}/api/agent/attachment/upload`, token, body
924
945
  );
925
946
  if (status !== "200") {
926
- console.error(`✗ HTTP ${status}: ${(json && json.error) || respBody}`);
947
+ // Print max_bytes/allowed when the server sends them. Without this the user
948
+ // reads "attachment exceeds maximum size" and is not told the maximum, so
949
+ // their next move is to guess — or to ask an agent, which will guess.
950
+ let detail = "";
951
+ if (json && typeof json.max_bytes === "number") {
952
+ detail = ` (server limit ${(json.max_bytes / 1024 / 1024).toFixed(1)}MiB` +
953
+ (typeof json.actual_bytes === "number"
954
+ ? `, this file ${(json.actual_bytes / 1024 / 1024).toFixed(1)}MiB)` : ")");
955
+ } else if (json && Array.isArray(json.allowed)) {
956
+ detail = ` (allowed: ${json.allowed.join(", ")})`;
957
+ }
958
+ console.error(`✗ HTTP ${status}: ${(json && json.error) || respBody}${detail}`);
927
959
  process.exit(1);
928
960
  }
929
961
  console.log(json.path);
@@ -1500,6 +1532,48 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1500
1532
  } catch {}
1501
1533
  return agWritten;
1502
1534
  }
1535
+ if (tool === "jcode") {
1536
+ // jcode is the ONLY harness here that gets a STDIO entry, and the reason
1537
+ // is not preference: jcode has no HTTP transport. Its loader keeps an
1538
+ // entry only when `is_stdio()` holds and drops the rest at load time --
1539
+ // "MCP: Skipping non-stdio server '<name>' (http); HTTP/SSE transports
1540
+ // are not yet supported" (crates/jcode-base/src/mcp/protocol.rs). So the
1541
+ // `type: "http"` entry every other harness receives is READ, RECOGNISED
1542
+ // AND DISCARDED by jcode, with a log line and no error. A bridge process
1543
+ // is the only shape it can run today.
1544
+ //
1545
+ // NAMED patchcord-jcode, NOT patchcord, AND THAT IS LOAD-BEARING.
1546
+ // jcode merges .jcode/mcp.json, then .mcp.json, then .claude/mcp.json,
1547
+ // with later files overriding same-named servers. A guard currently saves
1548
+ // us -- a non-stdio entry never displaces a working stdio one (their
1549
+ // issue #653) -- but that guard holds only while jcode CANNOT run http.
1550
+ // The day it gains that transport, a shared name means .mcp.json wins,
1551
+ // and jcode silently starts authenticating as claude_code's agent: two
1552
+ // harnesses, one credential, no error anywhere. A distinct name cannot
1553
+ // collide in the first place. Codex already does this (patchcord-codex).
1554
+ //
1555
+ // The bridge command mirrors the OpenClaw fallback this file already
1556
+ // prints. NOT VERIFIED END TO END from here: no jcode on this machine,
1557
+ // and proving it needs a live token against the real endpoint.
1558
+ const jdir = join(dir, ".jcode"); mkdirSync(jdir, { recursive: true });
1559
+ return writeJson(join(jdir, "mcp.json"), (o) => {
1560
+ o.mcpServers = o.mcpServers || {};
1561
+ // No `type` key at all. jcode infers stdio from the presence of
1562
+ // `command`, and writing "type": "stdio" is one more string to get
1563
+ // wrong for a default that is already correct.
1564
+ o.mcpServers["patchcord-jcode"] = {
1565
+ command: "npx",
1566
+ args: [
1567
+ "mcp-remote",
1568
+ `${baseUrl}/mcp`,
1569
+ "--header",
1570
+ `Authorization: Bearer ${token}`,
1571
+ "--header",
1572
+ `X-Patchcord-Machine: ${hostname}`,
1573
+ ],
1574
+ };
1575
+ });
1576
+ }
1503
1577
  if (tool === "hermes") {
1504
1578
  // Hermes reads MCP servers ONLY from its GLOBAL ~/.hermes/config.yaml
1505
1579
  // (mcp_servers key) — it ignores a project-local .mcp.json. So unlike the
@@ -1555,7 +1629,7 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1555
1629
  console.error(` .mcp.json → claude_code`);
1556
1630
  console.error(` .cursor/mcp.json → cursor`);
1557
1631
  console.error(` Defaulting would overwrite an agent you did not name, so there is no default.`);
1558
- console.error(` One of: claude_code, codex, cursor, kimi, opencode, antigravity, grok, hermes`);
1632
+ console.error(` One of: claude_code, codex, cursor, kimi, opencode, antigravity, grok, hermes, jcode`);
1559
1633
  console.error(` ${usage}`);
1560
1634
  process.exit(1);
1561
1635
  };
@@ -3194,7 +3268,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3194
3268
  // falls through to no choice instead of silently configuring a DIFFERENT
3195
3269
  // harness and writing someone's token to the wrong file.
3196
3270
  "vscode": "6", "zed": "7", "opencode": "8", "openclaw": "9", "antigravity": "10",
3197
- "cline": "11", "kimi": "12", "hermes": "13",
3271
+ "cline": "11", "kimi": "12", "hermes": "13", "jcode": "15",
3198
3272
  };
3199
3273
 
3200
3274
 
@@ -3583,6 +3657,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3583
3657
  const isCline = choice === "11";
3584
3658
  const isKimi = choice === "12";
3585
3659
  const isHermes = choice === "13";
3660
+ const isJcode = choice === "15";
3586
3661
 
3587
3662
  // MoonshotAI ships TWO CLIs that both use the `kimi` command:
3588
3663
  // • kimi-cli (Python): supports `--mcp-config-file`, config in .kimi/mcp.json
@@ -3657,6 +3732,36 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3657
3732
  } catch (e) {
3658
3733
  console.log(`\n ${yellow}⚠ Failed to write ${grokPath}: ${e.message}${r}`);
3659
3734
  }
3735
+ } else if (isJcode) {
3736
+ // jcode: project-local .jcode/mcp.json, and a STDIO entry rather than the
3737
+ // http one every other harness gets. jcode has no HTTP transport -- it
3738
+ // recognises `type: "http"` and drops the server at load time with a log
3739
+ // line. See the writeWorkerConfig branch above for why the entry is named
3740
+ // `patchcord-jcode` and not `patchcord`; the short version is that jcode
3741
+ // merges .jcode/mcp.json, .mcp.json and .claude/mcp.json by server NAME,
3742
+ // so a shared name lets claude_code's credential silently become jcode's
3743
+ // the day jcode gains http support.
3744
+ const jcodePath = join(cwd, ".jcode", "mcp.json");
3745
+ try {
3746
+ mkdirSync(dirname(jcodePath), { recursive: true });
3747
+ let jobj = {};
3748
+ try { jobj = JSON.parse(readFileSync(jcodePath, "utf-8")); } catch {}
3749
+ jobj.mcpServers = jobj.mcpServers || {};
3750
+ jobj.mcpServers["patchcord-jcode"] = {
3751
+ command: "npx",
3752
+ args: [
3753
+ "mcp-remote",
3754
+ `${serverUrl}/mcp`,
3755
+ "--header",
3756
+ `Authorization: Bearer ${token}`,
3757
+ ],
3758
+ };
3759
+ writeSecureFile(jcodePath, JSON.stringify(jobj, null, 2) + "\n");
3760
+ console.log(`\n ${green}✓${r} jcode configured: ${dim}${jcodePath}${r}`);
3761
+ console.log(` ${dim}Bridged over stdio — jcode does not speak HTTP MCP yet.${r}`);
3762
+ } catch (e) {
3763
+ console.log(`\n ${yellow}⚠ Failed to write ${jcodePath}: ${e.message}${r}`);
3764
+ }
3660
3765
  } else if (isHermes) {
3661
3766
  // Hermes: global only (~/.hermes/config.yaml, YAML, mcp_servers key)
3662
3767
  const hermesPath = join(HOME, ".hermes", "config.yaml");
@@ -4324,10 +4429,11 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
4324
4429
  // Hermes is global config (~/.hermes/config.yaml) — no per-project file to ignore.
4325
4430
  if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isHermes) {
4326
4431
  const gitignorePath = join(cwd, ".gitignore");
4327
- const configFile = isKimiCode ? ".kimi-code/mcp.json" : isKimi ? ".kimi/mcp.json" : isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isGrok ? ".grok/config.toml" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : isAntigravity ? ".agents/mcp_config.json" : ".mcp.json";
4432
+ const configFile = isJcode ? ".jcode/mcp.json" : isKimiCode ? ".kimi-code/mcp.json" : isKimi ? ".kimi/mcp.json" : isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isGrok ? ".grok/config.toml" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : isAntigravity ? ".agents/mcp_config.json" : ".mcp.json";
4328
4433
  // Forms that already cover this config (its file or its dir)
4329
4434
  const patterns = [configFile];
4330
- if (isKimiCode) patterns.push(".kimi-code/");
4435
+ if (isJcode) patterns.push(".jcode/");
4436
+ else if (isKimiCode) patterns.push(".kimi-code/");
4331
4437
  else if (isKimi) patterns.push(".kimi/");
4332
4438
  else if (isCodex) patterns.push(".codex/");
4333
4439
  else if (isCursor) patterns.push(".cursor/");
@@ -4352,7 +4458,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
4352
4458
  }
4353
4459
  }
4354
4460
 
4355
- const toolName = isHermes ? "Hermes" : isKimiCode ? "Kimi Code" : isKimi ? "Kimi Code" : isAntigravity ? "Antigravity CLI" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isGrok ? "Grok CLI" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
4461
+ const toolName = isJcode ? "jcode" : isHermes ? "Hermes" : isKimiCode ? "Kimi Code" : isKimi ? "Kimi Code" : isAntigravity ? "Antigravity CLI" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isGrok ? "Grok CLI" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
4356
4462
 
4357
4463
  if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isKimi && !isHermes) {
4358
4464
  console.log(`\n ${dim}To connect a second agent:${r}`);
package/harnesses.json CHANGED
@@ -217,6 +217,23 @@
217
217
  "installer_config": "cline_mcp_settings.json (VS Code globalStorage)",
218
218
  "harness_scope": "unknown",
219
219
  "listener": { "wake": "none", "mechanism": null, "self_arm": false, "survives_wake": null, "evidence": "declared" }
220
+ },
221
+ {
222
+ "id": "jcode",
223
+ "aliases": [],
224
+ "cli": "jcode",
225
+ "kind": "terminal",
226
+ "installer_scope": "project",
227
+ "installer_config": ".jcode/mcp.json",
228
+ "harness_scope": "project",
229
+ "listener": {
230
+ "wake": "none",
231
+ "mechanism": null,
232
+ "self_arm": false,
233
+ "survives_wake": null,
234
+ "evidence": "declared",
235
+ "note": "STDIO ONLY, WHICH IS WHY THIS ROW IS NOT LIKE THE OTHERS. jcode reads .jcode/mcp.json, .mcp.json and .claude/mcp.json, so it ALREADY finds the Claude Code entry we write - and drops it, because jcode supports stdio servers only and skips type http/sse at load time with a log line (crates/jcode-base/src/mcp/protocol.rs, retain on is_stdio). So our installer writes a stdio bridge entry instead of the http entry every other harness gets. Two consequences a reader must not undo: the server is named patchcord-jcode, NOT patchcord, because jcode merges those three files with later ones overriding by NAME - a shared name means the day jcode gains http transport, the .mcp.json entry becomes runnable and silently overrides, and jcode starts authenticating as claude_code's agent. And no wake: no subscribe path exists for jcode in this package."
236
+ }
220
237
  }
221
238
  ],
222
239
  "retired": [
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.42",
3
+ "version": "0.6.43",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "scripts": {
6
- "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json"
6
+ "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json agent-plugin/plugin.json"
7
7
  },
8
8
  "author": "ppravdin",
9
9
  "license": "MIT",
@@ -33,6 +33,7 @@
33
33
  "commands/",
34
34
  "README.md",
35
35
  "plugins/",
36
+ "agent-plugin/",
36
37
  "harnesses.json"
37
38
  ]
38
39
  }
@@ -99,7 +99,7 @@ To message a user outside your namespace, use `@username` as the to_agent. Examp
99
99
  ```
100
100
  patchcord upload /path/to/report.md --mime text/markdown
101
101
  ```
102
- Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat. 25MB cap.
102
+ Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat. The size limit belongs to the server: 10 MiB by default, raisable on a self-hosted server. Too large prints the server's own limit.
103
103
 
104
104
  **Public URLs → `attachment(relay=true, ...)`:**
105
105
  ```
@@ -105,7 +105,7 @@ To message a user outside your namespace, use `@username` as the to_agent. Examp
105
105
  ```
106
106
  patchcord upload /path/to/report.md --mime text/markdown
107
107
  ```
108
- Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. 25MB cap.
108
+ Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. The size limit is the server's, not a number to remember: it is 10 MiB by default and a self-hosted server can raise it. If a file is too large the command prints the server's own limit.
109
109
 
110
110
  **Public URLs → `attachment(relay=true, ...)`:**
111
111
  ```
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate the Agent Plugins 1.0.0 distribution at ./agent-plugin/.
4
+ *
5
+ * PURELY ADDITIVE. This does not touch .claude-plugin/, skills/, hooks/,
6
+ * commands/, or anything the current installer writes. Every existing harness
7
+ * keeps working exactly as it does today; the generated directory is a second,
8
+ * parallel packaging of the same content for clients that load the open
9
+ * standard (Codex/ChatGPT, Cursor, GitHub Copilot, Kiro, VS Code).
10
+ *
11
+ * WHY GENERATED RATHER THAN HAND-WRITTEN
12
+ *
13
+ * The skills are the same skills. Copying them by hand would create two
14
+ * SKILL.md bodies that drift, and the drift would be silent — both files stay
15
+ * valid, they just stop agreeing. Deriving them means the source of truth
16
+ * remains ./skills/ and this script is the only thing that has to be re-run.
17
+ *
18
+ * THE ONE TRANSFORM APPLIED
19
+ *
20
+ * Our skills declare `name: patchcord:inbox`. The Agent Skills specification
21
+ * that Agent Plugins defers to requires the name to be lowercase alphanumeric
22
+ * plus hyphens ONLY (a colon is invalid) and to MATCH THE PARENT DIRECTORY.
23
+ * Every real Codex plugin on this machine follows that rule.
24
+ *
25
+ * So the generated copies get `name: <directory>`. The originals are left
26
+ * alone, because that string is what Claude Code surfaces as
27
+ * `/patchcord:inbox` and renaming it in place would change a user-facing
28
+ * command name to fix a spec violation nobody is currently enforcing. Two
29
+ * packagings, two conventions, one body.
30
+ */
31
+
32
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, existsSync } from "node:fs";
33
+ import { join, dirname } from "node:path";
34
+ import { fileURLToPath } from "node:url";
35
+
36
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
37
+ const OUT = join(ROOT, "agent-plugin");
38
+ const SKILLS_SRC = join(ROOT, "skills");
39
+
40
+ const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
41
+ const VERSION = pkg.version;
42
+ const BASE_URL = "https://mcp.patchcord.dev";
43
+
44
+ // Regenerate from scratch so a skill deleted upstream does not linger here.
45
+ if (existsSync(OUT)) rmSync(OUT, { recursive: true });
46
+ mkdirSync(join(OUT, ".codex-plugin"), { recursive: true });
47
+
48
+ /* ---------- skills: copied, with `name` rewritten to the directory ---------- */
49
+
50
+ const skillNames = readdirSync(SKILLS_SRC, { withFileTypes: true })
51
+ .filter((d) => d.isDirectory() && existsSync(join(SKILLS_SRC, d.name, "SKILL.md")))
52
+ .map((d) => d.name);
53
+
54
+ if (skillNames.length === 0) {
55
+ console.error("build-agent-plugin: no skills found under ./skills — refusing to emit an empty plugin");
56
+ process.exit(1);
57
+ }
58
+
59
+ for (const name of skillNames) {
60
+ const body = readFileSync(join(SKILLS_SRC, name, "SKILL.md"), "utf-8");
61
+ // Replace only the `name:` line inside the leading frontmatter block. Anchored
62
+ // to the start of the file so a later `name:` in prose cannot be hit.
63
+ const fm = body.match(/^---\r?\n([\s\S]*?)\r?\n---/);
64
+ if (!fm) {
65
+ console.error(`build-agent-plugin: ${name}/SKILL.md has no frontmatter — refusing to guess`);
66
+ process.exit(1);
67
+ }
68
+ const rewritten = body.replace(/^(---\r?\n[\s\S]*?)^name:[ \t]*.*$/m, `$1name: ${name}`);
69
+ if (rewritten === body && !new RegExp(`^name:[ \\t]*${name}\\s*$`, "m").test(fm[1])) {
70
+ console.error(`build-agent-plugin: could not rewrite name: in ${name}/SKILL.md`);
71
+ process.exit(1);
72
+ }
73
+ mkdirSync(join(OUT, "skills", name), { recursive: true });
74
+ writeFileSync(join(OUT, "skills", name, "SKILL.md"), rewritten);
75
+ }
76
+
77
+ /* ---------- Agent Plugins 1.0.0 core: plugin.json + mcp.json ---------- */
78
+
79
+ // The AP manifest schema is CLOSED (additionalProperties: false). Only the
80
+ // fields it names may appear — notably there is no `skills` pointer, because
81
+ // the spec fixes that location at ./skills/ rather than letting a manifest
82
+ // redirect it.
83
+ writeFileSync(join(OUT, "plugin.json"), JSON.stringify({
84
+ $schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
85
+ name: "patchcord",
86
+ version: VERSION,
87
+ description: pkg.description,
88
+ author: { name: "ppravdin", url: "https://patchcord.dev" },
89
+ homepage: "https://patchcord.dev",
90
+ repository: "https://github.com/ppravdin/patchcord",
91
+ license: pkg.license,
92
+ keywords: pkg.keywords,
93
+ }, null, 2) + "\n");
94
+
95
+ // NOTE THE MISSING CREDENTIAL, DELIBERATELY.
96
+ //
97
+ // Agent Plugins 1.0.0 defines placeholder expansion for ${PLUGIN_ROOT} and
98
+ // ${PLUGIN_DATA} ONLY, and only in `args`, `env`, and `cwd`. `headers` is not
99
+ // in that list, and the spec has no host-environment passthrough at all. So
100
+ // there is no conformant way to say "put THIS user's bearer in the
101
+ // Authorization header" in this file.
102
+ //
103
+ // Writing `"Authorization": "Bearer ${PATCHCORD_TOKEN}"` here would produce a
104
+ // file that looks correct and sends the literal string `${PATCHCORD_TOKEN}` to
105
+ // the server. That is worse than an obviously incomplete file, so this one is
106
+ // obviously incomplete: it names the endpoint and the transport and stops.
107
+ //
108
+ // Clients supply the missing half through their own extensions — Codex via
109
+ // `bearer_token_env_var` in .mcp.json (below), VS Code via `envFile`/`headers`.
110
+ // See agent-plugin/README.md.
111
+ writeFileSync(join(OUT, "mcp.json"), JSON.stringify({
112
+ $schema: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
113
+ mcpServers: {
114
+ patchcord: { type: "streamable-http", url: `${BASE_URL}/mcp` },
115
+ },
116
+ }, null, 2) + "\n");
117
+
118
+ /* ---------- Codex client files ---------- */
119
+
120
+ // Codex reads .codex-plugin/plugin.json with POINTER fields (skills, mcpServers)
121
+ // rather than the spec's fixed locations, so it needs its own manifest. Both
122
+ // live in one directory without colliding: different filenames, different paths.
123
+ writeFileSync(join(OUT, ".codex-plugin", "plugin.json"), JSON.stringify({
124
+ name: "patchcord",
125
+ version: VERSION,
126
+ description: pkg.description,
127
+ author: { name: "ppravdin", url: "https://patchcord.dev" },
128
+ homepage: "https://patchcord.dev",
129
+ repository: "https://github.com/ppravdin/patchcord",
130
+ license: pkg.license,
131
+ keywords: pkg.keywords,
132
+ skills: "./skills/",
133
+ mcpServers: "./.mcp.json",
134
+ }, null, 2) + "\n");
135
+
136
+ // `bearer_token_env_var` is Codex's own field — it names an environment
137
+ // variable to read the token FROM, so the credential never enters this file.
138
+ // That is what makes the plugin publishable: it carries no secret and is
139
+ // identical for every user.
140
+ writeFileSync(join(OUT, ".mcp.json"), JSON.stringify({
141
+ mcpServers: {
142
+ patchcord: {
143
+ type: "http",
144
+ url: `${BASE_URL}/mcp`,
145
+ bearer_token_env_var: "PATCHCORD_TOKEN",
146
+ },
147
+ },
148
+ }, null, 2) + "\n");
149
+
150
+ /* ---------- README (emitted, because this directory is wiped each build) ---------- */
151
+
152
+ writeFileSync(join(OUT, "README.md"), `# patchcord — Agent Plugins 1.0.0 packaging
153
+
154
+ **Generated. Do not edit by hand** — run \`node scripts/build-agent-plugin.mjs\`.
155
+ This whole directory is deleted and rewritten on every build.
156
+
157
+ This is an EXPERIMENTAL second packaging of the same skills and the same MCP
158
+ server. It replaces nothing. \`npx patchcord\` and every per-harness config the
159
+ installer writes are untouched and keep working exactly as before.
160
+
161
+ ## What is in here
162
+
163
+ | File | Read by | Purpose |
164
+ |---|---|---|
165
+ | \`plugin.json\` | Agent Plugins clients | The open-standard manifest. Required: \`$schema\`, \`name\`. |
166
+ | \`mcp.json\` | Agent Plugins clients | Standard MCP declaration. **Carries no credential — see below.** |
167
+ | \`.codex-plugin/plugin.json\` | Codex | Codex uses pointer fields (\`skills\`, \`mcpServers\`) instead of the spec's fixed locations. |
168
+ | \`.mcp.json\` | Codex | Codex MCP config, with the token read from an env var. |
169
+ | \`skills/*/SKILL.md\` | both | Copied from \`../skills/\`, with \`name:\` rewritten to the directory name. |
170
+
171
+ ## The credential, and why \`mcp.json\` looks incomplete
172
+
173
+ Agent Plugins 1.0.0 expands \`\${PLUGIN_ROOT}\` and \`\${PLUGIN_DATA}\` only, and
174
+ only inside \`args\`, \`env\`, and \`cwd\`. \`headers\` is not in that list and there
175
+ is no host-environment passthrough anywhere in the spec.
176
+
177
+ So **the standard has no way to express "use this user's bearer token"**, and
178
+ patchcord is nothing but a per-project bearer token. Writing
179
+ \`"Authorization": "Bearer \${PATCHCORD_TOKEN}"\` into \`mcp.json\` would send that
180
+ literal string to the server. The file therefore names the endpoint and the
181
+ transport and stops, rather than looking complete and failing at runtime.
182
+
183
+ Clients close the gap with their own extensions, which is where the token
184
+ actually comes from:
185
+
186
+ - **Codex** — \`bearer_token_env_var\` in \`.mcp.json\`, pointing at
187
+ \`$PATCHCORD_TOKEN\`.
188
+ - **VS Code** — \`envFile\` / \`headers\`, neither of which is in the core schema.
189
+
190
+ This is the one finding worth taking upstream: the portable core can carry the
191
+ server's identity everywhere, but not its credential.
192
+
193
+ ## Trying it in Codex
194
+
195
+ The marketplace is already registered, so:
196
+
197
+ \`\`\`bash
198
+ export PATCHCORD_TOKEN=<an agent bearer for the namespace you want>
199
+ codex plugin add patchcord-ap@patchcord-marketplace
200
+ \`\`\`
201
+
202
+ Remove it with \`codex plugin remove patchcord-ap@patchcord-marketplace\`. The
203
+ existing \`patchcord@patchcord-marketplace\` entry is unaffected either way.
204
+
205
+ ## Known limitation: one token per environment
206
+
207
+ \`$PATCHCORD_TOKEN\` is a single value per shell, while patchcord's model is one
208
+ namespace per project. A plugin installed this way is therefore **one identity
209
+ per environment**, not one per project — the same constraint that already
210
+ applies to Hermes, and the reason the current per-project installer writes
211
+ per-directory config instead. Do not use this packaging for multi-seat work
212
+ until the standard grows a per-project secret mechanism.
213
+ `);
214
+
215
+ console.log(`build-agent-plugin: wrote agent-plugin/ (v${VERSION}, skills: ${skillNames.join(", ")})`);
@@ -126,6 +126,43 @@ const opencodeReader = (cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "
126
126
  const antigravityReader = (cwd) => readJsonAt(join(cwd, ".agents", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity");
127
127
  const grokReader = (cwd) => readGrokTomlShape(join(cwd, ".grok", "config.toml"));
128
128
  const codexReader = (cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml"));
129
+ // jcode: STDIO entry, so the bearer is in `args`, not in `headers`. readJsonAt
130
+ // cannot see it -- extractBearer requires headers.Authorization and a url, and
131
+ // this entry has neither. jcode has no HTTP transport, so an entry shaped like
132
+ // every other harness's would be dropped at load time, and a reader shaped like
133
+ // every other harness's finds nothing.
134
+ //
135
+ // Keyed on `patchcord-jcode`, matching what the installer writes. The distinct
136
+ // name exists so jcode's own merge (.jcode/mcp.json, then .mcp.json, then
137
+ // .claude/mcp.json -- later overriding by NAME) can never make jcode adopt
138
+ // claude_code's credential.
139
+ function readJcodeStdioShape(path) {
140
+ if (!existsSync(path)) return null;
141
+ try {
142
+ const obj = parseJsonc(readFileSync(path, "utf-8"));
143
+ const entry = obj?.mcpServers?.["patchcord-jcode"];
144
+ const args = Array.isArray(entry?.args) ? entry.args : null;
145
+ if (!args) return null;
146
+ // `--header` `Authorization: Bearer <token>` as two adjacent argv items.
147
+ let token = null;
148
+ for (let i = 0; i < args.length; i++) {
149
+ const m = typeof args[i] === "string" && args[i].match(/^Authorization:\s*Bearer\s+(\S+)$/i);
150
+ if (m) { token = m[1]; break; }
151
+ }
152
+ const urlArg = args.find((a) => typeof a === "string" && /^https?:\/\//.test(a));
153
+ if (!token || !urlArg) return null;
154
+ return {
155
+ token,
156
+ baseUrl: urlArg.replace(/\/mcp(\/bearer)?$/, ""),
157
+ configFile: path,
158
+ tool: "jcode",
159
+ };
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ const jcodeReader = (cwd) => readJcodeStdioShape(join(cwd, ".jcode", "mcp.json"));
129
166
  const kimiReader = (cwd) => readJsonAt(join(cwd, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
130
167
  const kimiCodeReader = (cwd) => readJsonAt(join(cwd, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
131
168
 
@@ -163,6 +200,7 @@ export function projectReadersForContext(ctx) {
163
200
  grok: grokReader,
164
201
  codex: codexReader,
165
202
  kimi: kimiCodeReader,
203
+ jcode: jcodeReader,
166
204
  };
167
205
  const defaultReaders = [
168
206
  claudeReader,
@@ -172,6 +210,7 @@ export function projectReadersForContext(ctx) {
172
210
  antigravityReader,
173
211
  grokReader,
174
212
  codexReader,
213
+ jcodeReader,
175
214
  ];
176
215
  const kimiReaders = [kimiReader, kimiCodeReader];
177
216
 
@@ -199,6 +238,7 @@ export function projectReadersForContext(ctx) {
199
238
  antigravityReader,
200
239
  grokReader,
201
240
  codexReader,
241
+ jcodeReader,
202
242
  claudeReader,
203
243
  ...kimiReaders,
204
244
  ];
@@ -10,15 +10,30 @@ import { readFileSync, writeFileSync } from "node:fs";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { dirname, join } from "node:path";
12
12
 
13
+ // EVERY manifest, not the one that existed when this was written. A second
14
+ // packaged plugin (agent-plugin/) arrived later and this script did not know
15
+ // about it, so a merge left it claiming 0.6.40 inside a 0.6.42 package — the
16
+ // same drift the comment above describes, reappearing the moment a new manifest
17
+ // was added. A list is what stops the next one being missed: add the path here
18
+ // and the sync is automatic, rather than a step someone must remember.
13
19
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
14
20
  const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
15
- const manifestPath = join(root, ".claude-plugin", "plugin.json");
16
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
21
+ const MANIFESTS = [
22
+ join(root, ".claude-plugin", "plugin.json"),
23
+ join(root, "agent-plugin", "plugin.json"),
24
+ ];
17
25
 
18
- if (manifest.version === pkg.version) {
19
- process.exit(0);
26
+ for (const manifestPath of MANIFESTS) {
27
+ let manifest;
28
+ try {
29
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
30
+ } catch {
31
+ // A manifest that is not present is not an error — agent-plugin/ is built
32
+ // in some trees and not others. A manifest that is present and STALE is.
33
+ continue;
34
+ }
35
+ if (manifest.version === pkg.version) continue;
36
+ manifest.version = pkg.version;
37
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
38
+ process.stderr.write(`synced ${manifestPath} version -> ${pkg.version}\n`);
20
39
  }
21
-
22
- manifest.version = pkg.version;
23
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
24
- process.stderr.write(`synced plugin.json version -> ${pkg.version}\n`);
@@ -114,7 +114,7 @@ To message a user outside your namespace, use `@username` as the to_agent. Examp
114
114
  ```
115
115
  patchcord upload /path/to/report.md --mime text/markdown
116
116
  ```
117
- Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. 25MB cap.
117
+ Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. The size limit is the server's, not a number to remember: it is 10 MiB by default and a self-hosted server can raise it. If a file is too large the command prints the server's own limit.
118
118
 
119
119
  **Public URLs → `attachment(relay=true, ...)`:**
120
120
  ```