@obtoai/agent-bridge 0.1.0-beta.17 → 0.1.0-beta.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obtoai/agent-bridge",
3
- "version": "0.1.0-beta.17",
3
+ "version": "0.1.0-beta.19",
4
4
  "description": "Local consumer for the OBTO Agent Bridge. Receives bridge events over SSE and drives a coding agent (Claude Code or OpenAI Codex) on your machine.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "OBTO Inc.",
@@ -25,10 +25,23 @@ const os = require('os');
25
25
 
26
26
  const CLAUDE_DIR = path.join(os.homedir(), '.claude', 'projects');
27
27
  const CODEX_DIR = path.join(os.homedir(), '.codex', 'sessions');
28
+ // Claude Desktop app's local-agent mode stores session JSONLs under
29
+ // ~/Library/Application Support/Claude/local-agent-mode-sessions/<uuid>/<uuid>/local_<uuid>/.claude/projects/<encoded>/<sid>.jsonl
30
+ // The file format is identical to Claude Code CLI's — same queue-operation
31
+ // preamble, same user/assistant message shape. Just a different root.
32
+ // Phase 6.2.5 — beta.19: surface these so the bridge sees every Claude
33
+ // session on disk, not just the CLI/extension subset.
34
+ const CLAUDE_DESKTOP_BASE = path.join(
35
+ os.homedir(),
36
+ 'Library', 'Application Support', 'Claude', 'local-agent-mode-sessions',
37
+ );
28
38
 
29
39
  const PREVIEW_MAX_CHARS = 200;
30
40
  const TITLE_MAX_CHARS = 80;
31
- const TAIL_READ_BYTES = 16384; // last 16KB covers the last message AND the late-appended ai-title record
41
+ const TAIL_READ_BYTES = 16384; // fixed-budget tail (cheap; used for ai-title + lastMessage)
42
+ const TAIL_STREAM_MAX_BYTES = 524288;// streaming tail cap — 512KB max read for recents (Phase 6.2.4)
43
+ const TAIL_STREAM_CHUNK = 65536; // 64KB chunk size for the backward stream
44
+ const TAIL_STREAM_MIN_MESSAGES = 10; // stop reading once we have this many text-bearing lines
32
45
  const TITLE_MAX_LINES = 40; // scan up to this many lines for a real first message
33
46
  const TITLE_MAX_BYTES = 65536; // hard ceiling per file even if MAX_LINES never reached
34
47
 
@@ -53,6 +66,59 @@ const readTail = (filePath, maxBytes = TAIL_READ_BYTES) => {
53
66
  }
54
67
  };
55
68
 
69
+ // Phase 6.2.4 — streaming tail. For sessions where individual JSONL records
70
+ // exceed the fixed 16KB budget (long assistant messages with lots of inline
71
+ // code), the fixed tail returns 0 parseable lines. This reads backward in
72
+ // 64KB chunks until we either have ≥TAIL_STREAM_MIN_MESSAGES text-bearing
73
+ // user/assistant lines OR hit TAIL_STREAM_MAX_BYTES (512KB) — whichever
74
+ // comes first. The first partial line at the buffer boundary is dropped
75
+ // because it won't JSON.parse cleanly; that's fine, the next chunk will
76
+ // pick it up complete.
77
+ const readTailUntilMessages = (filePath) => {
78
+ let fd = null;
79
+ try {
80
+ const stat = fs.statSync(filePath);
81
+ if (stat.size === 0) return '';
82
+ fd = fs.openSync(filePath, 'r');
83
+ let buffer = '';
84
+ let pos = stat.size;
85
+ while (pos > 0 && buffer.length < TAIL_STREAM_MAX_BYTES) {
86
+ const readSize = Math.min(TAIL_STREAM_CHUNK, pos);
87
+ pos -= readSize;
88
+ const chunk = Buffer.alloc(readSize);
89
+ fs.readSync(fd, chunk, 0, readSize, pos);
90
+ buffer = chunk.toString('utf8') + buffer;
91
+ // Cheap completeness check: count parseable user/assistant lines with text.
92
+ const lines = buffer.split(/\r?\n/);
93
+ let count = 0;
94
+ for (const line of lines) {
95
+ if (!line.trim()) continue;
96
+ let obj;
97
+ try { obj = JSON.parse(line); } catch (_) { continue; }
98
+ const role = (obj && (obj.role || (obj.message && obj.message.role))) || null;
99
+ if (role !== 'user' && role !== 'assistant') continue;
100
+ const raw = (obj.message && obj.message.content) || obj.content;
101
+ if (!raw) continue;
102
+ if (typeof raw === 'string') {
103
+ if (raw.trim()) count++;
104
+ } else if (Array.isArray(raw)) {
105
+ for (const p of raw) {
106
+ if (p && (p.type === 'text' || typeof p.text === 'string') && String(p.text || '').trim()) {
107
+ count++; break;
108
+ }
109
+ }
110
+ }
111
+ }
112
+ if (count >= TAIL_STREAM_MIN_MESSAGES) break;
113
+ }
114
+ return buffer;
115
+ } catch (_) {
116
+ return '';
117
+ } finally {
118
+ if (fd !== null) { try { fs.closeSync(fd); } catch (_) {} }
119
+ }
120
+ };
121
+
56
122
  // Pull the last N user/assistant *logical turns* from the tail, oldest-first.
57
123
  // Phase 6.2.3 — beta.17 fix: Claude Code streams a single assistant reply as
58
124
  // MULTIPLE JSONL lines (text → tool_use → tool_result → text → …), each its
@@ -295,14 +361,45 @@ const decodeClaudeProjectDir = (encoded) => {
295
361
  return encoded.replace(/-/g, '/');
296
362
  };
297
363
 
298
- // Scan ~/.claude/projects/<encoded-projectdir>/<sessionId>.jsonl
299
- const scanClaude = () => {
364
+ // Discover all Claude Desktop "local-agent-mode" project roots.
365
+ // Structure is 3 levels deep before we hit `.claude/projects/`:
366
+ // <BASE>/<accountUuid>/<workspaceUuid>/local_<sessionUuid>/.claude/projects/
367
+ // We collect every leaf `.claude/projects` dir, then walk each like we walk
368
+ // the CLI's ~/.claude/projects.
369
+ const findClaudeDesktopProjectRoots = () => {
370
+ const out = [];
371
+ let l1;
372
+ try { l1 = fs.readdirSync(CLAUDE_DESKTOP_BASE); } catch (_) { return out; }
373
+ for (const a of l1) {
374
+ const aPath = path.join(CLAUDE_DESKTOP_BASE, a);
375
+ let aStat; try { aStat = fs.statSync(aPath); } catch (_) { continue; }
376
+ if (!aStat.isDirectory()) continue;
377
+ let l2; try { l2 = fs.readdirSync(aPath); } catch (_) { continue; }
378
+ for (const b of l2) {
379
+ const bPath = path.join(aPath, b);
380
+ let bStat; try { bStat = fs.statSync(bPath); } catch (_) { continue; }
381
+ if (!bStat.isDirectory()) continue;
382
+ let l3; try { l3 = fs.readdirSync(bPath); } catch (_) { continue; }
383
+ for (const c of l3) {
384
+ if (!c.startsWith('local_')) continue;
385
+ const projects = path.join(bPath, c, '.claude', 'projects');
386
+ try { if (fs.statSync(projects).isDirectory()) out.push(projects); } catch (_) {}
387
+ }
388
+ }
389
+ }
390
+ return out;
391
+ };
392
+
393
+ // Walk a single Claude projects root (works for both ~/.claude/projects and
394
+ // each of the Desktop app's local-agent-mode project roots — file format is
395
+ // identical, only the path differs).
396
+ const walkClaudeProjectsRoot = (root) => {
300
397
  const out = [];
301
398
  let topEntries;
302
- try { topEntries = fs.readdirSync(CLAUDE_DIR); } catch (_) { return out; }
399
+ try { topEntries = fs.readdirSync(root); } catch (_) { return out; }
303
400
 
304
401
  for (const entry of topEntries) {
305
- const projectPath = path.join(CLAUDE_DIR, entry);
402
+ const projectPath = path.join(root, entry);
306
403
  let projectStat;
307
404
  try { projectStat = fs.statSync(projectPath); } catch (_) { continue; }
308
405
  if (!projectStat.isDirectory()) continue;
@@ -318,19 +415,16 @@ const scanClaude = () => {
318
415
  try { stat = fs.statSync(filePath); } catch (_) { continue; }
319
416
  const tail = readTail(filePath);
320
417
  const lastMsg = extractLastMessage(tail);
321
- // Prefer Claude Code's own LLM-summarized title (matches VSCode's list);
322
- // fall back to first-user-message scanning if a session is too new to
323
- // have an ai-title record yet.
324
418
  let title = extractAiTitleFromTail(tail);
325
419
  if (!title) title = extractTitleFromFile(filePath);
326
- const recentMessages = extractRecentMessages(tail);
420
+ const recentMessages = extractRecentMessages(readTailUntilMessages(filePath));
327
421
  out.push({
328
422
  source: 'claude',
329
423
  sessionId,
330
- projectDir: entry, // raw encoded form
331
- projectName: decodeClaudeProjectDir(entry), // best-effort decoded
332
- title: title, // ai-title (Claude) or first-user-message fallback
333
- recentMessages: recentMessages, // last ≤5 user/assistant turns for adopt-time history
424
+ projectDir: entry,
425
+ projectName: decodeClaudeProjectDir(entry),
426
+ title: title,
427
+ recentMessages: recentMessages,
334
428
  lastActivityAt: stat.mtimeMs,
335
429
  lastMessagePreview: lastMsg ? lastMsg.preview : '',
336
430
  lastMessageAuthor: lastMsg ? lastMsg.author : null,
@@ -340,6 +434,18 @@ const scanClaude = () => {
340
434
  return out;
341
435
  };
342
436
 
437
+ // Scan all Claude session storage on this machine — CLI + VSCode extension
438
+ // (~/.claude/projects) AND every Claude Desktop local-agent-mode subdir
439
+ // (~/Library/Application Support/Claude/local-agent-mode-sessions/.../).
440
+ const scanClaude = () => {
441
+ const roots = [CLAUDE_DIR].concat(findClaudeDesktopProjectRoots());
442
+ const out = [];
443
+ for (const root of roots) {
444
+ out.push(...walkClaudeProjectsRoot(root));
445
+ }
446
+ return out;
447
+ };
448
+
343
449
  // Scan ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<sessionId>.jsonl
344
450
  // The first JSONL line for a Codex rollout is a session-meta record that
345
451
  // contains the working directory; we read it once for projectDir.
@@ -399,7 +505,9 @@ const scanCodex = () => {
399
505
  const tail = readTail(filePath);
400
506
  const lastMsg = extractLastMessage(tail);
401
507
  const title = extractTitleFromFile(filePath);
402
- const recentMessages = extractRecentMessages(tail);
508
+ // Recent messages get the streaming tail (handles huge per-message
509
+ // assistant turns that overflow the 16KB fixed budget).
510
+ const recentMessages = extractRecentMessages(readTailUntilMessages(filePath));
403
511
  out.push({
404
512
  source: 'codex',
405
513
  sessionId,