@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 +1 -1
- package/src/external-scanner.js +122 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obtoai/agent-bridge",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
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.",
|
package/src/external-scanner.js
CHANGED
|
@@ -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;
|
|
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
|
-
//
|
|
299
|
-
|
|
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(
|
|
399
|
+
try { topEntries = fs.readdirSync(root); } catch (_) { return out; }
|
|
303
400
|
|
|
304
401
|
for (const entry of topEntries) {
|
|
305
|
-
const projectPath = path.join(
|
|
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(
|
|
420
|
+
const recentMessages = extractRecentMessages(readTailUntilMessages(filePath));
|
|
327
421
|
out.push({
|
|
328
422
|
source: 'claude',
|
|
329
423
|
sessionId,
|
|
330
|
-
projectDir: entry,
|
|
331
|
-
projectName: decodeClaudeProjectDir(entry),
|
|
332
|
-
title: title,
|
|
333
|
-
recentMessages: recentMessages,
|
|
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
|
-
|
|
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,
|