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.
- package/.claude-plugin/marketplace.json +5 -0
- package/.claude-plugin/plugin.json +1 -1
- package/agent-plugin/.codex-plugin/plugin.json +22 -0
- package/agent-plugin/.mcp.json +9 -0
- package/agent-plugin/README.md +61 -0
- package/agent-plugin/mcp.json +9 -0
- package/agent-plugin/plugin.json +21 -0
- package/agent-plugin/skills/inbox/SKILL.md +202 -0
- package/agent-plugin/skills/subscribe/SKILL.md +112 -0
- package/agent-plugin/skills/wait/SKILL.md +31 -0
- package/bin/patchcord.mjs +114 -8
- package/harnesses.json +17 -0
- package/package.json +3 -2
- package/per-project-skills/codex/SKILL.md +1 -1
- package/per-project-skills/cursor/inbox/SKILL.md +1 -1
- package/scripts/build-agent-plugin.mjs +215 -0
- package/scripts/lib/resolve-project-bearer.mjs +40 -0
- package/scripts/sync-plugin-version.mjs +23 -8
- package/skills/inbox/SKILL.md +1 -1
|
@@ -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
|
}
|
|
@@ -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,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,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
|
-
|
|
911
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
16
|
-
|
|
21
|
+
const MANIFESTS = [
|
|
22
|
+
join(root, ".claude-plugin", "plugin.json"),
|
|
23
|
+
join(root, "agent-plugin", "plugin.json"),
|
|
24
|
+
];
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
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`);
|
package/skills/inbox/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
```
|