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.
@@ -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
  ```