claude-bridge-cli 1.0.1 → 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.
- package/lib/bridge.js +337 -83
- package/package.json +1 -1
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 === "
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
send(200, { sessions
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
//
|
|
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