patchcord 0.6.41 → 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 +245 -0
- package/package.json +5 -3
- 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
|
@@ -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
|
```
|