claude-bridge-cli 2.0.3 → 2.0.6
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 +124 -9
- package/package.json +2 -2
package/lib/bridge.js
CHANGED
|
@@ -64,6 +64,92 @@ function saveImages(images, sessionId) {
|
|
|
64
64
|
return saved;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// ── Per-session file exchange (the Files drawer) ──────────────────────────
|
|
68
|
+
// Same per-session folder used for images doubles as a two-way file drawer:
|
|
69
|
+
// the user uploads ANY file here (Claude reads it), and Claude drops files here
|
|
70
|
+
// (the user downloads them). Old files pruned on access.
|
|
71
|
+
const MAX_FILE_BYTES = 45 * 1024 * 1024;
|
|
72
|
+
const FILE_PRUNE_DAYS = 14;
|
|
73
|
+
const FILE_NAME_RE = /^[A-Za-z0-9._ ()+\-]+$/;
|
|
74
|
+
const EXT_MIME = {
|
|
75
|
+
drawio: "application/xml", pdf: "application/pdf", json: "application/json",
|
|
76
|
+
csv: "text/csv", txt: "text/plain", md: "text/markdown", xml: "application/xml",
|
|
77
|
+
svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
78
|
+
gif: "image/gif", webp: "image/webp", zip: "application/zip", gz: "application/gzip",
|
|
79
|
+
tar: "application/x-tar", html: "text/html", css: "text/css", js: "text/javascript",
|
|
80
|
+
ts: "text/plain", py: "text/x-python", cs: "text/plain", java: "text/x-java",
|
|
81
|
+
yml: "text/yaml", yaml: "text/yaml", sql: "text/plain", log: "text/plain", sh: "text/x-sh",
|
|
82
|
+
};
|
|
83
|
+
function mimeFor(name) {
|
|
84
|
+
const e = path.extname(name || "").slice(1).toLowerCase();
|
|
85
|
+
return EXT_MIME[e] || "application/octet-stream";
|
|
86
|
+
}
|
|
87
|
+
function sanitizeAnyFilename(name) {
|
|
88
|
+
let base = path.basename((name || "").trim())
|
|
89
|
+
.replace(/[^A-Za-z0-9._ ()+\-]/g, "_")
|
|
90
|
+
.replace(/^[._ ]+|[._ ]+$/g, "");
|
|
91
|
+
return (base || "upload.bin").slice(0, 200);
|
|
92
|
+
}
|
|
93
|
+
function sessionFilesDir(sid) {
|
|
94
|
+
if (!/^[A-Za-z0-9._-]+$/.test(sid || "")) throw new Error("bad session id");
|
|
95
|
+
return path.join(dataDir(), "images", sid);
|
|
96
|
+
}
|
|
97
|
+
function pruneSessionFiles(dir) {
|
|
98
|
+
const cutoff = Date.now() - FILE_PRUNE_DAYS * 86400 * 1000;
|
|
99
|
+
let entries; try { entries = fs.readdirSync(dir); } catch { return; }
|
|
100
|
+
for (const f of entries) {
|
|
101
|
+
const fp = path.join(dir, f);
|
|
102
|
+
try { const st = fs.statSync(fp); if (st.isFile() && st.mtimeMs < cutoff) fs.unlinkSync(fp); } catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function listSessionFiles(sid) {
|
|
106
|
+
const dir = sessionFilesDir(sid);
|
|
107
|
+
pruneSessionFiles(dir);
|
|
108
|
+
let entries; try { entries = fs.readdirSync(dir); } catch { return []; }
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const f of entries.sort()) {
|
|
111
|
+
const fp = path.join(dir, f);
|
|
112
|
+
try {
|
|
113
|
+
const st = fs.statSync(fp);
|
|
114
|
+
if (st.isFile()) out.push({ name: f, size: st.size, mtime: Math.floor(st.mtimeMs / 1000), mime: mimeFor(f) });
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function readSessionFile(sid, name) {
|
|
120
|
+
const dir = sessionFilesDir(sid);
|
|
121
|
+
const safe = path.basename(name || "");
|
|
122
|
+
if (!safe || safe !== name || !FILE_NAME_RE.test(safe)) throw new Error("bad file name");
|
|
123
|
+
const fp = path.join(dir, safe);
|
|
124
|
+
if (!fs.existsSync(fp) || !fs.statSync(fp).isFile()) { const e = new Error("file not found"); e.notFound = true; throw e; }
|
|
125
|
+
if (fs.statSync(fp).size > MAX_FILE_BYTES) throw new Error("file exceeds limit — too large to transfer");
|
|
126
|
+
const buf = fs.readFileSync(fp);
|
|
127
|
+
return { name: safe, size: buf.length, mime: mimeFor(safe), data_base64: buf.toString("base64") };
|
|
128
|
+
}
|
|
129
|
+
function saveSessionFile(sid, name, dataB64) {
|
|
130
|
+
const dir = sessionFilesDir(sid);
|
|
131
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
132
|
+
pruneSessionFiles(dir);
|
|
133
|
+
if (typeof dataB64 !== "string") throw new Error("missing data_base64");
|
|
134
|
+
const mm = dataB64.match(/^data:[^;]+;base64,([\s\S]+)$/);
|
|
135
|
+
if (mm) dataB64 = mm[1];
|
|
136
|
+
const buf = Buffer.from(dataB64, "base64");
|
|
137
|
+
if (buf.length > MAX_FILE_BYTES) throw new Error("file too large");
|
|
138
|
+
const final = `${Date.now()}-${sanitizeAnyFilename(name || "upload.bin")}`;
|
|
139
|
+
const fp = path.join(dir, final);
|
|
140
|
+
fs.writeFileSync(fp, buf, { mode: 0o600 });
|
|
141
|
+
return { name: final, size: buf.length, mime: mimeFor(final), path: fp };
|
|
142
|
+
}
|
|
143
|
+
function deleteSessionFile(sid, name) {
|
|
144
|
+
const dir = sessionFilesDir(sid);
|
|
145
|
+
const safe = path.basename(name || "");
|
|
146
|
+
if (!safe || safe !== name || !FILE_NAME_RE.test(safe)) throw new Error("bad file name");
|
|
147
|
+
const fp = path.join(dir, safe);
|
|
148
|
+
if (!fs.existsSync(fp) || !fs.statSync(fp).isFile()) { const e = new Error("file not found"); e.notFound = true; throw e; }
|
|
149
|
+
fs.unlinkSync(fp);
|
|
150
|
+
return { deleted: safe };
|
|
151
|
+
}
|
|
152
|
+
|
|
67
153
|
function splitUserTextAndImages(text) {
|
|
68
154
|
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
155
|
if (!m) return { cleanText: text, imagePaths: [] };
|
|
@@ -466,18 +552,20 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
|
|
|
466
552
|
const isWin = process.platform === "win32";
|
|
467
553
|
const isCmdShim = isWin && config.claudeBin.endsWith(".cmd");
|
|
468
554
|
|
|
469
|
-
//
|
|
555
|
+
// Pass the prompt via a temp file (@file, read by claude itself) instead of as
|
|
556
|
+
// a command-line argument when:
|
|
470
557
|
// 1) Prompt exceeds Windows cmd.exe's ~8K arg limit (threshold 6000).
|
|
471
|
-
// 2)
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
558
|
+
// 2) We're on Windows with a .cmd shim — period. The shim runs through
|
|
559
|
+
// `cmd.exe /c`, which RE-PARSES the prompt with CMD's own rules: a double
|
|
560
|
+
// quote (") or a metacharacter (% & | < > ^) breaks the command line and
|
|
561
|
+
// surfaces as the cryptic "The system cannot find the file specified";
|
|
562
|
+
// a newline silently truncates the prompt at line 1. Node's argv quoting
|
|
563
|
+
// can't protect against cmd.exe's second parse. Passing @file sidesteps ALL
|
|
564
|
+
// of it, so do it for EVERY cmd-shim prompt — not just multi-line ones.
|
|
565
|
+
// (Previously only triggered on \r|\n, so quoted single-line prompts failed.)
|
|
478
566
|
const useTempFile =
|
|
479
567
|
finalPrompt.length > 6000 ||
|
|
480
|
-
(isWin && isCmdShim
|
|
568
|
+
(isWin && isCmdShim);
|
|
481
569
|
let tmpFile = null;
|
|
482
570
|
if (useTempFile) {
|
|
483
571
|
tmpFile = path.join(os.tmpdir(), `claude-prompt-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
@@ -767,6 +855,33 @@ function startBridge(config) {
|
|
|
767
855
|
}
|
|
768
856
|
}
|
|
769
857
|
|
|
858
|
+
// Session files — list (GET) / upload (POST)
|
|
859
|
+
m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/files$/);
|
|
860
|
+
if (m) {
|
|
861
|
+
if (req.method === "GET") {
|
|
862
|
+
try { send(200, { files: listSessionFiles(m[1]) }); } catch (e) { send(400, { error: String(e.message || e) }); }
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (req.method === "POST") {
|
|
866
|
+
const body = await readBody(req);
|
|
867
|
+
try { send(200, saveSessionFile(m[1], body.name, body.data_base64 || body.data)); }
|
|
868
|
+
catch (e) { send(400, { error: String(e.message || e) }); }
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// Session files — download one (GET) / remove one (DELETE)
|
|
873
|
+
m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/files\/(.+)$/);
|
|
874
|
+
if (m && req.method === "GET") {
|
|
875
|
+
try { send(200, readSessionFile(m[1], decodeURIComponent(m[2]))); }
|
|
876
|
+
catch (e) { send(e.notFound ? 404 : 400, { error: String(e.message || e) }); }
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (m && req.method === "DELETE") {
|
|
880
|
+
try { send(200, deleteSessionFile(m[1], decodeURIComponent(m[2]))); }
|
|
881
|
+
catch (e) { send(e.notFound ? 404 : 400, { error: String(e.message || e) }); }
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
770
885
|
send(404, { error: "not found" });
|
|
771
886
|
});
|
|
772
887
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-bridge-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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": {
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=18.0.0"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|