@obtoai/agent-bridge 0.1.0-beta.11 → 0.1.0-beta.12

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.11",
3
+ "version": "0.1.0-beta.12",
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.",
@@ -27,7 +27,9 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude', 'projects');
27
27
  const CODEX_DIR = path.join(os.homedir(), '.codex', 'sessions');
28
28
 
29
29
  const PREVIEW_MAX_CHARS = 200;
30
+ const TITLE_MAX_CHARS = 80;
30
31
  const TAIL_READ_BYTES = 8192; // read the last 8KB of each JSONL for the last message
32
+ const HEAD_READ_BYTES = 4096; // read the first 4KB for the first-user-message title
31
33
 
32
34
  // Read the tail of a (potentially large) JSONL file without slurping the
33
35
  // whole thing into memory. Returns a string (UTF-8) or '' on any failure.
@@ -50,6 +52,58 @@ const readTail = (filePath, maxBytes = TAIL_READ_BYTES) => {
50
52
  }
51
53
  };
52
54
 
55
+ // Read the head of a JSONL file (first ~4KB). Used to extract the first user
56
+ // message for a meaningful session title — "fix the auth bug" beats "OBTO".
57
+ const readHead = (filePath, maxBytes = HEAD_READ_BYTES) => {
58
+ let fd = null;
59
+ try {
60
+ const stat = fs.statSync(filePath);
61
+ const len = Math.min(stat.size, maxBytes);
62
+ if (len <= 0) return '';
63
+ fd = fs.openSync(filePath, 'r');
64
+ const buf = Buffer.alloc(len);
65
+ fs.readSync(fd, buf, 0, len, 0);
66
+ return buf.toString('utf8');
67
+ } catch (_) {
68
+ return '';
69
+ } finally {
70
+ if (fd !== null) { try { fs.closeSync(fd); } catch (_) {} }
71
+ }
72
+ };
73
+
74
+ // Walk JSONL lines forward to find the first user message. Same tolerance
75
+ // for the two SDKs' shapes as extractLastMessage. Returns a short title
76
+ // suitable for a thread name, or '' if no user message landed in the head.
77
+ const extractTitle = (jsonlHead) => {
78
+ if (!jsonlHead) return '';
79
+ const lines = jsonlHead.split(/\r?\n/).filter((l) => l.trim().length > 0);
80
+ for (let i = 0; i < lines.length; i++) {
81
+ let obj;
82
+ try { obj = JSON.parse(lines[i]); } catch (_) { continue; }
83
+ let role = null;
84
+ let raw = null;
85
+ if (obj && obj.message && (obj.message.role || obj.type)) {
86
+ role = obj.message.role || (obj.type === 'user' ? 'user' : 'assistant');
87
+ raw = obj.message.content;
88
+ } else if (obj && obj.role && (obj.content || obj.text)) {
89
+ role = obj.role;
90
+ raw = obj.content != null ? obj.content : obj.text;
91
+ }
92
+ if (role !== 'user' || raw == null) continue;
93
+ let text = '';
94
+ if (typeof raw === 'string') text = raw;
95
+ else if (Array.isArray(raw)) {
96
+ text = raw
97
+ .filter((p) => p && (p.type === 'text' || typeof p.text === 'string'))
98
+ .map((p) => String(p.text || ''))
99
+ .join(' ');
100
+ }
101
+ text = text.trim().replace(/\s+/g, ' ');
102
+ if (text) return text.slice(0, TITLE_MAX_CHARS);
103
+ }
104
+ return '';
105
+ };
106
+
53
107
  // Walk a JSONL tail backwards, parse each non-empty line as JSON, return the
54
108
  // first one we can extract a message from. Tolerant of multiple shapes —
55
109
  // Claude and Codex write slightly different envelopes and the formats have
@@ -142,12 +196,15 @@ const scanClaude = () => {
142
196
  let stat;
143
197
  try { stat = fs.statSync(filePath); } catch (_) { continue; }
144
198
  const tail = readTail(filePath);
199
+ const head = readHead(filePath);
145
200
  const lastMsg = extractLastMessage(tail);
201
+ const title = extractTitle(head);
146
202
  out.push({
147
203
  source: 'claude',
148
204
  sessionId,
149
205
  projectDir: entry, // raw encoded form
150
206
  projectName: decodeClaudeProjectDir(entry), // best-effort decoded
207
+ title: title, // first-user-message slice
151
208
  lastActivityAt: stat.mtimeMs,
152
209
  lastMessagePreview: lastMsg ? lastMsg.preview : '',
153
210
  lastMessageAuthor: lastMsg ? lastMsg.author : null,
@@ -214,12 +271,15 @@ const scanCodex = () => {
214
271
  }
215
272
 
216
273
  const tail = readTail(filePath);
274
+ const head = readHead(filePath);
217
275
  const lastMsg = extractLastMessage(tail);
276
+ const title = extractTitle(head);
218
277
  out.push({
219
278
  source: 'codex',
220
279
  sessionId,
221
280
  projectDir: projectDir || `${y}/${m}/${d}`,
222
281
  projectName: projectDir || null,
282
+ title: title,
223
283
  lastActivityAt: stat.mtimeMs,
224
284
  lastMessagePreview: lastMsg ? lastMsg.preview : '',
225
285
  lastMessageAuthor: lastMsg ? lastMsg.author : null,