claude-bridge-cli 1.0.0 → 1.1.0

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.
Files changed (3) hide show
  1. package/bin/cli.js +4 -0
  2. package/lib/bridge.js +337 -83
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -117,6 +117,10 @@ async function run(config) {
117
117
  // Start bridge HTTP server
118
118
  const bridge = await startBridge(config);
119
119
  console.log(`[bridge] Bridge ready on http://${config.host}:${config.port}`);
120
+ if (!config.relay) {
121
+ console.log(`[bridge] Bearer token: ${config.bearerToken}`);
122
+ console.log(`[bridge] Use this token when adding the endpoint in the extension.`);
123
+ }
120
124
 
121
125
  // Start relay client if configured
122
126
  if (config.relay) {
package/lib/bridge.js CHANGED
@@ -1,22 +1,24 @@
1
1
  "use strict";
2
2
 
3
3
  const http = require("node:http");
4
- const { spawn } = require("node:child_process");
4
+ const { spawn, execSync } = require("node:child_process");
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const crypto = require("node:crypto");
8
+ const os = require("node:os");
8
9
 
9
- // In-memory session processes (pid → {proc, sessionId})
10
10
  const running = new Map();
11
11
 
12
- // Data directory for marks, bindings, title overrides, starred
13
- function dataDir(config) {
14
- const d = path.join(config.cwd || process.cwd(), ".claude-bridge-data");
12
+ function homeDir() {
13
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
14
+ }
15
+
16
+ function dataDir() {
17
+ const d = path.join(homeDir(), ".claude-bridge");
15
18
  fs.mkdirSync(d, { recursive: true });
16
19
  return d;
17
20
  }
18
21
 
19
- // Simple JSON file helpers
20
22
  function readJson(filePath, def) {
21
23
  try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { return def; }
22
24
  }
@@ -26,97 +28,318 @@ function writeJson(filePath, data) {
26
28
  fs.renameSync(tmp, filePath);
27
29
  }
28
30
 
29
- // Claude projects dir (where JONSLs live)
30
31
  function projectsDir() {
31
- return path.join(process.env.HOME || process.env.USERPROFILE || "~", ".claude", "projects");
32
+ return path.join(homeDir(), ".claude", "projects");
33
+ }
34
+
35
+ // ── Image handling ──
36
+
37
+ const ALLOWED_IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp"]);
38
+ const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
39
+
40
+ function sanitizeFilename(name, fallbackExt = "png") {
41
+ let base = path.basename(name || "image").replace(/[^a-zA-Z0-9._-]/g, "_");
42
+ const ext = path.extname(base).slice(1).toLowerCase();
43
+ if (!ext || !ALLOWED_IMAGE_EXTS.has(ext)) base += "." + fallbackExt;
44
+ return base.slice(0, 200);
32
45
  }
33
46
 
34
- // List sessions by scanning JSONL files
35
- function listSessions(config, opts = {}) {
47
+ function saveImages(images, sessionId) {
48
+ if (!Array.isArray(images) || !images.length) return [];
49
+ const dir = path.join(dataDir(), "images", sessionId || "unsorted");
50
+ fs.mkdirSync(dir, { recursive: true });
51
+ const saved = [];
52
+ const ts = Date.now();
53
+ for (let i = 0; i < images.length; i++) {
54
+ const img = images[i];
55
+ if (!img || !img.data_base64) continue;
56
+ const buf = Buffer.from(img.data_base64, "base64");
57
+ if (buf.length > MAX_IMAGE_BYTES) continue;
58
+ const name = sanitizeFilename(img.name || `image-${i}.png`);
59
+ const filename = `${ts}-${i}-${name}`;
60
+ const filePath = path.join(dir, filename);
61
+ fs.writeFileSync(filePath, buf, { mode: 0o600 });
62
+ saved.push(filePath);
63
+ }
64
+ return saved;
65
+ }
66
+
67
+ function splitUserTextAndImages(text) {
68
+ const m = text.match(/\nThe user attached \d+ image\(s\) at these absolute paths\. Use the Read tool to view them:\n([\s\S]+)$/);
69
+ if (!m) return { cleanText: text, imagePaths: [] };
70
+ const cleanText = text.slice(0, m.index).trimEnd();
71
+ const paths = m[1].split("\n").map(l => l.replace(/^- /, "").trim()).filter(Boolean);
72
+ const imageData = [];
73
+ for (const p of paths) {
74
+ try {
75
+ const buf = fs.readFileSync(p);
76
+ const ext = path.extname(p).slice(1).toLowerCase();
77
+ const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg"
78
+ : ext === "gif" ? "image/gif"
79
+ : ext === "webp" ? "image/webp"
80
+ : "image/png";
81
+ imageData.push({ name: path.basename(p), data_base64: buf.toString("base64"), mime });
82
+ } catch {}
83
+ }
84
+ return { cleanText, imagePaths: paths, imageData };
85
+ }
86
+
87
+ // ── Session scanning ──
88
+
89
+ function scanSessionFiles() {
36
90
  const base = projectsDir();
37
91
  if (!fs.existsSync(base)) return [];
38
- const sessions = [];
39
- for (const project of fs.readdirSync(base)) {
92
+ const results = [];
93
+ let dirs;
94
+ try { dirs = fs.readdirSync(base); } catch { return []; }
95
+ for (const project of dirs) {
40
96
  const projDir = path.join(base, project);
41
- if (!fs.statSync(projDir).isDirectory()) continue;
42
- for (const file of fs.readdirSync(projDir)) {
97
+ let stat;
98
+ try { stat = fs.statSync(projDir); } catch { continue; }
99
+ if (!stat.isDirectory()) continue;
100
+ let files;
101
+ try { files = fs.readdirSync(projDir); } catch { continue; }
102
+ for (const file of files) {
43
103
  if (!file.endsWith(".jsonl")) continue;
44
104
  const id = file.replace(".jsonl", "");
45
105
  const filePath = path.join(projDir, file);
46
- const stat = fs.statSync(filePath);
47
- let preview = "", aiTitle = "", msgCount = 0, lastPrompt = "";
106
+ results.push({ id, project, filePath });
107
+ }
108
+ }
109
+ return results;
110
+ }
111
+
112
+ function looksLikeInternal(text) {
113
+ if (!text) return false;
114
+ const t = text.slice(0, 500);
115
+ return /^\s*\{/.test(t) && /"tool_use_id"|"tool_result"|"is_error"/.test(t);
116
+ }
117
+
118
+ function parseSessionFile(filePath) {
119
+ let preview = "", aiTitle = "", customTitle = "", msgCount = 0, lastPrompt = "";
120
+ try {
121
+ const lines = fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean);
122
+ msgCount = lines.length;
123
+ for (const line of lines) {
48
124
  try {
49
- const lines = fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean);
50
- msgCount = lines.length;
51
- for (const line of lines) {
52
- try {
53
- const obj = JSON.parse(line);
54
- if (obj.type === "summary" && obj.summary) { preview = preview || obj.summary.slice(0, 200); }
55
- if (obj.type === "user" && obj.message?.content) {
56
- const text = typeof obj.message.content === "string" ? obj.message.content : JSON.stringify(obj.message.content);
57
- if (!preview) preview = text.slice(0, 200);
58
- lastPrompt = text.slice(0, 200);
59
- }
60
- if (obj.type === "result" && obj.result) {
61
- aiTitle = aiTitle || (obj.result.metadata?.title?.value || "");
62
- }
63
- } catch {}
125
+ const obj = JSON.parse(line);
126
+ if (obj.type === "summary" && obj.summary) preview = preview || obj.summary.slice(0, 200);
127
+ if (obj.type === "user" && obj.message?.content) {
128
+ const text = typeof obj.message.content === "string" ? obj.message.content : JSON.stringify(obj.message.content);
129
+ if (!preview && !looksLikeInternal(text)) preview = text.slice(0, 200);
130
+ if (!looksLikeInternal(text)) lastPrompt = text.slice(0, 200);
131
+ }
132
+ if (obj.type === "result" && obj.result) {
133
+ aiTitle = aiTitle || (obj.result.metadata?.title?.value || "");
64
134
  }
135
+ if (obj.type === "ai-title" && obj.title) aiTitle = obj.title;
136
+ if (obj.type === "custom-title" && obj.title) customTitle = obj.title;
65
137
  } catch {}
66
- sessions.push({
67
- id, project,
68
- cwd: projDir,
69
- mtime: stat.mtimeMs / 1000,
70
- mtime_iso: stat.mtime.toISOString(),
71
- preview, ai_title: aiTitle,
72
- message_count: msgCount,
73
- last_prompt: lastPrompt,
74
- size_bytes: stat.size,
75
- });
76
138
  }
139
+ } catch {}
140
+ return { preview, ai_title: customTitle || aiTitle, message_count: msgCount, last_prompt: lastPrompt };
141
+ }
142
+
143
+ function projectDirToCwd(name) {
144
+ return name.replace(/-/g, "/");
145
+ }
146
+
147
+ function listSessions(opts = {}) {
148
+ const dd = dataDir();
149
+ const files = scanSessionFiles();
150
+ const titleOverrides = readJson(path.join(dd, "title-overrides.json"), {});
151
+ const starred = new Set(readJson(path.join(dd, "starred.json"), []));
152
+ const sessions = [];
153
+
154
+ for (const { id, project, filePath } of files) {
155
+ let stat;
156
+ try { stat = fs.statSync(filePath); } catch { continue; }
157
+ const parsed = parseSessionFile(filePath);
158
+ if (!opts.includeTiny && parsed.message_count < 3) continue;
159
+ if (opts.project && project !== opts.project) continue;
160
+ const title = titleOverrides[id] || parsed.ai_title;
161
+ sessions.push({
162
+ id, project,
163
+ cwd: projectDirToCwd(project),
164
+ mtime: stat.mtimeMs / 1000,
165
+ mtime_iso: stat.mtime.toISOString(),
166
+ preview: parsed.preview,
167
+ ai_title: title,
168
+ message_count: parsed.message_count,
169
+ last_prompt: parsed.last_prompt,
170
+ size_bytes: stat.size,
171
+ starred: starred.has(id),
172
+ in_progress: running.has(id),
173
+ });
77
174
  }
78
- sessions.sort((a, b) => b.mtime - a.mtime);
79
175
 
80
- // Add starred flag
81
- const starred = new Set(readJson(path.join(dataDir(config), "starred.json"), []));
82
- for (const s of sessions) s.starred = starred.has(s.id);
83
176
  sessions.sort((a, b) => (b.starred ? 1 : 0) - (a.starred ? 1 : 0) || b.mtime - a.mtime);
84
-
85
- return sessions;
177
+ return sessions.slice(0, opts.limit || 400);
86
178
  }
87
179
 
88
- // Get session messages
89
- function getSessionMessages(sessionId) {
90
- const base = projectsDir();
91
- for (const project of fs.readdirSync(base)) {
92
- const filePath = path.join(base, project, sessionId + ".jsonl");
93
- if (fs.existsSync(filePath)) {
94
- const messages = [];
95
- for (const line of fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean)) {
180
+ // ── Search ──
181
+
182
+ function searchSessions(query, limit = 30) {
183
+ if (!query || query.length < 2) return [];
184
+ const q = query.toLowerCase();
185
+ const dd = dataDir();
186
+ const titleOverrides = readJson(path.join(dd, "title-overrides.json"), {});
187
+ const starred = new Set(readJson(path.join(dd, "starred.json"), []));
188
+ const files = scanSessionFiles();
189
+
190
+ files.sort((a, b) => {
191
+ try {
192
+ return fs.statSync(b.filePath).mtimeMs - fs.statSync(a.filePath).mtimeMs;
193
+ } catch { return 0; }
194
+ });
195
+
196
+ const results = [];
197
+ for (const { id, project, filePath } of files.slice(0, 200)) {
198
+ let stat;
199
+ try { stat = fs.statSync(filePath); } catch { continue; }
200
+ let content;
201
+ try { content = fs.readFileSync(filePath, "utf8"); } catch { continue; }
202
+
203
+ const lower = content.toLowerCase();
204
+ const idx = lower.indexOf(q);
205
+ if (idx < 0) continue;
206
+
207
+ const matchCount = lower.split(q).length - 1;
208
+ const snippetStart = Math.max(0, idx - 40);
209
+ const snippetEnd = Math.min(content.length, idx + q.length + 80);
210
+ const snippet = content.slice(snippetStart, snippetEnd).replace(/\n/g, " ").trim();
211
+
212
+ let aiTitle = "", msgCount = 0;
213
+ try {
214
+ const lines = content.split("\n").filter(Boolean);
215
+ msgCount = lines.length;
216
+ for (const line of lines) {
96
217
  try {
97
218
  const obj = JSON.parse(line);
98
- if (obj.type === "user" && obj.message?.content) {
99
- messages.push({ role: "user", text: typeof obj.message.content === "string" ? obj.message.content : JSON.stringify(obj.message.content), timestamp: obj.timestamp });
100
- } else if (obj.type === "assistant" && obj.message?.content) {
101
- const parts = Array.isArray(obj.message.content) ? obj.message.content : [obj.message.content];
102
- const text = parts.map(p => typeof p === "string" ? p : p.text || "").join("");
103
- if (text) messages.push({ role: "assistant", text, timestamp: obj.timestamp });
104
- } else if (obj.type === "result" && obj.result?.assistantMessage) {
105
- const text = typeof obj.result.assistantMessage === "string" ? obj.result.assistantMessage : "";
106
- if (text) messages.push({ role: "assistant", text, timestamp: obj.timestamp });
219
+ if (obj.type === "result" && obj.result?.metadata?.title?.value) {
220
+ aiTitle = obj.result.metadata.title.value;
107
221
  }
222
+ if (obj.type === "ai-title" && obj.title) aiTitle = obj.title;
223
+ if (obj.type === "custom-title" && obj.title) aiTitle = obj.title;
108
224
  } catch {}
109
225
  }
110
- return { session_id: sessionId, messages };
226
+ } catch {}
227
+
228
+ results.push({
229
+ id, project,
230
+ cwd: projectDirToCwd(project),
231
+ mtime: stat.mtimeMs / 1000,
232
+ mtime_iso: stat.mtime.toISOString(),
233
+ ai_title: titleOverrides[id] || aiTitle,
234
+ snippet,
235
+ match_count: matchCount,
236
+ message_count: msgCount,
237
+ starred: starred.has(id),
238
+ });
239
+ if (results.length >= limit) break;
240
+ }
241
+ return results;
242
+ }
243
+
244
+ // ── Session messages ──
245
+
246
+ function getSessionMessages(sessionId) {
247
+ const base = projectsDir();
248
+ let dirs;
249
+ try { dirs = fs.readdirSync(base); } catch { return { error: "session not found" }; }
250
+ for (const project of dirs) {
251
+ const filePath = path.join(base, project, sessionId + ".jsonl");
252
+ if (!fs.existsSync(filePath)) continue;
253
+ const messages = [];
254
+ for (const line of fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean)) {
255
+ try {
256
+ const obj = JSON.parse(line);
257
+ if (obj.type === "user" && obj.message?.content) {
258
+ let text = typeof obj.message.content === "string" ? obj.message.content : JSON.stringify(obj.message.content);
259
+ const split = splitUserTextAndImages(text);
260
+ messages.push({
261
+ role: "user",
262
+ text: split.cleanText,
263
+ images: split.imageData || [],
264
+ timestamp: obj.timestamp,
265
+ });
266
+ } else if (obj.type === "assistant" && obj.message?.content) {
267
+ const parts = Array.isArray(obj.message.content) ? obj.message.content : [obj.message.content];
268
+ const text = parts.map(p => typeof p === "string" ? p : p.text || "").join("");
269
+ if (text) messages.push({ role: "assistant", text, timestamp: obj.timestamp });
270
+ } else if (obj.type === "result" && obj.result?.assistantMessage) {
271
+ const text = typeof obj.result.assistantMessage === "string" ? obj.result.assistantMessage : "";
272
+ if (text) messages.push({ role: "assistant", text, timestamp: obj.timestamp });
273
+ }
274
+ } catch {}
111
275
  }
276
+ return { session_id: sessionId, messages, in_progress: running.has(sessionId) };
112
277
  }
113
278
  return { error: "session not found" };
114
279
  }
115
280
 
116
- // Run claude -p
281
+ // ── Session delete / wipe ──
282
+
283
+ function deleteSession(sessionId) {
284
+ const base = projectsDir();
285
+ let dirs;
286
+ try { dirs = fs.readdirSync(base); } catch { return { error: "not found" }; }
287
+ for (const project of dirs) {
288
+ const filePath = path.join(base, project, sessionId + ".jsonl");
289
+ if (!fs.existsSync(filePath)) continue;
290
+ fs.unlinkSync(filePath);
291
+ const imgDir = path.join(dataDir(), "images", sessionId);
292
+ try { fs.rmSync(imgDir, { recursive: true, force: true }); } catch {}
293
+ return { deleted: true, session_id: sessionId };
294
+ }
295
+ return { error: "not found" };
296
+ }
297
+
298
+ function wipeAllSessions() {
299
+ const base = projectsDir();
300
+ let removed = 0;
301
+ try {
302
+ for (const project of fs.readdirSync(base)) {
303
+ const projDir = path.join(base, project);
304
+ if (!fs.statSync(projDir).isDirectory()) continue;
305
+ for (const file of fs.readdirSync(projDir)) {
306
+ if (!file.endsWith(".jsonl")) continue;
307
+ try { fs.unlinkSync(path.join(projDir, file)); removed++; } catch {}
308
+ }
309
+ }
310
+ } catch {}
311
+ const dd = dataDir();
312
+ for (const f of ["marks.json", "bindings.json", "title-overrides.json", "starred.json"]) {
313
+ try { fs.unlinkSync(path.join(dd, f)); } catch {}
314
+ }
315
+ try { fs.rmSync(path.join(dd, "images"), { recursive: true, force: true }); } catch {}
316
+ return { removed_jsonls: removed };
317
+ }
318
+
319
+ // ── Session rename ──
320
+
321
+ function renameSession(sessionId, title) {
322
+ const dd = dataDir();
323
+ const overrides = readJson(path.join(dd, "title-overrides.json"), {});
324
+ overrides[sessionId] = (title || "").slice(0, 500);
325
+ writeJson(path.join(dd, "title-overrides.json"), overrides);
326
+ return { ok: true, title: overrides[sessionId] };
327
+ }
328
+
329
+ // ── Run claude -p ──
330
+
117
331
  function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_tools }) {
118
332
  return new Promise((resolve) => {
119
- const args = ["-p", prompt, "--output-format", "json"];
333
+ let finalPrompt = prompt;
334
+ if (images && images.length) {
335
+ const saved = saveImages(images, session_id || "pending");
336
+ if (saved.length) {
337
+ finalPrompt += `\nThe user attached ${saved.length} image(s) at these absolute paths. Use the Read tool to view them:\n` +
338
+ saved.map(p => `- ${p}`).join("\n");
339
+ }
340
+ }
341
+
342
+ const args = ["-p", finalPrompt, "--output-format", "json"];
120
343
  if (session_id) args.push("--session-id", session_id);
121
344
  if (plan_mode) args.push("--plan");
122
345
 
@@ -138,9 +361,15 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
138
361
  if (session_id) running.delete(session_id);
139
362
  try {
140
363
  const result = JSON.parse(stdout);
364
+ const sid = result.session_id || session_id || crypto.randomUUID();
365
+ if (images && images.length && session_id === "pending" && sid !== "pending") {
366
+ const oldDir = path.join(dataDir(), "images", "pending");
367
+ const newDir = path.join(dataDir(), "images", sid);
368
+ try { fs.renameSync(oldDir, newDir); } catch {}
369
+ }
141
370
  resolve({
142
371
  response: result.result || result.assistantMessage || stdout.slice(0, 5000),
143
- session_id: result.session_id || session_id || crypto.randomUUID(),
372
+ session_id: sid,
144
373
  cost_usd: result.cost_usd || null,
145
374
  duration_ms: result.duration_ms || null,
146
375
  context: result.context || null,
@@ -161,21 +390,23 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
161
390
  });
162
391
  }
163
392
 
164
- // Stop a running session
393
+ // ── Stop session ──
394
+
165
395
  function stopSession(sessionId) {
166
396
  const proc = running.get(sessionId);
167
397
  if (!proc) return { stopped: false, reason: "not_running" };
168
398
  try { proc.kill("SIGINT"); } catch {}
169
399
  setTimeout(() => { try { proc.kill("SIGTERM"); } catch {} }, 3000);
400
+ running.delete(sessionId);
170
401
  return { stopped: true };
171
402
  }
172
403
 
173
- // HTTP server
404
+ // ── HTTP server ──
405
+
174
406
  function startBridge(config) {
175
- const dd = dataDir(config);
407
+ const dd = dataDir();
176
408
 
177
409
  const server = http.createServer(async (req, res) => {
178
- // CORS
179
410
  res.setHeader("Access-Control-Allow-Origin", "*");
180
411
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
181
412
  res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Confirm-Wipe");
@@ -186,7 +417,6 @@ function startBridge(config) {
186
417
  res.end(JSON.stringify(obj));
187
418
  };
188
419
 
189
- // Auth
190
420
  const auth = req.headers.authorization || "";
191
421
  if (req.url !== "/health" && auth !== `Bearer ${config.bearerToken}`) {
192
422
  send(401, { error: "unauthorized" });
@@ -194,6 +424,7 @@ function startBridge(config) {
194
424
  }
195
425
 
196
426
  const url = new URL(req.url, `http://${req.headers.host}`);
427
+ let m;
197
428
 
198
429
  // Health
199
430
  if (url.pathname === "/health") {
@@ -211,9 +442,18 @@ function startBridge(config) {
211
442
  // Sessions list
212
443
  if (url.pathname === "/sessions" && req.method === "GET") {
213
444
  const includeTiny = url.searchParams.get("include_tiny") === "1";
214
- let sessions = listSessions(config);
215
- if (!includeTiny) sessions = sessions.filter(s => s.message_count >= 3);
216
- send(200, { sessions: sessions.slice(0, 120) });
445
+ const project = url.searchParams.get("project") || "";
446
+ const sessions = listSessions({ includeTiny, project });
447
+ send(200, { sessions });
448
+ return;
449
+ }
450
+
451
+ // Search
452
+ if (url.pathname === "/sessions/search" && req.method === "GET") {
453
+ const q = url.searchParams.get("q") || "";
454
+ const limit = parseInt(url.searchParams.get("limit") || "30", 10);
455
+ const results = searchSessions(q, limit);
456
+ send(200, { results });
217
457
  return;
218
458
  }
219
459
 
@@ -223,8 +463,18 @@ function startBridge(config) {
223
463
  return;
224
464
  }
225
465
 
466
+ // Wipe all sessions
467
+ if (url.pathname === "/sessions/all" && req.method === "DELETE") {
468
+ if (req.headers["x-confirm-wipe"] !== "yes-i-am-sure") {
469
+ send(400, { error: "missing X-Confirm-Wipe header" });
470
+ return;
471
+ }
472
+ send(200, wipeAllSessions());
473
+ return;
474
+ }
475
+
226
476
  // Session messages
227
- let m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/messages$/);
477
+ m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/messages$/);
228
478
  if (m && req.method === "GET") {
229
479
  const result = getSessionMessages(m[1]);
230
480
  send(result.error ? 404 : 200, result);
@@ -275,14 +525,19 @@ function startBridge(config) {
275
525
  m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/title$/);
276
526
  if (m && (req.method === "PATCH" || req.method === "POST")) {
277
527
  const body = await readBody(req);
278
- const overrides = readJson(path.join(dd, "title-overrides.json"), {});
279
- overrides[m[1]] = body.title || "";
280
- writeJson(path.join(dd, "title-overrides.json"), overrides);
281
- send(200, { ok: true, title: body.title });
528
+ send(200, renameSession(m[1], body.title));
529
+ return;
530
+ }
531
+
532
+ // Delete single session
533
+ m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)$/);
534
+ if (m && req.method === "DELETE") {
535
+ const result = deleteSession(m[1]);
536
+ send(result.error ? 404 : 200, result);
282
537
  return;
283
538
  }
284
539
 
285
- // Bindings
540
+ // ChatGPT bindings
286
541
  if (url.pathname === "/chatgpt-bindings") {
287
542
  const bindingsFile = path.join(dd, "bindings.json");
288
543
  if (req.method === "GET") {
@@ -310,7 +565,6 @@ function startBridge(config) {
310
565
  });
311
566
  }
312
567
 
313
- // Read JSON body
314
568
  function readBody(req) {
315
569
  return new Promise((resolve) => {
316
570
  let data = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-bridge-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Use Claude Code from your browser. Runs a local server that connects your browser tools to the Claude CLI.",
5
5
  "main": "lib/bridge.js",
6
6
  "bin": {