claude-bridge-cli 2.0.3 → 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.
Files changed (2) hide show
  1. package/lib/bridge.js +110 -9
  2. 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
- // Fall back to @file syntax in two cases:
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) Prompt contains a newline AND we're on Windows with a .cmd shim
472
- // cmd.exe parses arg strings line-by-line and silently truncates
473
- // at the first \n, even with Node.js's proper argv quoting. Without
474
- // this fallback, any multi-line prompt to Claude on Windows lost
475
- // everything after line 1, and the model saw a stub message + the
476
- // header but none of the body. Manifested as "your message got
477
- // cut off" responses for prompts that LOOK short.
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 && /\r|\n/.test(finalPrompt));
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`);
@@ -767,6 +846,28 @@ function startBridge(config) {
767
846
  }
768
847
  }
769
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
+
770
871
  send(404, { error: "not found" });
771
872
  });
772
873
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-bridge-cli",
3
- "version": "2.0.3",
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
+ }