claude-bridge-cli 2.0.23 → 2.0.25
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/lib/bridge.js +134 -2
- package/package.json +1 -1
package/lib/bridge.js
CHANGED
|
@@ -64,6 +64,16 @@ function dataDir() {
|
|
|
64
64
|
return d;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
let mcpHealth = { at: 0, data: {} }; // cached `claude mcp list` health
|
|
68
|
+
|
|
69
|
+
// Where always-on personal skills live for THIS machine (a plugin dir, since
|
|
70
|
+
// ~/.claude/skills is not discovered by the CLI). Shared by the turn builder
|
|
71
|
+
// (--plugin-dir) and the /skills endpoint so they can never disagree.
|
|
72
|
+
function globalSkillsDir() {
|
|
73
|
+
return process.env.CLAUDE_BRIDGE_GLOBAL_SKILLS_DIR ||
|
|
74
|
+
path.join(dataDir(), "global-skills");
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
function readJson(filePath, def) {
|
|
68
78
|
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { return def; }
|
|
69
79
|
}
|
|
@@ -890,8 +900,7 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
|
|
|
890
900
|
// cwd. So personal always-on skills live in <data>/global-skills as a plugin
|
|
891
901
|
// and are attached to every turn — global instead of per-project.
|
|
892
902
|
try {
|
|
893
|
-
const gsDir =
|
|
894
|
-
path.join(dataDir(), "global-skills");
|
|
903
|
+
const gsDir = globalSkillsDir();
|
|
895
904
|
if (fs.existsSync(path.join(gsDir, "skills"))) args.push("--plugin-dir", gsDir);
|
|
896
905
|
} catch {}
|
|
897
906
|
// Optional model override from the extension's model picker (alias or id).
|
|
@@ -1200,6 +1209,129 @@ function startBridge(config) {
|
|
|
1200
1209
|
}
|
|
1201
1210
|
|
|
1202
1211
|
// ChatGPT bindings
|
|
1212
|
+
// ── /skills — global-skills CRUD, per machine ────────────────────────────
|
|
1213
|
+
// The extension's "/" autocomplete and Skills tab call this on whichever
|
|
1214
|
+
// machine is active. Without it the request 404s, the host list comes back
|
|
1215
|
+
// empty, and the menu silently shows ONLY the CLI built-ins — which reads
|
|
1216
|
+
// as "this machine has no skills". Mirrors the Python bridge's contract.
|
|
1217
|
+
// ── /mcp — configured MCP servers for THIS machine ───────────────────────
|
|
1218
|
+
// Config read is instant; ?health=1 shells out to `claude mcp list`, which
|
|
1219
|
+
// actually connects to each server (seconds), so it's cached — but it's the
|
|
1220
|
+
// only way to distinguish Connected from needs-authentication.
|
|
1221
|
+
if (url.pathname === "/mcp") {
|
|
1222
|
+
const servers = {};
|
|
1223
|
+
const bases = new Set([process.env.CLAUDE_CONFIG_DIR || homeDir(), homeDir()]);
|
|
1224
|
+
for (const base of bases) {
|
|
1225
|
+
let cfg; try { cfg = JSON.parse(fs.readFileSync(path.join(base, ".claude.json"), "utf8")); }
|
|
1226
|
+
catch { continue; }
|
|
1227
|
+
for (const [n, spec] of Object.entries(cfg.mcpServers || {}))
|
|
1228
|
+
if (!(n in servers)) servers[n] = spec || {};
|
|
1229
|
+
for (const proj of Object.values(cfg.projects || {}))
|
|
1230
|
+
for (const [n, spec] of Object.entries((proj && proj.mcpServers) || {}))
|
|
1231
|
+
if (!(n in servers)) servers[n] = spec || {};
|
|
1232
|
+
}
|
|
1233
|
+
const out = Object.keys(servers).sort().map((n) => ({
|
|
1234
|
+
name: n,
|
|
1235
|
+
transport: servers[n].type || (servers[n].url ? "http" : "stdio"),
|
|
1236
|
+
target: servers[n].url || servers[n].command || "",
|
|
1237
|
+
}));
|
|
1238
|
+
const want = url.searchParams.get("health");
|
|
1239
|
+
if (out.length && want && want !== "0" && want !== "false") {
|
|
1240
|
+
const now = Date.now();
|
|
1241
|
+
if (now - mcpHealth.at > 120000) {
|
|
1242
|
+
const states = {};
|
|
1243
|
+
try {
|
|
1244
|
+
const p = execSync(`${JSON.stringify(config.claudeBin)} mcp list`,
|
|
1245
|
+
{ encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"] });
|
|
1246
|
+
for (const line of String(p).split("\n")) {
|
|
1247
|
+
const m = /^\s*([A-Za-z0-9._-]+):\s*(.*?)\s*-\s*(.+?)\s*$/.exec(line);
|
|
1248
|
+
if (m) states[m[1]] = m[3].replace(/[✔⚠○⏸]/g, "").trim();
|
|
1249
|
+
}
|
|
1250
|
+
} catch {}
|
|
1251
|
+
mcpHealth = { at: now, data: states };
|
|
1252
|
+
}
|
|
1253
|
+
for (const s of out) s.state = mcpHealth.data[s.name];
|
|
1254
|
+
}
|
|
1255
|
+
send(200, { servers: out });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (url.pathname === "/skills" || url.pathname.startsWith("/skills/")) {
|
|
1260
|
+
const skillsRoot = path.join(globalSkillsDir(), "skills");
|
|
1261
|
+
const nameOf = decodeURIComponent(url.pathname.slice("/skills/".length) || "");
|
|
1262
|
+
const validName = (n) => /^[a-z0-9][a-z0-9-]{0,63}$/.test(n);
|
|
1263
|
+
const fileOf = (n) => path.join(skillsRoot, n, "SKILL.md");
|
|
1264
|
+
|
|
1265
|
+
if (req.method === "GET" && url.pathname === "/skills") {
|
|
1266
|
+
const out = [];
|
|
1267
|
+
let entries = [];
|
|
1268
|
+
try { entries = fs.readdirSync(skillsRoot); } catch {}
|
|
1269
|
+
for (const n of entries.sort()) {
|
|
1270
|
+
const f = fileOf(n);
|
|
1271
|
+
let md; try { md = fs.readFileSync(f, "utf8"); } catch { continue; }
|
|
1272
|
+
const fm = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(md);
|
|
1273
|
+
const desc = fm ? (/^description:\s*(.+)$/m.exec(fm[1]) || [])[1] : "";
|
|
1274
|
+
let readonly = false;
|
|
1275
|
+
try { readonly = fs.lstatSync(path.join(skillsRoot, n)).isSymbolicLink(); } catch {}
|
|
1276
|
+
out.push({ name: n, description: (desc || "").trim().replace(/^["']|["']$/g, ""),
|
|
1277
|
+
size: md.length, readonly });
|
|
1278
|
+
}
|
|
1279
|
+
send(200, { skills: out, dir: skillsRoot });
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (!validName(nameOf)) { send(404, { error: "no such skill" }); return; }
|
|
1283
|
+
if (req.method === "GET") {
|
|
1284
|
+
try {
|
|
1285
|
+
const md = fs.readFileSync(fileOf(nameOf), "utf8");
|
|
1286
|
+
let readonly = false;
|
|
1287
|
+
try { readonly = fs.lstatSync(path.join(skillsRoot, nameOf)).isSymbolicLink(); } catch {}
|
|
1288
|
+
send(200, { name: nameOf, content: md, readonly });
|
|
1289
|
+
} catch { send(404, { error: "no such skill: " + nameOf }); }
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (req.method === "POST") {
|
|
1293
|
+
const body = await readBody(req);
|
|
1294
|
+
const content = body && body.content;
|
|
1295
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
1296
|
+
send(400, { error: "content is required" }); return;
|
|
1297
|
+
}
|
|
1298
|
+
const fm = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(content);
|
|
1299
|
+
if (!fm || !/^description:\s*\S/m.test(fm[1])) {
|
|
1300
|
+
send(400, { error: "SKILL.md must start with '---' frontmatter containing "
|
|
1301
|
+
+ "name: and description: lines — description is what "
|
|
1302
|
+
+ "makes Claude auto-select the skill" });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const dir = path.join(skillsRoot, nameOf);
|
|
1306
|
+
try {
|
|
1307
|
+
if (fs.existsSync(dir) && fs.lstatSync(dir).isSymbolicLink()) {
|
|
1308
|
+
send(400, { error: `'${nameOf}' is repo-owned (symlink) — edit it in its repo` });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1312
|
+
const tmp = path.join(dir, ".SKILL.md.tmp");
|
|
1313
|
+
fs.writeFileSync(tmp, content);
|
|
1314
|
+
fs.renameSync(tmp, fileOf(nameOf));
|
|
1315
|
+
send(200, { ok: true, name: nameOf });
|
|
1316
|
+
} catch (e) { send(500, { error: "write failed: " + e.message }); }
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (req.method === "DELETE") {
|
|
1320
|
+
const dir = path.join(skillsRoot, nameOf);
|
|
1321
|
+
try {
|
|
1322
|
+
if (fs.lstatSync(dir).isSymbolicLink()) {
|
|
1323
|
+
fs.unlinkSync(dir); // detach only; repo copy survives
|
|
1324
|
+
send(200, { ok: true, name: nameOf, detached_symlink: true }); return;
|
|
1325
|
+
}
|
|
1326
|
+
fs.rmSync(dir, { recursive: true });
|
|
1327
|
+
send(200, { ok: true, name: nameOf });
|
|
1328
|
+
} catch { send(404, { error: "no such skill: " + nameOf }); }
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
send(405, { error: "method not allowed" });
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1203
1335
|
if (url.pathname === "/chatgpt-bindings") {
|
|
1204
1336
|
const bindingsFile = path.join(dd, "bindings.json");
|
|
1205
1337
|
if (req.method === "GET") {
|
package/package.json
CHANGED