@obtoai/agent-bridge 0.1.0-beta.11 → 0.1.0-beta.13
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 +85 -0
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.13",
|
|
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
|
@@ -27,7 +27,10 @@ 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 TITLE_MAX_LINES = 40; // scan up to this many lines for a real first message
|
|
33
|
+
const TITLE_MAX_BYTES = 65536; // hard ceiling per file even if MAX_LINES never reached
|
|
31
34
|
|
|
32
35
|
// Read the tail of a (potentially large) JSONL file without slurping the
|
|
33
36
|
// whole thing into memory. Returns a string (UTF-8) or '' on any failure.
|
|
@@ -50,6 +53,83 @@ const readTail = (filePath, maxBytes = TAIL_READ_BYTES) => {
|
|
|
50
53
|
}
|
|
51
54
|
};
|
|
52
55
|
|
|
56
|
+
// Claude Code (and to a lesser extent Codex) prepend a parade of injected
|
|
57
|
+
// "user" messages BEFORE the human's first real prompt — system reminders,
|
|
58
|
+
// IDE state, untrusted-metadata wrappers, command blocks, etc. These are
|
|
59
|
+
// noise from a label perspective. Filter them so the first PLAIN user
|
|
60
|
+
// prompt is what we use as the thread title.
|
|
61
|
+
const isInjectionMessage = (text) => {
|
|
62
|
+
const s = String(text || '').trim();
|
|
63
|
+
if (!s) return true;
|
|
64
|
+
if (/^<[a-zA-Z!]/.test(s)) return true; // <system-reminder>, <ide_opened_file>, etc.
|
|
65
|
+
if (/^Sender \(untrusted metadata\)/i.test(s)) return true;
|
|
66
|
+
if (/^Conversation info \(untrusted metadata\)/i.test(s)) return true;
|
|
67
|
+
if (/^untrusted metadata/i.test(s)) return true;
|
|
68
|
+
if (/^Caveat:/i.test(s)) return true;
|
|
69
|
+
if (/^Claude was launched/i.test(s)) return true;
|
|
70
|
+
if (/^```json/i.test(s)) return true;
|
|
71
|
+
if (/^`<command-name>/.test(s)) return true;
|
|
72
|
+
if (/^This session is being continued/i.test(s)) return true; // /resume preamble
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Walk a JSONL file line-by-line until we find a plain user message (skipping
|
|
77
|
+
// metadata injections) or we've checked TITLE_MAX_LINES / TITLE_MAX_BYTES.
|
|
78
|
+
// Streaming so big files don't get fully slurped. Returns a clean ≤80-char
|
|
79
|
+
// title or '' if nothing meaningful was found in the bound.
|
|
80
|
+
const extractTitleFromFile = (filePath) => {
|
|
81
|
+
let fd = null;
|
|
82
|
+
try {
|
|
83
|
+
fd = fs.openSync(filePath, 'r');
|
|
84
|
+
const CHUNK = 16 * 1024;
|
|
85
|
+
const buf = Buffer.alloc(CHUNK);
|
|
86
|
+
let carry = '';
|
|
87
|
+
let bytesRead = 0;
|
|
88
|
+
let linesChecked = 0;
|
|
89
|
+
while (bytesRead < TITLE_MAX_BYTES && linesChecked < TITLE_MAX_LINES) {
|
|
90
|
+
const n = fs.readSync(fd, buf, 0, CHUNK, bytesRead);
|
|
91
|
+
if (n === 0) break;
|
|
92
|
+
bytesRead += n;
|
|
93
|
+
carry += buf.toString('utf8', 0, n);
|
|
94
|
+
const lines = carry.split(/\r?\n/);
|
|
95
|
+
carry = lines.pop() || '';
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
if (linesChecked >= TITLE_MAX_LINES) break;
|
|
98
|
+
if (!line.trim()) continue;
|
|
99
|
+
linesChecked++;
|
|
100
|
+
let obj;
|
|
101
|
+
try { obj = JSON.parse(line); } catch (_) { continue; }
|
|
102
|
+
let role = null, raw = null;
|
|
103
|
+
if (obj && obj.message && (obj.message.role || obj.type)) {
|
|
104
|
+
role = obj.message.role || (obj.type === 'user' ? 'user' : 'assistant');
|
|
105
|
+
raw = obj.message.content;
|
|
106
|
+
} else if (obj && obj.role && (obj.content || obj.text)) {
|
|
107
|
+
role = obj.role;
|
|
108
|
+
raw = obj.content != null ? obj.content : obj.text;
|
|
109
|
+
}
|
|
110
|
+
if (role !== 'user' || raw == null) continue;
|
|
111
|
+
let text = '';
|
|
112
|
+
if (typeof raw === 'string') text = raw;
|
|
113
|
+
else if (Array.isArray(raw)) {
|
|
114
|
+
text = raw
|
|
115
|
+
.filter((p) => p && (p.type === 'text' || typeof p.text === 'string'))
|
|
116
|
+
.map((p) => String(p.text || ''))
|
|
117
|
+
.join(' ');
|
|
118
|
+
}
|
|
119
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
120
|
+
if (!text) continue;
|
|
121
|
+
if (isInjectionMessage(text)) continue;
|
|
122
|
+
return text.length > TITLE_MAX_CHARS ? text.slice(0, TITLE_MAX_CHARS) : text;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
} catch (_) {
|
|
127
|
+
return '';
|
|
128
|
+
} finally {
|
|
129
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch (_) {} }
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
53
133
|
// Walk a JSONL tail backwards, parse each non-empty line as JSON, return the
|
|
54
134
|
// first one we can extract a message from. Tolerant of multiple shapes —
|
|
55
135
|
// Claude and Codex write slightly different envelopes and the formats have
|
|
@@ -143,11 +223,13 @@ const scanClaude = () => {
|
|
|
143
223
|
try { stat = fs.statSync(filePath); } catch (_) { continue; }
|
|
144
224
|
const tail = readTail(filePath);
|
|
145
225
|
const lastMsg = extractLastMessage(tail);
|
|
226
|
+
const title = extractTitleFromFile(filePath);
|
|
146
227
|
out.push({
|
|
147
228
|
source: 'claude',
|
|
148
229
|
sessionId,
|
|
149
230
|
projectDir: entry, // raw encoded form
|
|
150
231
|
projectName: decodeClaudeProjectDir(entry), // best-effort decoded
|
|
232
|
+
title: title, // first-user-message slice
|
|
151
233
|
lastActivityAt: stat.mtimeMs,
|
|
152
234
|
lastMessagePreview: lastMsg ? lastMsg.preview : '',
|
|
153
235
|
lastMessageAuthor: lastMsg ? lastMsg.author : null,
|
|
@@ -214,12 +296,15 @@ const scanCodex = () => {
|
|
|
214
296
|
}
|
|
215
297
|
|
|
216
298
|
const tail = readTail(filePath);
|
|
299
|
+
const head = readHead(filePath);
|
|
217
300
|
const lastMsg = extractLastMessage(tail);
|
|
301
|
+
const title = extractTitle(head);
|
|
218
302
|
out.push({
|
|
219
303
|
source: 'codex',
|
|
220
304
|
sessionId,
|
|
221
305
|
projectDir: projectDir || `${y}/${m}/${d}`,
|
|
222
306
|
projectName: projectDir || null,
|
|
307
|
+
title: title,
|
|
223
308
|
lastActivityAt: stat.mtimeMs,
|
|
224
309
|
lastMessagePreview: lastMsg ? lastMsg.preview : '',
|
|
225
310
|
lastMessageAuthor: lastMsg ? lastMsg.author : null,
|