claudepack-connector 0.4.1

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/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # ClaudePack MCP connector
2
+
3
+ The desktop bridge that lets **Claude install ClaudePack skills for itself**. Connect it
4
+ to Claude Code, then just ask in plain language:
5
+
6
+ > "find a skill for writing commit messages and install it"
7
+
8
+ Claude searches the catalog, picks the match, and writes the files into
9
+ `~/.claude/skills/` — no manual file placing.
10
+
11
+ ## What it exposes (MCP tools)
12
+
13
+ | Tool | What it does |
14
+ |---|---|
15
+ | `search_skills(query)` | Search the catalog by keyword (id/title/summary/tags). |
16
+ | `install_skill(id)` | Copy a skill's files into `~/.claude/skills/<id>/`. |
17
+ | `list_installed_skills()` | List what's already installed. |
18
+
19
+ ## Connect it to Claude Code
20
+
21
+ Already wired for this project via `../.mcp.json`. To use it from **any** directory,
22
+ register it globally:
23
+
24
+ ```
25
+ claude mcp add claudepack -- node "C:\Users\SUMIT\Downloads\vs\claudepack-mcp\server.mjs"
26
+ ```
27
+
28
+ Then in a new session: `"search the catalog for a code review skill and install it"`.
29
+ After install, **start a new Claude Code session** so the skill loads.
30
+
31
+ ## How it works
32
+
33
+ - Dependency-free Node MCP server (newline-delimited JSON-RPC 2.0 over stdio).
34
+ - Catalog lives in `catalog/` — `catalog.json` (metadata) + `skills/<id>/` (the real files).
35
+ - Installs are real file copies. Override the target with `CLAUDEPACK_SKILLS_DIR` (used by tests).
36
+
37
+ ## Roadmap (incremental realism)
38
+
39
+ `local file catalog (now)` → `fetch from the ClaudePack registry` → `creator uploads + paid items`.
40
+ The tool surface (`search` → `install`) stays the same as the backend gets real.
@@ -0,0 +1,8 @@
1
+ [
2
+ { "id": "commit-message", "type": "skill", "version": "1.0.0", "title": "Commit Message Writer", "summary": "Writes clean Conventional Commits messages from a staged diff.", "description": "Generates a conventional commit message from your staged git changes, with an optional body explaining what changed and why.", "tags": ["git", "commits", "conventional-commits", "message", "workflow"], "files": ["SKILL.md"] },
3
+ { "id": "code-reviewer", "type": "skill", "version": "1.0.0", "title": "Code Reviewer", "summary": "Reviews a diff for correctness bugs, security issues and simplifications.", "description": "Reviews your code changes for logic bugs, security vulnerabilities, and simplifications, ranked by severity.", "tags": ["review", "quality", "security", "bugs", "git"], "files": ["SKILL.md"] },
4
+ { "id": "explain-error", "type": "skill", "version": "1.0.0", "title": "Error Explainer", "summary": "Explains a stack trace or error message and suggests concrete fixes.", "description": "Takes an error or stack trace and explains the root cause in plain language, then suggests the most likely fix.", "tags": ["debugging", "errors", "stacktrace", "fix", "exception"], "files": ["SKILL.md"] },
5
+ { "id": "pr-description", "type": "skill", "version": "1.0.0", "title": "PR Description Writer", "summary": "Drafts a clear pull request description from the branch diff.", "description": "Writes a pull request description (title, what & why, changes, testing) from your branch's diff and commit log.", "tags": ["git", "pull-request", "pr", "docs", "workflow"], "files": ["SKILL.md"] },
6
+ { "id": "test-writer", "type": "agent", "version": "1.0.0", "title": "Test Writer (subagent)", "summary": "A subagent that writes unit tests for a function, file or module.", "description": "A subagent that reads target code, detects your test framework, and writes focused unit tests covering happy paths, edge cases and errors.", "tags": ["testing", "unit-tests", "tdd", "subagent", "quality"], "files": ["test-writer.md"] },
7
+ { "id": "changelog", "type": "command", "version": "1.0.0", "title": "/changelog", "summary": "Slash command that turns recent commits into a changelog.", "description": "A slash command that reads recent git commits and writes a grouped, human-readable changelog entry.", "tags": ["git", "changelog", "release", "docs", "command"], "files": ["changelog.md"] }
8
+ ]
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Summarize recent commits into a human-readable changelog entry.
3
+ ---
4
+
5
+ Generate a changelog from recent git history.
6
+
7
+ 1. Run `git log --oneline -n 30`, or since the last tag: `git log $(git describe --tags --abbrev=0)..HEAD --oneline`.
8
+ 2. Group commits into **Added**, **Changed**, **Fixed**, **Removed**. Drop noise (merge commits, "wip", formatting-only churn).
9
+ 3. Rewrite each entry as a user-facing sentence, not the raw commit subject.
10
+ 4. Output markdown under a `## <version> — <date>` heading.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: code-reviewer
3
+ description: Use when the user wants their changes reviewed. Reviews a diff for correctness bugs, security issues, and simplifications.
4
+ ---
5
+
6
+ # Code Reviewer
7
+
8
+ When asked to review code:
9
+
10
+ 1. Get the diff: `git diff` (or `git diff <base>...HEAD` for a whole branch).
11
+ 2. Review only the changed lines, in three passes:
12
+ - **Correctness** — logic errors, off-by-one, null/undefined, error handling, race conditions.
13
+ - **Security** — injection, missing authorization, leaked secrets, unsafe input, XSS.
14
+ - **Simplification** — duplication, dead code, clearer equivalents.
15
+ 3. Report findings as a list: `file:line — severity — what's wrong — suggested fix`.
16
+ 4. Lead with the highest-severity issues. If nothing is wrong, say so plainly — don't invent nits.
17
+
18
+ Verify every claim against the actual code; never flag a line you haven't read.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: commit-message
3
+ description: Use when the user wants a commit message for their changes. Generates a Conventional Commits message from the staged diff.
4
+ ---
5
+
6
+ # Commit Message Writer
7
+
8
+ When the user asks for a commit message:
9
+
10
+ 1. Run `git diff --staged` to read the staged changes. If nothing is staged, run `git diff` and note you are describing unstaged changes.
11
+ 2. Write a single Conventional Commits subject line: `<type>(<scope>): <description>`, ≤72 chars, imperative mood.
12
+ - types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
13
+ 3. For a non-trivial change, add a body: a blank line, then 1–3 bullets on what changed and why (not how).
14
+ 4. Output ONLY the commit message in a code block — nothing else.
15
+
16
+ Describe what the diff actually does; never invent changes that aren't there.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: explain-error
3
+ description: Use when the user pastes an error message or stack trace. Explains the root cause in plain language and suggests concrete fixes.
4
+ ---
5
+
6
+ # Error Explainer
7
+
8
+ When given an error or stack trace:
9
+
10
+ 1. Identify the error type and the exact frame that triggered it — the first frame in the user's own code, not library internals.
11
+ 2. Explain the root cause in one or two plain sentences: what actually went wrong, not just a restatement of the message.
12
+ 3. Give the most likely fix as a concrete code change. If several causes are plausible, list them ranked by likelihood.
13
+ 4. If you need a file's contents to be sure, open it rather than guessing.
14
+
15
+ Point at the real line. Don't theorize about what "should" work.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: pr-description
3
+ description: Use when the user wants a pull request description. Drafts a clear PR summary from the branch diff.
4
+ ---
5
+
6
+ # PR Description Writer
7
+
8
+ When asked for a PR description:
9
+
10
+ 1. Get the branch diff vs the base: `git diff <base>...HEAD` and `git log <base>..HEAD --oneline`.
11
+ 2. Draft a description with:
12
+ - **Title** — one line, imperative.
13
+ - **What & why** — 2–4 sentences on the change and its motivation.
14
+ - **Changes** — a bullet list of the notable edits.
15
+ - **Testing** — how it was verified (or "not yet tested" if unknown).
16
+ 3. Output as markdown in a code block.
17
+
18
+ Summarize what the diff actually contains — don't claim tests or behaviour you can't see.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: test-writer
3
+ description: Use to write unit tests for a function, module, or file. Reads the target code and produces focused tests covering main paths and edge cases.
4
+ tools: Read, Grep, Glob, Edit, Bash
5
+ ---
6
+
7
+ You are a focused test-writing subagent.
8
+
9
+ When invoked:
10
+
11
+ 1. Read the target file(s) the user named. If they named a function, grep for it and read its definition plus a couple of call sites.
12
+ 2. Detect the test framework already in the project (jest / vitest / pytest / go test / …) from config and existing tests. Match it exactly — same imports, file naming, assertion style.
13
+ 3. Write tests covering: the happy path, boundary values, empty/null inputs, and error cases. Name each test descriptively for the behaviour it pins.
14
+ 4. Place tests where the project's convention puts them. If a test command exists, run the suite and report pass/fail.
15
+
16
+ Never invent behaviour — test what the code actually does. If the code looks buggy, flag it; don't write a test that locks in the bug.
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "claudepack-connector",
3
+ "version": "0.4.1",
4
+ "description": "Connect Claude to ClaudePack - search and install skills, subagents, commands and more by description. Catalog fetched live; no server, no cost.",
5
+ "type": "module",
6
+ "bin": {
7
+ "claudepack-connector": "server.mjs"
8
+ },
9
+ "files": [
10
+ "server.mjs",
11
+ "catalog"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "MIT",
17
+ "keywords": [
18
+ "claude",
19
+ "claude-code",
20
+ "mcp",
21
+ "skills",
22
+ "marketplace"
23
+ ]
24
+ }
package/server.mjs ADDED
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+ // ClaudePack MCP connector. Claude searches the ClaudePack catalog by a natural-
3
+ // language description and installs items (skills, subagents, commands, output-
4
+ // styles, plugins) into ~/.claude/ for itself.
5
+ //
6
+ // AUTO-UPDATE + SCALE + ZERO COST. Three data modes, tried in order:
7
+ // 1. API mode (CLAUDEPACK_API_URL): search hits /api/search and install hits
8
+ // /api/item/<id> - only results are transferred, so it scales to tens of
9
+ // thousands of items. Runs on free serverless (Vercel/Cloudflare free tier).
10
+ // 2. Static mode (CLAUDEPACK_CATALOG_URL): fetch catalog.json + files from a
11
+ // free static host (GitHub raw / Pages). Fine up to a few thousand items.
12
+ // 3. Bundled snapshot: offline fallback shipped with the package.
13
+ // Files always live on static hosting; only search/lookup needs the API at scale.
14
+ // Dependency-free: Node 18+ global fetch, JSON-RPC 2.0 over stdio.
15
+
16
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
17
+ import { writeFile, mkdir } from "node:fs/promises";
18
+ import { join, dirname, basename, resolve, relative, isAbsolute, sep } from "node:path";
19
+ import { homedir } from "node:os";
20
+ import { fileURLToPath } from "node:url";
21
+ import { createInterface } from "node:readline";
22
+
23
+ const HERE = dirname(fileURLToPath(import.meta.url));
24
+ const BUNDLED_DIR = join(HERE, "catalog");
25
+ const SERVER = { name: "claudepack", version: "0.4.1" };
26
+ const PROTOCOL_FALLBACK = "2025-06-18";
27
+
28
+ // ── path-safety ────────────────────────────────────────────────────
29
+ // The connector writes files to the user's disk based on data fetched from a
30
+ // remote catalog. Treat every id and file path from the catalog as untrusted:
31
+ // a malicious or compromised entry must never escape ~/.claude via "..",
32
+ // absolute paths, or drive letters. These guards run before any read/write.
33
+ const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
34
+
35
+ function assertSafeId(id) {
36
+ if (typeof id !== "string" || !SAFE_ID.test(id) || id.includes("..")) {
37
+ throw new Error(`Unsafe item id: ${JSON.stringify(id)}`);
38
+ }
39
+ return id;
40
+ }
41
+
42
+ // Validate a catalog-relative file path (may contain forward-slash subdirs like
43
+ // "references/x.md"). Reject absolute paths, backslashes, drive letters, and any
44
+ // ".." segment. Returns the normalized forward-slash relative path.
45
+ function assertSafeRel(rel) {
46
+ if (typeof rel !== "string" || rel.length === 0) {
47
+ throw new Error(`Unsafe file path: ${JSON.stringify(rel)}`);
48
+ }
49
+ if (rel.includes("\\") || isAbsolute(rel) || /^[a-zA-Z]:/.test(rel)) {
50
+ throw new Error(`Unsafe file path (absolute/backslash): ${JSON.stringify(rel)}`);
51
+ }
52
+ const parts = rel.split("/").filter((p) => p.length && p !== ".");
53
+ if (parts.some((p) => p === "..")) {
54
+ throw new Error(`Unsafe file path (traversal): ${JSON.stringify(rel)}`);
55
+ }
56
+ if (!parts.length) throw new Error(`Empty file path: ${JSON.stringify(rel)}`);
57
+ return parts.join("/");
58
+ }
59
+
60
+ // Final defense: ensure a resolved destination never leaves its base directory.
61
+ function assertWithin(baseDir, dest) {
62
+ const rel = relative(resolve(baseDir), resolve(dest));
63
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel) || rel.split(sep).includes("..")) {
64
+ throw new Error(`Refusing to write outside ${baseDir}: ${dest}`);
65
+ }
66
+ return dest;
67
+ }
68
+
69
+ const clean = (u) => (u || "").replace(/\/+$/, "");
70
+ // API origin for scalable search/lookup (optional). When set it wins.
71
+ const API_URL = clean(process.env.CLAUDEPACK_API_URL);
72
+ // Static catalog base (pure-static deployments / small catalogs).
73
+ const CATALOG_URL = clean(process.env.CLAUDEPACK_CATALOG_URL
74
+ || "https://raw.githubusercontent.com/plox-sumit/claudepack-catalog/main");
75
+ // Where item files live. In API mode they sit under <api>/catalog; else CATALOG_URL.
76
+ const FILES_BASE = API_URL ? `${API_URL}/catalog` : CATALOG_URL;
77
+
78
+ const claudeDir = () => process.env.CLAUDEPACK_CLAUDE_DIR || join(homedir(), ".claude");
79
+
80
+ const TARGETS = {
81
+ skill: { dir: "skills", layout: "folder", label: "skill" },
82
+ agent: { dir: "agents", layout: "flat", label: "subagent" },
83
+ command: { dir: "commands", layout: "flat", label: "command" },
84
+ style: { dir: "output-styles", layout: "flat", label: "output style" },
85
+ plugin: { dir: "plugins", layout: "folder", label: "plugin" },
86
+ };
87
+
88
+ const log = (...a) => process.stderr.write("[claudepack-mcp] " + a.join(" ") + "\n");
89
+
90
+ async function fetchText(url) {
91
+ const res = await fetch(url, { headers: { "user-agent": "claudepack-connector" } });
92
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
93
+ return res.text();
94
+ }
95
+
96
+ // Full catalog (static mode) with bundled fallback. Used when there's no API and
97
+ // for local ranking.
98
+ function rankLocal(items, query, limit = 10) {
99
+ const q = String(query || "").toLowerCase().trim();
100
+ if (!q) return items.slice(0, limit);
101
+ const terms = q.split(/\s+/).filter(Boolean);
102
+ return items
103
+ .map((it) => {
104
+ const hay = [it.id, it.title, it.summary, it.description, (it.tags || []).join(" "), it.type]
105
+ .filter(Boolean).join(" ").toLowerCase();
106
+ let score = 0;
107
+ for (const t of terms) if (hay.includes(t)) score++;
108
+ return { it, score };
109
+ })
110
+ .filter((s) => s.score > 0)
111
+ .sort((a, b) => b.score - a.score)
112
+ .slice(0, limit)
113
+ .map((s) => s.it);
114
+ }
115
+
116
+ async function loadFullCatalog() {
117
+ try {
118
+ return { items: JSON.parse(await fetchText(`${CATALOG_URL}/catalog.json`)), source: CATALOG_URL };
119
+ } catch (e) {
120
+ log("static catalog unavailable, using bundled snapshot:", e.message);
121
+ try {
122
+ return { items: JSON.parse(readFileSync(join(BUNDLED_DIR, "catalog.json"), "utf8")), source: "bundled" };
123
+ } catch {
124
+ return { items: [], source: "none" };
125
+ }
126
+ }
127
+ }
128
+
129
+ // search -> top matches. API mode transfers only results; otherwise full catalog.
130
+ async function doSearch(query, limit = 10) {
131
+ if (API_URL) {
132
+ try {
133
+ const url = `${API_URL}/api/search?q=${encodeURIComponent(query || "")}&limit=${limit}`;
134
+ return { items: JSON.parse(await fetchText(url)), source: `${API_URL}/api/search` };
135
+ } catch (e) {
136
+ log("api search failed, falling back to full catalog:", e.message);
137
+ }
138
+ }
139
+ const { items, source } = await loadFullCatalog();
140
+ return { items: rankLocal(items, query, limit), source };
141
+ }
142
+
143
+ // resolve one item's manifest. API mode = O(1); else scan full catalog.
144
+ async function resolveItem(id) {
145
+ if (API_URL) {
146
+ try {
147
+ const item = JSON.parse(await fetchText(`${API_URL}/api/item/${encodeURIComponent(id)}`));
148
+ if (item && item.id) return { item, mode: "remote" };
149
+ } catch (e) {
150
+ log("api item lookup failed, falling back:", e.message);
151
+ }
152
+ }
153
+ const { items, source } = await loadFullCatalog();
154
+ return { item: items.find((i) => i.id === id), mode: source === "bundled" ? "bundled" : "remote" };
155
+ }
156
+
157
+ async function getFile(mode, id, rel) {
158
+ assertSafeId(id);
159
+ const safeRel = assertSafeRel(rel);
160
+ if (mode !== "bundled") {
161
+ // Build the URL from validated, per-segment-encoded parts (keep slashes).
162
+ const encoded = safeRel.split("/").map(encodeURIComponent).join("/");
163
+ try { return await fetchText(`${FILES_BASE}/items/${encodeURIComponent(id)}/${encoded}`); }
164
+ catch (e) { log(`remote file miss (${id}/${safeRel}):`, e.message); }
165
+ }
166
+ const dest = assertWithin(join(BUNDLED_DIR, "items"), join(BUNDLED_DIR, "items", id, safeRel));
167
+ return readFileSync(dest, "utf8");
168
+ }
169
+
170
+ async function installItem(id) {
171
+ try { assertSafeId(id); }
172
+ catch { return { ok: false, message: `Invalid item id "${id}".` }; }
173
+
174
+ const { item, mode } = await resolveItem(id);
175
+ if (!item) return { ok: false, message: `No item "${id}" in the catalog. Run search_catalog first.` };
176
+ const target = TARGETS[item.type];
177
+ if (!target) return { ok: false, message: `Unsupported item type "${item.type}".` };
178
+ const files = Array.isArray(item.files) ? item.files.filter(Boolean) : [];
179
+ if (!files.length) return { ok: false, message: `Catalog entry "${id}" lists no files.` };
180
+
181
+ const base = join(claudeDir(), target.dir);
182
+ const folderRoot = join(base, id); // where folder-layout items live
183
+ const written = [];
184
+ try {
185
+ for (const rel of files) {
186
+ const safeRel = assertSafeRel(rel);
187
+ const content = await getFile(mode, id, safeRel);
188
+ const dest =
189
+ target.layout === "folder"
190
+ ? assertWithin(folderRoot, join(folderRoot, safeRel))
191
+ : assertWithin(base, join(base, basename(safeRel)));
192
+ await mkdir(dirname(dest), { recursive: true });
193
+ await writeFile(dest, content);
194
+ written.push(dest);
195
+ }
196
+ } catch (e) {
197
+ return { ok: false, message: `Refused to install "${id}": ${e.message}` };
198
+ }
199
+ const installedTo = target.layout === "folder" ? folderRoot : base;
200
+ return { ok: true, id, kind: target.label, version: item.version, mode, installedTo, files: written };
201
+ }
202
+
203
+ function listInstalled() {
204
+ const base = claudeDir();
205
+ const out = {};
206
+ for (const t of Object.values(TARGETS)) {
207
+ const dir = join(base, t.dir);
208
+ if (!existsSync(dir)) continue;
209
+ try {
210
+ const entries = readdirSync(dir).filter((n) => !n.startsWith("."));
211
+ if (entries.length) out[t.label] = entries;
212
+ } catch { /* ignore */ }
213
+ }
214
+ return out;
215
+ }
216
+
217
+ const TOOLS = [
218
+ {
219
+ name: "search_catalog",
220
+ description:
221
+ "Search ClaudePack for installable Claude extensions - skills, subagents, commands, output-styles, plugins - by a natural-language description or keywords. The catalog is fetched live, so newly published and updated items appear automatically. Use whenever the user asks Claude to find or add something from ClaudePack. Returns matches with id, type, title and summary.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ query: { type: "string", description: "A description of what you want, e.g. 'something that writes my git commit messages' or 'a subagent that writes tests'." },
226
+ limit: { type: "number", description: "Max results (default 10)." },
227
+ },
228
+ required: ["query"],
229
+ },
230
+ },
231
+ {
232
+ name: "install_item",
233
+ description:
234
+ "Install a ClaudePack item (skill / subagent / command / output-style / plugin) into the right ~/.claude/ folder. Always pulls the latest published version. Pass an id from search_catalog.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: { id: { type: "string", description: "The item id to install." } },
238
+ required: ["id"],
239
+ },
240
+ },
241
+ {
242
+ name: "list_installed",
243
+ description: "List what's already installed under ~/.claude/ (skills, subagents, commands, styles, plugins).",
244
+ inputSchema: { type: "object", properties: {} },
245
+ },
246
+ ];
247
+
248
+ async function callTool(name, args) {
249
+ if (name === "search_catalog") {
250
+ const { items, source } = await doSearch(args.query, args.limit);
251
+ const head = `(catalog source: ${source})`;
252
+ if (!items.length) return `${head}\nNothing in ClaudePack matched "${args.query}".`;
253
+ return head + "\n" + items.map((x) =>
254
+ `* [${x.type}] ${x.id}${x.version ? " v" + x.version : ""} - ${x.title}\n ${x.summary}\n tags: ${(x.tags || []).join(", ")}`).join("\n");
255
+ }
256
+ if (name === "install_item") {
257
+ const res = await installItem(args.id);
258
+ if (!res.ok) return res.message;
259
+ return `Installed ${res.kind} "${res.id}"${res.version ? " v" + res.version : ""} -> ${res.installedTo}\n` +
260
+ res.files.map((f) => " " + f).join("\n") +
261
+ `\n\nStart a new Claude Code session to load it.`;
262
+ }
263
+ if (name === "list_installed") {
264
+ const m = listInstalled();
265
+ const lines = Object.entries(m).map(([k, v]) => `${k}: ${v.join(", ")}`);
266
+ return lines.length ? lines.join("\n") : "Nothing installed yet.";
267
+ }
268
+ throw new Error("Unknown tool: " + name);
269
+ }
270
+
271
+ function send(msg) { process.stdout.write(JSON.stringify(msg) + "\n"); }
272
+
273
+ async function handle(req) {
274
+ const { id, method, params } = req;
275
+ if (method === "initialize") {
276
+ return { jsonrpc: "2.0", id, result: {
277
+ protocolVersion: params?.protocolVersion || PROTOCOL_FALLBACK,
278
+ capabilities: { tools: {} }, serverInfo: SERVER,
279
+ } };
280
+ }
281
+ if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
282
+ if (method === "tools/call") {
283
+ try {
284
+ const text = await callTool(params?.name, params?.arguments || {});
285
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text }] } };
286
+ } catch (e) {
287
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "Error: " + e.message }], isError: true } };
288
+ }
289
+ }
290
+ if (method === "ping") return { jsonrpc: "2.0", id, result: {} };
291
+ if (id === undefined || id === null) return null;
292
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found: " + method } };
293
+ }
294
+
295
+ createInterface({ input: process.stdin }).on("line", async (line) => {
296
+ const s = line.trim(); if (!s) return;
297
+ let req; try { req = JSON.parse(s); } catch { return; }
298
+ const res = await handle(req); if (res) send(res);
299
+ });
300
+
301
+ log("ready;", API_URL ? `api=${API_URL}` : `catalog=${CATALOG_URL}`, "; claude dir =", claudeDir());