agentgui 1.0.817 → 1.0.819
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/CHANGELOG.md +1 -0
- package/CLAUDE.md +7 -5
- package/lib/jsonl-parser.js +179 -0
- package/lib/jsonl-watcher.js +9 -164
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/CLAUDE.md
CHANGED
|
@@ -32,7 +32,8 @@ lib/codec.js msgpack encode/decode (pack/unpack wrappers)
|
|
|
32
32
|
lib/db-queries.js All 88 query functions (createQueries factory, extracted from database.js)
|
|
33
33
|
lib/execution-machine.js XState v5 machine per conversation: idle/streaming/draining/rate_limited states
|
|
34
34
|
lib/gm-agent-configs.js GM agent configuration and spawning
|
|
35
|
-
lib/jsonl-
|
|
35
|
+
lib/jsonl-parser.js JSONL event parsing, session tracking, streaming state (extracted from jsonl-watcher.js)
|
|
36
|
+
lib/jsonl-watcher.js Watches ~/.claude/projects for JSONL file changes, delegates parsing to jsonl-parser.js
|
|
36
37
|
lib/oauth-common.js Shared OAuth helpers (buildBaseUrl, isRemoteRequest, encodeOAuthState, result/relay pages)
|
|
37
38
|
lib/oauth-gemini.js Gemini OAuth flow (credential discovery, token exchange, callback handling)
|
|
38
39
|
lib/oauth-codex.js Codex CLI OAuth flow (PKCE S256, token exchange, callback handling)
|
|
@@ -42,8 +43,9 @@ lib/pm2-manager.js PM2 process management wrapper
|
|
|
42
43
|
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
43
44
|
lib/speech-manager.js TTS orchestration (eager TTS, voice cache, model download, broadcastModelProgress)
|
|
44
45
|
lib/tool-install-machine.js XState v5 machine per tool: unchecked/checking/idle/installing/installed/updating/needs_update/failed states
|
|
45
|
-
lib/tool-manager.js Tool facade - re-exports from tool-version, tool-spawner, tool-provisioner
|
|
46
|
-
lib/tool-version.js
|
|
46
|
+
lib/tool-manager.js Tool facade - re-exports from tool-version-check, tool-version-fetch, tool-spawner, tool-provisioner
|
|
47
|
+
lib/tool-version-check.js Sync/local version detection: BIN_MAP, FRAMEWORK_PATHS, checkCliInstalled, getCliVersion, checkToolInstalled, compareVersions, getInstalledVersion
|
|
48
|
+
lib/tool-version-fetch.js Async/network version functions: getPublishedVersion, fetchPublishedVersion, clearVersionCache, checkToolViaBunx
|
|
47
49
|
lib/tool-spawner.js npm/bun install/update spawn with timeout and heartbeat
|
|
48
50
|
lib/tool-provisioner.js Auto-provisioning and periodic update checking
|
|
49
51
|
lib/routes-speech.js Speech/TTS HTTP route handlers (stt, tts, voices, speech-status)
|
|
@@ -237,9 +239,9 @@ Current tools:
|
|
|
237
239
|
- `cli-agent-browser`: bin=`agent-browser`, pkg=`agent-browser` — uses `-V` flag (not `--version`) for version detection
|
|
238
240
|
- `gm-cc`, `gm-oc`, `gm-gc`, `gm-kilo`, `gm-codex`: plugin tools
|
|
239
241
|
|
|
240
|
-
**BIN_MAP gotcha:** `lib/tool-version.js` has a single `BIN_MAP` constant shared by `checkCliInstalled()` and `getCliVersion()`. Any new CLI tool must be added there. `agent-browser` uses `-V` (not `--version`) — a `versionFlag` override handles this.
|
|
242
|
+
**BIN_MAP gotcha:** `lib/tool-version-check.js` has a single `BIN_MAP` constant shared by `checkCliInstalled()` and `getCliVersion()`. Any new CLI tool must be added there. `agent-browser` uses `-V` (not `--version`) — a `versionFlag` override handles this.
|
|
241
243
|
|
|
242
|
-
**Framework paths:** `lib/tool-version.js` uses a `FRAMEWORK_PATHS` data table instead of per-framework if/else chains. Each framework entry defines pluginDir, versionFile, parseVersion, and optional markerFile/fallbackInstalled. Adding a new framework means adding one entry to this table.
|
|
244
|
+
**Framework paths:** `lib/tool-version-check.js` uses a `FRAMEWORK_PATHS` data table instead of per-framework if/else chains. Each framework entry defines pluginDir, versionFile, parseVersion, and optional markerFile/fallbackInstalled. Adding a new framework means adding one entry to this table.
|
|
243
245
|
|
|
244
246
|
**Background provisioning:** `autoProvision()` runs at startup, checks/installs missing tools (~10s). `startPeriodicUpdateCheck()` runs every 6 hours in background to check for updates. Both broadcast tool status via WebSocket so UI stays in sync.
|
|
245
247
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
export class JsonlParser {
|
|
4
|
+
constructor({ broadcastSync, queries, ownedSessionIds }) {
|
|
5
|
+
this._bc = broadcastSync;
|
|
6
|
+
this._q = queries;
|
|
7
|
+
this._owned = ownedSessionIds;
|
|
8
|
+
this._convMap = new Map();
|
|
9
|
+
this._emitted = new Map();
|
|
10
|
+
this._seqs = new Map();
|
|
11
|
+
this._streaming = new Set();
|
|
12
|
+
this._sessions = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
clear() {
|
|
16
|
+
this._convMap.clear();
|
|
17
|
+
this._emitted.clear();
|
|
18
|
+
this._seqs.clear();
|
|
19
|
+
this._streaming.clear();
|
|
20
|
+
this._sessions.clear();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
removeSid(sid) {
|
|
24
|
+
this._convMap.delete(sid);
|
|
25
|
+
this._seqs.delete(sid);
|
|
26
|
+
this._streaming.delete(sid);
|
|
27
|
+
this._sessions.delete(sid);
|
|
28
|
+
for (const key of [...this._emitted.keys()]) if (key.startsWith(`${sid}:`)) this._emitted.delete(key);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
endAllStreaming() {
|
|
32
|
+
for (const sid of [...this._streaming]) this._endStreaming(this._convMap.get(sid), sid);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_line(fp, line) {
|
|
36
|
+
line = line.trim(); if (!line) return;
|
|
37
|
+
let e; try { e = JSON.parse(line); } catch (_) { return; }
|
|
38
|
+
if (!e || !e.sessionId) return;
|
|
39
|
+
if (this._owned?.has(e.sessionId)) return;
|
|
40
|
+
const cid = this._conv(e.sessionId, e, fp);
|
|
41
|
+
if (cid) this._route(cid, e.sessionId, e);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_conv(sid, e) {
|
|
45
|
+
if (this._convMap.has(sid)) return this._convMap.get(sid);
|
|
46
|
+
const found = this._q.getConversations().find(c => c.claudeSessionId === sid);
|
|
47
|
+
if (found) { this._convMap.set(sid, found.id); return found.id; }
|
|
48
|
+
if (e.type === 'queue-operation' || e.type === 'last-prompt') return null;
|
|
49
|
+
if (e.type === 'user' && e.isMeta) return null;
|
|
50
|
+
const cwd = e.cwd || process.cwd();
|
|
51
|
+
const branch = e.gitBranch || '';
|
|
52
|
+
const base = path.basename(cwd);
|
|
53
|
+
const title = branch ? `${branch} @ ${base}` : base;
|
|
54
|
+
const conv = this._q.createConversation('claude-code', title, cwd);
|
|
55
|
+
this._q.setClaudeSessionId(conv.id, sid);
|
|
56
|
+
this._convMap.set(sid, conv.id);
|
|
57
|
+
this._bc({ type: 'conversation_created', conversation: conv, timestamp: Date.now() });
|
|
58
|
+
return conv.id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_seq(sid) { const n = (this._seqs.get(sid) || 0) + 1; this._seqs.set(sid, n); return n; }
|
|
62
|
+
|
|
63
|
+
_dbSession(cid, sid) {
|
|
64
|
+
if (this._sessions.has(sid)) return this._sessions.get(sid);
|
|
65
|
+
const sess = this._q.createSession(cid);
|
|
66
|
+
this._q.updateSession(sess.id, { status: 'active' });
|
|
67
|
+
this._sessions.set(sid, sess.id);
|
|
68
|
+
return sess.id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_emit(cid, sid, block, role, extra) {
|
|
72
|
+
const dbSid = this._dbSession(cid, sid);
|
|
73
|
+
const seq = this._seq(sid);
|
|
74
|
+
try { this._q.createChunk(dbSid, cid, seq, block.type || 'unknown', block); } catch (_) {}
|
|
75
|
+
this._bc({ type: 'streaming_progress', sessionId: dbSid, conversationId: cid, block, blockRole: role, seq, timestamp: Date.now(), ...extra });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_emitBlocks(cid, sid, blocks, role) {
|
|
79
|
+
for (const b of blocks) {
|
|
80
|
+
if (!b || !b.type) continue;
|
|
81
|
+
this._emit(cid, sid, b, role);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_startStreaming(cid, sid) {
|
|
86
|
+
if (this._streaming.has(sid)) return;
|
|
87
|
+
this._streaming.add(sid);
|
|
88
|
+
const dbSid = this._dbSession(cid, sid);
|
|
89
|
+
this._q.setIsStreaming(cid, true);
|
|
90
|
+
this._bc({ type: 'streaming_start', sessionId: dbSid, conversationId: cid, agentId: 'cli-claude', timestamp: Date.now() });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_endStreaming(cid, sid) {
|
|
94
|
+
if (!this._streaming.has(sid)) return;
|
|
95
|
+
this._streaming.delete(sid);
|
|
96
|
+
const dbSid = this._sessions.get(sid);
|
|
97
|
+
if (dbSid) {
|
|
98
|
+
try { this._q.updateSession(dbSid, { status: 'completed', completed_at: Date.now() }); } catch (_) {}
|
|
99
|
+
}
|
|
100
|
+
this._q.setIsStreaming(cid, false);
|
|
101
|
+
this._bc({ type: 'streaming_complete', sessionId: dbSid || sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() });
|
|
102
|
+
this._sessions.delete(sid);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_route(cid, sid, e) {
|
|
106
|
+
if (e.type === 'queue-operation' || e.type === 'last-prompt' || (e.type === 'user' && e.isMeta)) return;
|
|
107
|
+
|
|
108
|
+
if (e.isApiErrorMessage && e.error === 'rate_limit') {
|
|
109
|
+
this._bc({ type: 'streaming_error', sessionId: this._sessions.get(sid) || sid, conversationId: cid, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (e.type === 'system') {
|
|
114
|
+
if (e.subtype === 'init') { this._startStreaming(cid, sid); return; }
|
|
115
|
+
if (e.subtype === 'turn_duration' || e.subtype === 'stop_hook_summary') { this._endStreaming(cid, sid); return; }
|
|
116
|
+
this._startStreaming(cid, sid);
|
|
117
|
+
this._emit(cid, sid, { type: 'system', subtype: e.subtype, model: e.model, cwd: e.cwd, tools: e.tools, preTokens: e.compactMetadata?.preTokens }, 'system');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (e.type === 'assistant' && e.message?.content) {
|
|
122
|
+
this._startStreaming(cid, sid);
|
|
123
|
+
const key = `${sid}:${e.message.id}`;
|
|
124
|
+
const prev = this._emitted.get(key) || 0;
|
|
125
|
+
const content = e.message.content;
|
|
126
|
+
const newBlocks = content.slice(prev);
|
|
127
|
+
if (newBlocks.length > 0) {
|
|
128
|
+
this._emitted.set(key, content.length);
|
|
129
|
+
this._emitBlocks(cid, sid, newBlocks, 'assistant');
|
|
130
|
+
}
|
|
131
|
+
if (e.message.stop_reason) this._emitted.delete(key);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (e.type === 'user' && e.message?.content) {
|
|
136
|
+
if (e.isCompactSummary) { this._emit(cid, sid, { type: 'compact_summary', content: e.message.content }, 'system'); return; }
|
|
137
|
+
this._startStreaming(cid, sid);
|
|
138
|
+
const content = e.message.content;
|
|
139
|
+
const textParts = Array.isArray(content)
|
|
140
|
+
? content.filter(b => b.type === 'text' && b.text && !b.text.startsWith('<task-notification>')).map(b => b.text).join('\n')
|
|
141
|
+
: (typeof content === 'string' && !content.startsWith('<task-notification>') ? content : '');
|
|
142
|
+
if (textParts) {
|
|
143
|
+
try { this._q.createMessage(cid, 'user', textParts); } catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
146
|
+
for (const b of blocks) if (b.type === 'tool_result') this._emit(cid, sid, b, 'tool_result');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (e.type === 'progress') {
|
|
151
|
+
this._startStreaming(cid, sid);
|
|
152
|
+
const d = e.data || {};
|
|
153
|
+
if (d.type === 'agent_progress') {
|
|
154
|
+
const inner = d.message?.message || d.message;
|
|
155
|
+
const content = inner?.content;
|
|
156
|
+
if (Array.isArray(content)) {
|
|
157
|
+
const role = inner.role === 'user' ? 'tool_result' : 'assistant';
|
|
158
|
+
this._emitBlocks(cid, sid, content, role);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (d.type === 'hook_progress') {
|
|
163
|
+
this._emit(cid, sid, { type: 'hook_progress', hookEvent: d.hookEvent, hookName: d.hookName }, 'system');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (d.type === 'mcp_progress') {
|
|
167
|
+
this._emit(cid, sid, { type: 'mcp_progress', status: d.status, serverName: d.serverName, toolName: d.toolName }, 'system');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this._emit(cid, sid, { type: d.type || e.subtype || 'progress', content: e.content || d }, 'progress');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (e.type === 'result') {
|
|
175
|
+
this._emit(cid, sid, { type: 'result', result: e.result, subtype: e.subtype, duration_ms: e.duration_ms, total_cost_usd: e.total_cost_usd, is_error: e.is_error || false }, 'result', { isResult: true });
|
|
176
|
+
this._endStreaming(cid, sid);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
package/lib/jsonl-watcher.js
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { JsonlParser } from './jsonl-parser.js';
|
|
4
5
|
|
|
5
6
|
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
6
7
|
const DEBOUNCE_MS = 16;
|
|
7
8
|
|
|
8
9
|
export class JsonlWatcher {
|
|
9
10
|
constructor({ broadcastSync, queries, ownedSessionIds }) {
|
|
10
|
-
this.
|
|
11
|
-
this._q = queries;
|
|
12
|
-
this._owned = ownedSessionIds;
|
|
11
|
+
this._parser = new JsonlParser({ broadcastSync, queries, ownedSessionIds });
|
|
13
12
|
this._tails = new Map();
|
|
14
|
-
this._convMap = new Map();
|
|
15
|
-
this._emitted = new Map();
|
|
16
13
|
this._timers = new Map();
|
|
17
|
-
this._seqs = new Map();
|
|
18
|
-
this._streaming = new Set();
|
|
19
|
-
this._sessions = new Map();
|
|
20
14
|
this._watcher = null;
|
|
21
15
|
}
|
|
22
16
|
|
|
@@ -24,7 +18,7 @@ export class JsonlWatcher {
|
|
|
24
18
|
if (!fs.existsSync(PROJECTS_DIR)) return;
|
|
25
19
|
this._scanDir(PROJECTS_DIR, 0);
|
|
26
20
|
for (const [fp, t] of this._timers.entries()) { clearTimeout(t); this._timers.delete(fp); this._read(fp); }
|
|
27
|
-
|
|
21
|
+
this._parser.endAllStreaming();
|
|
28
22
|
try {
|
|
29
23
|
this._watcher = fs.watch(PROJECTS_DIR, { recursive: true }, (_, f) => {
|
|
30
24
|
if (f && f.endsWith('.jsonl')) this._debounce(path.join(PROJECTS_DIR, f));
|
|
@@ -40,13 +34,9 @@ export class JsonlWatcher {
|
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
removeConversation(conversationId) {
|
|
43
|
-
const sids = [...this._convMap.entries()].filter(([, cid]) => cid === conversationId).map(([sid]) => sid);
|
|
37
|
+
const sids = [...this._parser._convMap.entries()].filter(([, cid]) => cid === conversationId).map(([sid]) => sid);
|
|
44
38
|
for (const sid of sids) {
|
|
45
|
-
this.
|
|
46
|
-
this._seqs.delete(sid);
|
|
47
|
-
this._streaming.delete(sid);
|
|
48
|
-
this._sessions.delete(sid);
|
|
49
|
-
for (const key of [...this._emitted.keys()]) if (key.startsWith(`${sid}:`)) this._emitted.delete(key);
|
|
39
|
+
this._parser.removeSid(sid);
|
|
50
40
|
for (const [fp, s] of this._tails.entries()) {
|
|
51
41
|
if (!fp.includes(sid)) continue;
|
|
52
42
|
if (s.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
|
|
@@ -60,9 +50,9 @@ export class JsonlWatcher {
|
|
|
60
50
|
removeAllConversations() {
|
|
61
51
|
for (const s of this._tails.values()) if (s.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
|
|
62
52
|
for (const t of this._timers.values()) clearTimeout(t);
|
|
63
|
-
this._tails.clear();
|
|
64
|
-
this._timers.clear();
|
|
65
|
-
this.
|
|
53
|
+
this._tails.clear();
|
|
54
|
+
this._timers.clear();
|
|
55
|
+
this._parser.clear();
|
|
66
56
|
}
|
|
67
57
|
|
|
68
58
|
_scanDir(dir, depth) {
|
|
@@ -99,155 +89,10 @@ export class JsonlWatcher {
|
|
|
99
89
|
s.partial = text.slice(start);
|
|
100
90
|
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
101
91
|
if (ms > 5) console.warn(`[JsonlWatcher] hot path ${ms.toFixed(1)}ms (${lines.length} lines)`);
|
|
102
|
-
for (const l of lines) this._line(fp, l);
|
|
92
|
+
for (const l of lines) this._parser._line(fp, l);
|
|
103
93
|
} catch (e) {
|
|
104
94
|
if (e.code !== 'ENOENT') console.error('[JsonlWatcher] read error:', e.message);
|
|
105
95
|
if (s.fd !== null) { try { fs.closeSync(s.fd); } catch (_) {} s.fd = null; }
|
|
106
96
|
}
|
|
107
97
|
}
|
|
108
|
-
|
|
109
|
-
_line(fp, line) {
|
|
110
|
-
line = line.trim(); if (!line) return;
|
|
111
|
-
let e; try { e = JSON.parse(line); } catch (_) { return; }
|
|
112
|
-
if (!e || !e.sessionId) return;
|
|
113
|
-
if (this._owned?.has(e.sessionId)) return;
|
|
114
|
-
const cid = this._conv(e.sessionId, e, fp);
|
|
115
|
-
if (cid) this._route(cid, e.sessionId, e);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
_conv(sid, e) {
|
|
119
|
-
if (this._convMap.has(sid)) return this._convMap.get(sid);
|
|
120
|
-
const found = this._q.getConversations().find(c => c.claudeSessionId === sid);
|
|
121
|
-
if (found) { this._convMap.set(sid, found.id); return found.id; }
|
|
122
|
-
if (e.type === 'queue-operation' || e.type === 'last-prompt') return null;
|
|
123
|
-
if (e.type === 'user' && e.isMeta) return null;
|
|
124
|
-
const cwd = e.cwd || process.cwd();
|
|
125
|
-
const branch = e.gitBranch || '';
|
|
126
|
-
const base = path.basename(cwd);
|
|
127
|
-
const title = branch ? `${branch} @ ${base}` : base;
|
|
128
|
-
const conv = this._q.createConversation('claude-code', title, cwd);
|
|
129
|
-
this._q.setClaudeSessionId(conv.id, sid);
|
|
130
|
-
this._convMap.set(sid, conv.id);
|
|
131
|
-
this._bc({ type: 'conversation_created', conversation: conv, timestamp: Date.now() });
|
|
132
|
-
return conv.id;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
_seq(sid) { const n = (this._seqs.get(sid) || 0) + 1; this._seqs.set(sid, n); return n; }
|
|
136
|
-
|
|
137
|
-
_dbSession(cid, sid) {
|
|
138
|
-
if (this._sessions.has(sid)) return this._sessions.get(sid);
|
|
139
|
-
const sess = this._q.createSession(cid);
|
|
140
|
-
this._q.updateSession(sess.id, { status: 'active' });
|
|
141
|
-
this._sessions.set(sid, sess.id);
|
|
142
|
-
return sess.id;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
_emit(cid, sid, block, role, extra) {
|
|
146
|
-
const dbSid = this._dbSession(cid, sid);
|
|
147
|
-
const seq = this._seq(sid);
|
|
148
|
-
try { this._q.createChunk(dbSid, cid, seq, block.type || 'unknown', block); } catch (_) {}
|
|
149
|
-
this._bc({ type: 'streaming_progress', sessionId: dbSid, conversationId: cid, block, blockRole: role, seq, timestamp: Date.now(), ...extra });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
_emitBlocks(cid, sid, blocks, role) {
|
|
153
|
-
for (const b of blocks) {
|
|
154
|
-
if (!b || !b.type) continue;
|
|
155
|
-
this._emit(cid, sid, b, role);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
_startStreaming(cid, sid) {
|
|
160
|
-
if (this._streaming.has(sid)) return;
|
|
161
|
-
this._streaming.add(sid);
|
|
162
|
-
const dbSid = this._dbSession(cid, sid);
|
|
163
|
-
this._q.setIsStreaming(cid, true);
|
|
164
|
-
this._bc({ type: 'streaming_start', sessionId: dbSid, conversationId: cid, agentId: 'cli-claude', timestamp: Date.now() });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
_endStreaming(cid, sid) {
|
|
168
|
-
if (!this._streaming.has(sid)) return;
|
|
169
|
-
this._streaming.delete(sid);
|
|
170
|
-
const dbSid = this._sessions.get(sid);
|
|
171
|
-
if (dbSid) {
|
|
172
|
-
try { this._q.updateSession(dbSid, { status: 'completed', completed_at: Date.now() }); } catch (_) {}
|
|
173
|
-
}
|
|
174
|
-
this._q.setIsStreaming(cid, false);
|
|
175
|
-
this._bc({ type: 'streaming_complete', sessionId: dbSid || sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() });
|
|
176
|
-
this._sessions.delete(sid);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
_route(cid, sid, e) {
|
|
180
|
-
if (e.type === 'queue-operation' || e.type === 'last-prompt' || (e.type === 'user' && e.isMeta)) return;
|
|
181
|
-
|
|
182
|
-
if (e.isApiErrorMessage && e.error === 'rate_limit') {
|
|
183
|
-
this._bc({ type: 'streaming_error', sessionId: this._sessions.get(sid) || sid, conversationId: cid, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() });
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (e.type === 'system') {
|
|
188
|
-
if (e.subtype === 'init') { this._startStreaming(cid, sid); return; }
|
|
189
|
-
if (e.subtype === 'turn_duration' || e.subtype === 'stop_hook_summary') { this._endStreaming(cid, sid); return; }
|
|
190
|
-
this._startStreaming(cid, sid);
|
|
191
|
-
this._emit(cid, sid, { type: 'system', subtype: e.subtype, model: e.model, cwd: e.cwd, tools: e.tools, preTokens: e.compactMetadata?.preTokens }, 'system');
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (e.type === 'assistant' && e.message?.content) {
|
|
196
|
-
this._startStreaming(cid, sid);
|
|
197
|
-
const key = `${sid}:${e.message.id}`;
|
|
198
|
-
const prev = this._emitted.get(key) || 0;
|
|
199
|
-
const content = e.message.content;
|
|
200
|
-
const newBlocks = content.slice(prev);
|
|
201
|
-
if (newBlocks.length > 0) {
|
|
202
|
-
this._emitted.set(key, content.length);
|
|
203
|
-
this._emitBlocks(cid, sid, newBlocks, 'assistant');
|
|
204
|
-
}
|
|
205
|
-
if (e.message.stop_reason) this._emitted.delete(key);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (e.type === 'user' && e.message?.content) {
|
|
210
|
-
if (e.isCompactSummary) { this._emit(cid, sid, { type: 'compact_summary', content: e.message.content }, 'system'); return; }
|
|
211
|
-
this._startStreaming(cid, sid);
|
|
212
|
-
const content = e.message.content;
|
|
213
|
-
const textParts = Array.isArray(content)
|
|
214
|
-
? content.filter(b => b.type === 'text' && b.text && !b.text.startsWith('<task-notification>')).map(b => b.text).join('\n')
|
|
215
|
-
: (typeof content === 'string' && !content.startsWith('<task-notification>') ? content : '');
|
|
216
|
-
if (textParts) {
|
|
217
|
-
try { this._q.createMessage(cid, 'user', textParts); } catch (_) {}
|
|
218
|
-
}
|
|
219
|
-
const blocks = Array.isArray(content) ? content : [];
|
|
220
|
-
for (const b of blocks) if (b.type === 'tool_result') this._emit(cid, sid, b, 'tool_result');
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (e.type === 'progress') {
|
|
225
|
-
this._startStreaming(cid, sid);
|
|
226
|
-
const d = e.data || {};
|
|
227
|
-
if (d.type === 'agent_progress') {
|
|
228
|
-
const inner = d.message?.message || d.message;
|
|
229
|
-
const content = inner?.content;
|
|
230
|
-
if (Array.isArray(content)) {
|
|
231
|
-
const role = inner.role === 'user' ? 'tool_result' : 'assistant';
|
|
232
|
-
this._emitBlocks(cid, sid, content, role);
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (d.type === 'hook_progress') {
|
|
237
|
-
this._emit(cid, sid, { type: 'hook_progress', hookEvent: d.hookEvent, hookName: d.hookName }, 'system');
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (d.type === 'mcp_progress') {
|
|
241
|
-
this._emit(cid, sid, { type: 'mcp_progress', status: d.status, serverName: d.serverName, toolName: d.toolName }, 'system');
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
this._emit(cid, sid, { type: d.type || e.subtype || 'progress', content: e.content || d }, 'progress');
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (e.type === 'result') {
|
|
249
|
-
this._emit(cid, sid, { type: 'result', result: e.result, subtype: e.subtype, duration_ms: e.duration_ms, total_cost_usd: e.total_cost_usd, is_error: e.is_error || false }, 'result', { isResult: true });
|
|
250
|
-
this._endStreaming(cid, sid);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
98
|
}
|