claude-bridge-cli 2.0.2 → 2.0.5
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 +131 -9
- package/package.json +2 -2
package/lib/bridge.js
CHANGED
|
@@ -64,6 +64,83 @@ 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
|
+
|
|
67
144
|
function splitUserTextAndImages(text) {
|
|
68
145
|
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
146
|
if (!m) return { cleanText: text, imagePaths: [] };
|
|
@@ -466,18 +543,20 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
|
|
|
466
543
|
const isWin = process.platform === "win32";
|
|
467
544
|
const isCmdShim = isWin && config.claudeBin.endsWith(".cmd");
|
|
468
545
|
|
|
469
|
-
//
|
|
546
|
+
// Pass the prompt via a temp file (@file, read by claude itself) instead of as
|
|
547
|
+
// a command-line argument when:
|
|
470
548
|
// 1) Prompt exceeds Windows cmd.exe's ~8K arg limit (threshold 6000).
|
|
471
|
-
// 2)
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
549
|
+
// 2) We're on Windows with a .cmd shim — period. The shim runs through
|
|
550
|
+
// `cmd.exe /c`, which RE-PARSES the prompt with CMD's own rules: a double
|
|
551
|
+
// quote (") or a metacharacter (% & | < > ^) breaks the command line and
|
|
552
|
+
// surfaces as the cryptic "The system cannot find the file specified";
|
|
553
|
+
// a newline silently truncates the prompt at line 1. Node's argv quoting
|
|
554
|
+
// can't protect against cmd.exe's second parse. Passing @file sidesteps ALL
|
|
555
|
+
// of it, so do it for EVERY cmd-shim prompt — not just multi-line ones.
|
|
556
|
+
// (Previously only triggered on \r|\n, so quoted single-line prompts failed.)
|
|
478
557
|
const useTempFile =
|
|
479
558
|
finalPrompt.length > 6000 ||
|
|
480
|
-
(isWin && isCmdShim
|
|
559
|
+
(isWin && isCmdShim);
|
|
481
560
|
let tmpFile = null;
|
|
482
561
|
if (useTempFile) {
|
|
483
562
|
tmpFile = path.join(os.tmpdir(), `claude-prompt-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
@@ -710,6 +789,27 @@ function startBridge(config) {
|
|
|
710
789
|
}
|
|
711
790
|
}
|
|
712
791
|
|
|
792
|
+
// AutoMode cross-device state + single-runner lease (per session, like marks)
|
|
793
|
+
m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/automode$/);
|
|
794
|
+
if (m) {
|
|
795
|
+
const amFile = path.join(dd, "automode-state.json");
|
|
796
|
+
const allAm = readJson(amFile, {});
|
|
797
|
+
if (req.method === "GET") {
|
|
798
|
+
send(200, { automode: allAm[m[1]] || {} });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (req.method === "PATCH" || req.method === "POST") {
|
|
802
|
+
const body = await readBody(req);
|
|
803
|
+
const allowed = ["to_claude", "to_gpt", "gpt_ready", "claude_ready", "active_client", "active_ts"];
|
|
804
|
+
const cur = allAm[m[1]] || {};
|
|
805
|
+
for (const k of allowed) if (k in body) cur[k] = body[k];
|
|
806
|
+
allAm[m[1]] = cur;
|
|
807
|
+
writeJson(amFile, allAm);
|
|
808
|
+
send(200, { automode: cur });
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
713
813
|
// Title rename
|
|
714
814
|
m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/title$/);
|
|
715
815
|
if (m && (req.method === "PATCH" || req.method === "POST")) {
|
|
@@ -746,6 +846,28 @@ function startBridge(config) {
|
|
|
746
846
|
}
|
|
747
847
|
}
|
|
748
848
|
|
|
849
|
+
// Session files — list (GET) / upload (POST)
|
|
850
|
+
m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/files$/);
|
|
851
|
+
if (m) {
|
|
852
|
+
if (req.method === "GET") {
|
|
853
|
+
try { send(200, { files: listSessionFiles(m[1]) }); } catch (e) { send(400, { error: String(e.message || e) }); }
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (req.method === "POST") {
|
|
857
|
+
const body = await readBody(req);
|
|
858
|
+
try { send(200, saveSessionFile(m[1], body.name, body.data_base64 || body.data)); }
|
|
859
|
+
catch (e) { send(400, { error: String(e.message || e) }); }
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
// Session files — download one (base64 in JSON)
|
|
864
|
+
m = url.pathname.match(/^\/sessions\/([A-Za-z0-9._-]+)\/files\/(.+)$/);
|
|
865
|
+
if (m && req.method === "GET") {
|
|
866
|
+
try { send(200, readSessionFile(m[1], decodeURIComponent(m[2]))); }
|
|
867
|
+
catch (e) { send(e.notFound ? 404 : 400, { error: String(e.message || e) }); }
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
749
871
|
send(404, { error: "not found" });
|
|
750
872
|
});
|
|
751
873
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-bridge-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
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
|
+
}
|