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.
- package/bin/cli.js +7 -3
- package/lib/bridge.js +340 -84
- 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
|
-
|
|
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
|
-
|
|
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,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(
|
|
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
|
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
send(200, { sessions
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
//
|
|
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