claude-bridge-cli 1.0.1 → 1.1.2

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 +7 -3
  2. package/lib/bridge.js +340 -84
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -17,6 +17,7 @@ Usage:
17
17
  Options:
18
18
  --port <n> HTTP port for the local bridge (default: 8091)
19
19
  --host <ip> Bind address (default: 127.0.0.1)
20
+ --token <secret> Bridge password (use the same in the extension). Auto-generated if omitted.
20
21
  --cwd <path> Default working directory for Claude (default: current dir)
21
22
  --claude-bin <path> Path to claude CLI binary (default: auto-detect)
22
23
  --timeout <seconds> Max time for a single Claude call (default: 7200)
@@ -24,7 +25,7 @@ Options:
24
25
  Relay options (connect to a remote relay server):
25
26
  --relay-url <url> WebSocket URL of the relay server
26
27
  --machine <name> Machine name for the relay
27
- --token <bearer> Machine bearer token
28
+ --token <bearer> Machine bearer token (when using relay, --token is the relay auth)
28
29
  --cf-id <id> Cloudflare Access Client ID (optional)
29
30
  --cf-secret <secret> Cloudflare Access Client Secret (optional)
30
31
 
@@ -80,9 +81,12 @@ function main() {
80
81
  } : null,
81
82
  };
82
83
 
83
- // Generate a random local bearer token for bridge ↔ relay-client auth
84
84
  const crypto = require("node:crypto");
85
- config.bearerToken = crypto.randomBytes(32).toString("hex");
85
+ if (config.relay) {
86
+ config.bearerToken = crypto.randomBytes(32).toString("hex");
87
+ } else {
88
+ config.bearerToken = values.token || crypto.randomBytes(32).toString("hex");
89
+ }
86
90
 
87
91
  run(config);
88
92
  } else if (command === "install-service") {
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,106 +28,329 @@ 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
 
346
+ const isWin = process.platform === "win32";
123
347
  const proc = spawn(config.claudeBin, args, {
124
348
  cwd: cwd || config.cwd,
125
349
  timeout: config.timeout,
126
350
  env: { ...process.env },
127
351
  stdio: ["pipe", "pipe", "pipe"],
128
- detached: process.platform !== "win32",
352
+ detached: !isWin,
353
+ shell: isWin,
129
354
  });
130
355
 
131
356
  let stdout = "", stderr = "";
@@ -138,9 +363,15 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
138
363
  if (session_id) running.delete(session_id);
139
364
  try {
140
365
  const result = JSON.parse(stdout);
366
+ const sid = result.session_id || session_id || crypto.randomUUID();
367
+ if (images && images.length && session_id === "pending" && sid !== "pending") {
368
+ const oldDir = path.join(dataDir(), "images", "pending");
369
+ const newDir = path.join(dataDir(), "images", sid);
370
+ try { fs.renameSync(oldDir, newDir); } catch {}
371
+ }
141
372
  resolve({
142
373
  response: result.result || result.assistantMessage || stdout.slice(0, 5000),
143
- session_id: result.session_id || session_id || crypto.randomUUID(),
374
+ session_id: sid,
144
375
  cost_usd: result.cost_usd || null,
145
376
  duration_ms: result.duration_ms || null,
146
377
  context: result.context || null,
@@ -161,21 +392,23 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
161
392
  });
162
393
  }
163
394
 
164
- // Stop a running session
395
+ // ── Stop session ──
396
+
165
397
  function stopSession(sessionId) {
166
398
  const proc = running.get(sessionId);
167
399
  if (!proc) return { stopped: false, reason: "not_running" };
168
400
  try { proc.kill("SIGINT"); } catch {}
169
401
  setTimeout(() => { try { proc.kill("SIGTERM"); } catch {} }, 3000);
402
+ running.delete(sessionId);
170
403
  return { stopped: true };
171
404
  }
172
405
 
173
- // HTTP server
406
+ // ── HTTP server ──
407
+
174
408
  function startBridge(config) {
175
- const dd = dataDir(config);
409
+ const dd = dataDir();
176
410
 
177
411
  const server = http.createServer(async (req, res) => {
178
- // CORS
179
412
  res.setHeader("Access-Control-Allow-Origin", "*");
180
413
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
181
414
  res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Confirm-Wipe");
@@ -186,7 +419,6 @@ function startBridge(config) {
186
419
  res.end(JSON.stringify(obj));
187
420
  };
188
421
 
189
- // Auth
190
422
  const auth = req.headers.authorization || "";
191
423
  if (req.url !== "/health" && auth !== `Bearer ${config.bearerToken}`) {
192
424
  send(401, { error: "unauthorized" });
@@ -194,6 +426,7 @@ function startBridge(config) {
194
426
  }
195
427
 
196
428
  const url = new URL(req.url, `http://${req.headers.host}`);
429
+ let m;
197
430
 
198
431
  // Health
199
432
  if (url.pathname === "/health") {
@@ -211,9 +444,18 @@ function startBridge(config) {
211
444
  // Sessions list
212
445
  if (url.pathname === "/sessions" && req.method === "GET") {
213
446
  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) });
447
+ const project = url.searchParams.get("project") || "";
448
+ const sessions = listSessions({ includeTiny, project });
449
+ send(200, { sessions });
450
+ return;
451
+ }
452
+
453
+ // Search
454
+ if (url.pathname === "/sessions/search" && req.method === "GET") {
455
+ const q = url.searchParams.get("q") || "";
456
+ const limit = parseInt(url.searchParams.get("limit") || "30", 10);
457
+ const results = searchSessions(q, limit);
458
+ send(200, { results });
217
459
  return;
218
460
  }
219
461
 
@@ -223,8 +465,18 @@ function startBridge(config) {
223
465
  return;
224
466
  }
225
467
 
468
+ // Wipe all sessions
469
+ if (url.pathname === "/sessions/all" && req.method === "DELETE") {
470
+ if (req.headers["x-confirm-wipe"] !== "yes-i-am-sure") {
471
+ send(400, { error: "missing X-Confirm-Wipe header" });
472
+ return;
473
+ }
474
+ send(200, wipeAllSessions());
475
+ return;
476
+ }
477
+
226
478
  // Session messages
227
- let m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/messages$/);
479
+ m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/messages$/);
228
480
  if (m && req.method === "GET") {
229
481
  const result = getSessionMessages(m[1]);
230
482
  send(result.error ? 404 : 200, result);
@@ -275,14 +527,19 @@ function startBridge(config) {
275
527
  m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/title$/);
276
528
  if (m && (req.method === "PATCH" || req.method === "POST")) {
277
529
  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 });
530
+ send(200, renameSession(m[1], body.title));
531
+ return;
532
+ }
533
+
534
+ // Delete single session
535
+ m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)$/);
536
+ if (m && req.method === "DELETE") {
537
+ const result = deleteSession(m[1]);
538
+ send(result.error ? 404 : 200, result);
282
539
  return;
283
540
  }
284
541
 
285
- // Bindings
542
+ // ChatGPT bindings
286
543
  if (url.pathname === "/chatgpt-bindings") {
287
544
  const bindingsFile = path.join(dd, "bindings.json");
288
545
  if (req.method === "GET") {
@@ -310,7 +567,6 @@ function startBridge(config) {
310
567
  });
311
568
  }
312
569
 
313
- // Read JSON body
314
570
  function readBody(req) {
315
571
  return new Promise((resolve) => {
316
572
  let data = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-bridge-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.2",
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": {