claude-bridge-cli 2.0.22 → 2.0.24
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 +119 -5
- package/package.json +1 -1
package/lib/bridge.js
CHANGED
|
@@ -64,6 +64,14 @@ function dataDir() {
|
|
|
64
64
|
return d;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Where always-on personal skills live for THIS machine (a plugin dir, since
|
|
68
|
+
// ~/.claude/skills is not discovered by the CLI). Shared by the turn builder
|
|
69
|
+
// (--plugin-dir) and the /skills endpoint so they can never disagree.
|
|
70
|
+
function globalSkillsDir() {
|
|
71
|
+
return process.env.CLAUDE_BRIDGE_GLOBAL_SKILLS_DIR ||
|
|
72
|
+
path.join(dataDir(), "global-skills");
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
function readJson(filePath, def) {
|
|
68
76
|
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { return def; }
|
|
69
77
|
}
|
|
@@ -427,8 +435,13 @@ function searchSessions(query, limit = 30) {
|
|
|
427
435
|
} catch { return 0; }
|
|
428
436
|
});
|
|
429
437
|
|
|
438
|
+
// Scan far more than the old 200 newest — on a busy machine that cut off
|
|
439
|
+
// sessions only a few days old, so a term you KNEW was there returned
|
|
440
|
+
// nothing. The loop already stops once `limit` results are collected, and
|
|
441
|
+
// the whole-file prefilter below makes a non-matching session cheap.
|
|
430
442
|
const results = [];
|
|
431
|
-
for (const { id, project, filePath } of files.slice(0,
|
|
443
|
+
for (const { id, project, filePath } of files.slice(0, 2000)) {
|
|
444
|
+
if (results.length >= limit) break;
|
|
432
445
|
let stat;
|
|
433
446
|
try { stat = fs.statSync(filePath); } catch { continue; }
|
|
434
447
|
let content;
|
|
@@ -450,9 +463,30 @@ function searchSessions(query, limit = 30) {
|
|
|
450
463
|
let rec;
|
|
451
464
|
try { rec = JSON.parse(line); } catch { continue; }
|
|
452
465
|
const t = rec.type;
|
|
453
|
-
if (t !== "user" && t !== "assistant") continue;
|
|
454
|
-
const msg = rec.message || {};
|
|
455
466
|
const texts = [];
|
|
467
|
+
// Text also lives OUTSIDE message.content: a queued message you typed,
|
|
468
|
+
// the recorded last prompt, compaction summaries, system notes. Parsing
|
|
469
|
+
// only user/assistant records made those unfindable — the old whole-file
|
|
470
|
+
// grep did match them, so skipping them was a regression.
|
|
471
|
+
if (t !== "user" && t !== "assistant") {
|
|
472
|
+
for (const k of ["summary", "lastPrompt", "content", "text"]) {
|
|
473
|
+
if (typeof rec[k] === "string" && rec[k]) texts.push(rec[k]);
|
|
474
|
+
}
|
|
475
|
+
for (const text of texts) {
|
|
476
|
+
const tl = text.toLowerCase();
|
|
477
|
+
let from = tl.indexOf(q);
|
|
478
|
+
if (from < 0) continue;
|
|
479
|
+
while (from >= 0) { matchCount++; from = tl.indexOf(q, from + q.length); }
|
|
480
|
+
if (!snippet) {
|
|
481
|
+
const i = tl.indexOf(q);
|
|
482
|
+
const s = Math.max(0, i - 40), e = Math.min(text.length, i + q.length + 80);
|
|
483
|
+
snippet = (s > 0 ? "…" : "") + text.slice(s, e).replace(/\s+/g, " ").trim()
|
|
484
|
+
+ (e < text.length ? "…" : "");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const msg = rec.message || {};
|
|
456
490
|
const c = msg.content;
|
|
457
491
|
if (typeof c === "string") texts.push(c);
|
|
458
492
|
else if (Array.isArray(c)) {
|
|
@@ -864,8 +898,7 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
|
|
|
864
898
|
// cwd. So personal always-on skills live in <data>/global-skills as a plugin
|
|
865
899
|
// and are attached to every turn — global instead of per-project.
|
|
866
900
|
try {
|
|
867
|
-
const gsDir =
|
|
868
|
-
path.join(dataDir(), "global-skills");
|
|
901
|
+
const gsDir = globalSkillsDir();
|
|
869
902
|
if (fs.existsSync(path.join(gsDir, "skills"))) args.push("--plugin-dir", gsDir);
|
|
870
903
|
} catch {}
|
|
871
904
|
// Optional model override from the extension's model picker (alias or id).
|
|
@@ -1174,6 +1207,87 @@ function startBridge(config) {
|
|
|
1174
1207
|
}
|
|
1175
1208
|
|
|
1176
1209
|
// ChatGPT bindings
|
|
1210
|
+
// ── /skills — global-skills CRUD, per machine ────────────────────────────
|
|
1211
|
+
// The extension's "/" autocomplete and Skills tab call this on whichever
|
|
1212
|
+
// machine is active. Without it the request 404s, the host list comes back
|
|
1213
|
+
// empty, and the menu silently shows ONLY the CLI built-ins — which reads
|
|
1214
|
+
// as "this machine has no skills". Mirrors the Python bridge's contract.
|
|
1215
|
+
if (url.pathname === "/skills" || url.pathname.startsWith("/skills/")) {
|
|
1216
|
+
const skillsRoot = path.join(globalSkillsDir(), "skills");
|
|
1217
|
+
const nameOf = decodeURIComponent(url.pathname.slice("/skills/".length) || "");
|
|
1218
|
+
const validName = (n) => /^[a-z0-9][a-z0-9-]{0,63}$/.test(n);
|
|
1219
|
+
const fileOf = (n) => path.join(skillsRoot, n, "SKILL.md");
|
|
1220
|
+
|
|
1221
|
+
if (req.method === "GET" && url.pathname === "/skills") {
|
|
1222
|
+
const out = [];
|
|
1223
|
+
let entries = [];
|
|
1224
|
+
try { entries = fs.readdirSync(skillsRoot); } catch {}
|
|
1225
|
+
for (const n of entries.sort()) {
|
|
1226
|
+
const f = fileOf(n);
|
|
1227
|
+
let md; try { md = fs.readFileSync(f, "utf8"); } catch { continue; }
|
|
1228
|
+
const fm = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(md);
|
|
1229
|
+
const desc = fm ? (/^description:\s*(.+)$/m.exec(fm[1]) || [])[1] : "";
|
|
1230
|
+
let readonly = false;
|
|
1231
|
+
try { readonly = fs.lstatSync(path.join(skillsRoot, n)).isSymbolicLink(); } catch {}
|
|
1232
|
+
out.push({ name: n, description: (desc || "").trim().replace(/^["']|["']$/g, ""),
|
|
1233
|
+
size: md.length, readonly });
|
|
1234
|
+
}
|
|
1235
|
+
send(200, { skills: out, dir: skillsRoot });
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (!validName(nameOf)) { send(404, { error: "no such skill" }); return; }
|
|
1239
|
+
if (req.method === "GET") {
|
|
1240
|
+
try {
|
|
1241
|
+
const md = fs.readFileSync(fileOf(nameOf), "utf8");
|
|
1242
|
+
let readonly = false;
|
|
1243
|
+
try { readonly = fs.lstatSync(path.join(skillsRoot, nameOf)).isSymbolicLink(); } catch {}
|
|
1244
|
+
send(200, { name: nameOf, content: md, readonly });
|
|
1245
|
+
} catch { send(404, { error: "no such skill: " + nameOf }); }
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (req.method === "POST") {
|
|
1249
|
+
const body = await readBody(req);
|
|
1250
|
+
const content = body && body.content;
|
|
1251
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
1252
|
+
send(400, { error: "content is required" }); return;
|
|
1253
|
+
}
|
|
1254
|
+
const fm = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(content);
|
|
1255
|
+
if (!fm || !/^description:\s*\S/m.test(fm[1])) {
|
|
1256
|
+
send(400, { error: "SKILL.md must start with '---' frontmatter containing "
|
|
1257
|
+
+ "name: and description: lines — description is what "
|
|
1258
|
+
+ "makes Claude auto-select the skill" });
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const dir = path.join(skillsRoot, nameOf);
|
|
1262
|
+
try {
|
|
1263
|
+
if (fs.existsSync(dir) && fs.lstatSync(dir).isSymbolicLink()) {
|
|
1264
|
+
send(400, { error: `'${nameOf}' is repo-owned (symlink) — edit it in its repo` });
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1268
|
+
const tmp = path.join(dir, ".SKILL.md.tmp");
|
|
1269
|
+
fs.writeFileSync(tmp, content);
|
|
1270
|
+
fs.renameSync(tmp, fileOf(nameOf));
|
|
1271
|
+
send(200, { ok: true, name: nameOf });
|
|
1272
|
+
} catch (e) { send(500, { error: "write failed: " + e.message }); }
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (req.method === "DELETE") {
|
|
1276
|
+
const dir = path.join(skillsRoot, nameOf);
|
|
1277
|
+
try {
|
|
1278
|
+
if (fs.lstatSync(dir).isSymbolicLink()) {
|
|
1279
|
+
fs.unlinkSync(dir); // detach only; repo copy survives
|
|
1280
|
+
send(200, { ok: true, name: nameOf, detached_symlink: true }); return;
|
|
1281
|
+
}
|
|
1282
|
+
fs.rmSync(dir, { recursive: true });
|
|
1283
|
+
send(200, { ok: true, name: nameOf });
|
|
1284
|
+
} catch { send(404, { error: "no such skill: " + nameOf }); }
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
send(405, { error: "method not allowed" });
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1177
1291
|
if (url.pathname === "/chatgpt-bindings") {
|
|
1178
1292
|
const bindingsFile = path.join(dd, "bindings.json");
|
|
1179
1293
|
if (req.method === "GET") {
|
package/package.json
CHANGED