claude-bridge-cli 2.0.6 → 2.0.8

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 CHANGED
@@ -116,6 +116,7 @@ Usage:
116
116
  claude-code-bridge install-service Register as a system service (auto-start on boot)
117
117
  claude-code-bridge uninstall-service Remove the system service
118
118
  claude-code-bridge --help Show this help
119
+ claude-code-bridge --version Print the installed version
119
120
 
120
121
  Options:
121
122
  --port <n> HTTP port for the local bridge (default: 8091)
@@ -152,6 +153,10 @@ function env(key, def) {
152
153
 
153
154
  function main() {
154
155
  const args = process.argv.slice(2);
156
+ if (args.includes("--version") || args.includes("-v") || args[0] === "version") {
157
+ console.log(require("../package.json").version);
158
+ process.exit(0);
159
+ }
155
160
  if (args.includes("--help") || args.includes("-h") || args.length === 0) {
156
161
  console.log(HELP);
157
162
  process.exit(0);
@@ -216,6 +221,7 @@ function main() {
216
221
  }
217
222
 
218
223
  async function run(config) {
224
+ console.log(`[bridge] claude-bridge-cli v${require("../package.json").version}`);
219
225
  console.log(`[bridge] Starting on http://${config.host}:${config.port}`);
220
226
  console.log(`[bridge] Claude CWD: ${config.cwd}`);
221
227
 
package/lib/bridge.js CHANGED
@@ -32,6 +32,49 @@ function projectsDir() {
32
32
  return path.join(homeDir(), ".claude", "projects");
33
33
  }
34
34
 
35
+ // ── Persisted turn-completion markers ──────────────────────────────────────
36
+ // A turn is "complete" ONLY when its claude process actually resolved with a
37
+ // parseable result. We persist that fact to disk (keyed by session id) so the
38
+ // completion signal survives a bridge restart. Without it, getSessionMessages
39
+ // falls back to the in-memory `running` set — which is wiped on restart — and
40
+ // then promotes the trailing JSONL assistant line to `final_text`. For a turn
41
+ // orphaned by a restart, that trailing line is often a mid-turn tool-call
42
+ // preamble ("Running the type-check: I'll report once…") which then gets
43
+ // mis-rendered as the final answer. The marker lets us tell genuine completion
44
+ // apart from an interrupted/orphaned turn.
45
+ function completionDir() {
46
+ const d = path.join(dataDir(), "completions");
47
+ fs.mkdirSync(d, { recursive: true });
48
+ return d;
49
+ }
50
+ function completionMarkerPath(sid) {
51
+ return path.join(completionDir(), encodeURIComponent(sid) + ".json");
52
+ }
53
+ function writeCompletionMarker(sid, finalText) {
54
+ if (!sid || sid === "pending") return;
55
+ try {
56
+ writeJson(completionMarkerPath(sid), {
57
+ session_id: sid,
58
+ stop_reason: "end_turn",
59
+ final_text: typeof finalText === "string" ? finalText : "",
60
+ completed_at: Date.now(),
61
+ });
62
+ } catch {}
63
+ // Best-effort prune of stale markers (>30d) so the dir can't grow forever.
64
+ try {
65
+ const dir = completionDir();
66
+ const cutoff = Date.now() - 30 * 24 * 3600 * 1000;
67
+ for (const f of fs.readdirSync(dir)) {
68
+ const fp = path.join(dir, f);
69
+ try { if (fs.statSync(fp).mtimeMs < cutoff) fs.unlinkSync(fp); } catch {}
70
+ }
71
+ } catch {}
72
+ }
73
+ function readCompletionMarker(sid) {
74
+ if (!sid) return null;
75
+ return readJson(completionMarkerPath(sid), null);
76
+ }
77
+
35
78
  // ── Image handling ──
36
79
 
37
80
  const ALLOWED_IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp"]);
@@ -451,12 +494,18 @@ function getSessionMessages(sessionId) {
451
494
  const parts = Array.isArray(obj.message.content) ? obj.message.content : [obj.message.content];
452
495
  const text = parts.map(p => typeof p === "string" ? p : p.text || "").join("");
453
496
  if (!text) continue;
454
- // Merge consecutive assistant turns into one (tool_use cycles)
497
+ // Merge consecutive assistant turns into one (tool_use cycles).
498
+ // Track the trailing event's stop_reason — `end_turn`/`stop_sequence`
499
+ // means the turn finished; `tool_use` means it stopped on a tool-call
500
+ // preamble (i.e. mid-action). This is the authoritative, in-transcript
501
+ // completion signal and works for every session, old or new.
502
+ const sr = obj.message?.stop_reason || null;
455
503
  if (messages.length && messages[messages.length - 1].role === "assistant") {
456
504
  messages[messages.length - 1].text = (messages[messages.length - 1].text + "\n" + text).trim();
457
505
  messages[messages.length - 1].timestamp = obj.timestamp || messages[messages.length - 1].timestamp;
506
+ messages[messages.length - 1]._stop_reason = sr;
458
507
  } else {
459
- messages.push({ role: "assistant", text, timestamp: obj.timestamp });
508
+ messages.push({ role: "assistant", text, timestamp: obj.timestamp, _stop_reason: sr });
460
509
  }
461
510
  } else if (obj.type === "result" && obj.result?.assistantMessage) {
462
511
  const text = typeof obj.result.assistantMessage === "string" ? obj.result.assistantMessage : "";
@@ -470,18 +519,51 @@ function getSessionMessages(sessionId) {
470
519
  // forever even though Claude is done (the stuck-loader bug on this bridge).
471
520
  const inProgress = running.has(sessionId);
472
521
  const last_event_ts = messages.length ? (messages[messages.length - 1].timestamp || null) : null;
522
+ let status = inProgress ? "in_progress" : "complete";
523
+ let interrupted = false;
473
524
  if (!inProgress && messages.length) {
474
525
  const last = messages[messages.length - 1];
526
+ // Completion is decided by two independent signals, either of which is
527
+ // sufficient:
528
+ // 1. Transcript: the trailing assistant event's stop_reason is
529
+ // `end_turn`/`stop_sequence` (claude closed the turn). Authoritative
530
+ // and present in every session, so old sessions stay "complete".
531
+ // 2. Marker: a persisted completion record exists AND was written no
532
+ // earlier than the trailing event (nothing newer ran after it).
533
+ // If NEITHER holds — e.g. the trailing line is a `tool_use` preamble and
534
+ // no fresh marker exists because the bridge was restarted mid-turn — the
535
+ // turn was orphaned. We must NOT fabricate an end_turn on that line.
536
+ const transcriptDone = last._stop_reason === "end_turn" || last._stop_reason === "stop_sequence";
537
+ const marker = readCompletionMarker(sessionId);
538
+ const lastTs = last_event_ts ? Date.parse(last_event_ts) : 0;
539
+ const markerGenuine = marker && (!lastTs || lastTs <= (marker.completed_at + 2000));
540
+ const genuine = transcriptDone || markerGenuine;
475
541
  if (last.role === "assistant" && last.text) {
476
- last.stop_reason = "end_turn";
477
- last.final_text = last.text;
542
+ if (genuine) {
543
+ last.stop_reason = "end_turn";
544
+ last.final_text = last.text;
545
+ } else {
546
+ // Interrupted/orphaned: finalize honestly instead of passing the
547
+ // trailing preamble off as the answer (and instead of leaving the
548
+ // client's loader spinning forever with no end marker).
549
+ status = "interrupted";
550
+ interrupted = true;
551
+ last.interrupted = true;
552
+ last.stop_reason = "interrupted";
553
+ last.final_text = last.text +
554
+ "\n\n⚠️ This turn was interrupted before it finished (the bridge was " +
555
+ "likely restarted mid-turn). The text above may be a partial step, not " +
556
+ "the final result. Reply \"continue\" to resume.";
557
+ }
478
558
  }
479
559
  }
560
+ for (const m of messages) delete m._stop_reason; // internal-only signal
480
561
  return {
481
562
  session_id: sessionId,
482
563
  messages,
483
564
  in_progress: inProgress,
484
- status: inProgress ? "in_progress" : "complete",
565
+ status,
566
+ interrupted,
485
567
  last_event_ts,
486
568
  };
487
569
  }
@@ -630,6 +712,13 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
630
712
  try {
631
713
  const result = JSON.parse(stdout);
632
714
  const sid = result.session_id || session_id || crypto.randomUUID();
715
+ // Persist genuine completion so it survives a bridge restart. This is
716
+ // the ONLY place a clean end-of-turn is recorded — the error/partial
717
+ // branch below deliberately writes no marker, so an interrupted turn
718
+ // stays distinguishable.
719
+ const finalText = result.result || result.assistantMessage || "";
720
+ writeCompletionMarker(sid, finalText);
721
+ if (session_id && session_id !== sid) writeCompletionMarker(session_id, finalText);
633
722
  if (images && images.length && session_id === "pending" && sid !== "pending") {
634
723
  const oldDir = path.join(dataDir(), "images", "pending");
635
724
  const newDir = path.join(dataDir(), "images", sid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-bridge-cli",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
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": {