afk-code 0.1.0 → 0.1.3
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/LICENSE +21 -0
- package/README.md +64 -97
- package/dist/cli/index.js +1972 -0
- package/package.json +13 -9
- package/slack-manifest.json +3 -3
- package/src/cli/discord.ts +0 -183
- package/src/cli/index.ts +0 -83
- package/src/cli/run.ts +0 -126
- package/src/cli/slack.ts +0 -193
- package/src/discord/channel-manager.ts +0 -191
- package/src/discord/discord-app.ts +0 -359
- package/src/discord/types.ts +0 -4
- package/src/slack/channel-manager.ts +0 -175
- package/src/slack/index.ts +0 -58
- package/src/slack/message-formatter.ts +0 -91
- package/src/slack/session-manager.ts +0 -567
- package/src/slack/slack-app.ts +0 -443
- package/src/slack/types.ts +0 -6
- package/src/types/index.ts +0 -6
- package/src/utils/image-extractor.ts +0 -72
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session manager for Slack bot - handles JSONL watching and Unix socket communication
|
|
3
|
-
* This replaces the need for the daemon + relay.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { watch, type FSWatcher } from 'fs';
|
|
7
|
-
import { readdir } from 'fs/promises';
|
|
8
|
-
import type { Socket } from 'bun';
|
|
9
|
-
import type { TodoItem } from '../types';
|
|
10
|
-
|
|
11
|
-
const DAEMON_SOCKET = '/tmp/afk-code-daemon.sock';
|
|
12
|
-
|
|
13
|
-
export interface SessionInfo {
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
cwd: string;
|
|
17
|
-
projectDir: string;
|
|
18
|
-
status: 'running' | 'idle' | 'ended';
|
|
19
|
-
startedAt: Date;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface InternalSession extends SessionInfo {
|
|
23
|
-
socket: Socket<unknown>;
|
|
24
|
-
watcher?: FSWatcher;
|
|
25
|
-
watchedFile?: string;
|
|
26
|
-
seenMessages: Set<string>;
|
|
27
|
-
slugFound: boolean;
|
|
28
|
-
lastTodosHash: string;
|
|
29
|
-
inPlanMode: boolean;
|
|
30
|
-
initialFileStats: Map<string, number>; // path -> mtime at session start
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ChatMessage {
|
|
34
|
-
role: 'user' | 'assistant';
|
|
35
|
-
content: string;
|
|
36
|
-
timestamp: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ToolCallInfo {
|
|
40
|
-
id: string;
|
|
41
|
-
name: string;
|
|
42
|
-
input: any;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ToolResultInfo {
|
|
46
|
-
toolUseId: string;
|
|
47
|
-
content: string;
|
|
48
|
-
isError: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface SessionEvents {
|
|
52
|
-
onSessionStart: (session: SessionInfo) => void;
|
|
53
|
-
onSessionEnd: (sessionId: string) => void;
|
|
54
|
-
onSessionUpdate: (sessionId: string, name: string) => void;
|
|
55
|
-
onSessionStatus: (sessionId: string, status: 'running' | 'idle' | 'ended') => void;
|
|
56
|
-
onMessage: (sessionId: string, role: 'user' | 'assistant', content: string) => void;
|
|
57
|
-
onTodos: (sessionId: string, todos: TodoItem[]) => void;
|
|
58
|
-
onToolCall: (sessionId: string, tool: ToolCallInfo) => void;
|
|
59
|
-
onToolResult: (sessionId: string, result: ToolResultInfo) => void;
|
|
60
|
-
onPlanModeChange: (sessionId: string, inPlanMode: boolean) => void;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export class SessionManager {
|
|
64
|
-
private sessions = new Map<string, InternalSession>();
|
|
65
|
-
private claimedFiles = new Set<string>();
|
|
66
|
-
private events: SessionEvents;
|
|
67
|
-
private server: ReturnType<typeof Bun.listen> | null = null;
|
|
68
|
-
|
|
69
|
-
constructor(events: SessionEvents) {
|
|
70
|
-
this.events = events;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async start(): Promise<void> {
|
|
74
|
-
// Remove old socket file
|
|
75
|
-
try {
|
|
76
|
-
await Bun.$`rm -f ${DAEMON_SOCKET}`.quiet();
|
|
77
|
-
} catch {}
|
|
78
|
-
|
|
79
|
-
// Start Unix socket server
|
|
80
|
-
this.server = Bun.listen({
|
|
81
|
-
unix: DAEMON_SOCKET,
|
|
82
|
-
socket: {
|
|
83
|
-
data: (socket, data) => {
|
|
84
|
-
const messages = data.toString().split('\n').filter(Boolean);
|
|
85
|
-
for (const msg of messages) {
|
|
86
|
-
try {
|
|
87
|
-
const parsed = JSON.parse(msg);
|
|
88
|
-
this.handleSessionMessage(socket, parsed);
|
|
89
|
-
} catch (error) {
|
|
90
|
-
console.error('[SessionManager] Error parsing message:', error);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
error: (socket, error) => {
|
|
95
|
-
console.error('[SessionManager] Socket error:', error);
|
|
96
|
-
},
|
|
97
|
-
close: (socket) => {
|
|
98
|
-
// Find and cleanup session for this socket
|
|
99
|
-
for (const [id, session] of this.sessions) {
|
|
100
|
-
if (session.socket === socket) {
|
|
101
|
-
console.log(`[SessionManager] Session disconnected: ${id}`);
|
|
102
|
-
this.stopWatching(session);
|
|
103
|
-
this.sessions.delete(id);
|
|
104
|
-
this.events.onSessionEnd(id);
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
console.log(`[SessionManager] Listening on ${DAEMON_SOCKET}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
stop(): void {
|
|
116
|
-
for (const session of this.sessions.values()) {
|
|
117
|
-
this.stopWatching(session);
|
|
118
|
-
}
|
|
119
|
-
this.sessions.clear();
|
|
120
|
-
// Note: Bun.listen doesn't have a close method, socket will close on process exit
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
sendInput(sessionId: string, text: string): boolean {
|
|
124
|
-
const session = this.sessions.get(sessionId);
|
|
125
|
-
if (!session) {
|
|
126
|
-
console.error(`[SessionManager] Session not found: ${sessionId}`);
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Send text first, then Enter
|
|
131
|
-
try {
|
|
132
|
-
session.socket.write(JSON.stringify({ type: 'input', text }) + '\n');
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error(`[SessionManager] Failed to send input to ${sessionId}:`, err);
|
|
135
|
-
// Socket is dead, clean up
|
|
136
|
-
this.stopWatching(session);
|
|
137
|
-
this.sessions.delete(sessionId);
|
|
138
|
-
this.events.onSessionEnd(sessionId);
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
setTimeout(() => {
|
|
143
|
-
try {
|
|
144
|
-
session.socket.write(JSON.stringify({ type: 'input', text: '\r' }) + '\n');
|
|
145
|
-
} catch {
|
|
146
|
-
// Session likely already cleaned up from the first write failure
|
|
147
|
-
}
|
|
148
|
-
}, 50);
|
|
149
|
-
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
getSession(sessionId: string): SessionInfo | undefined {
|
|
154
|
-
const session = this.sessions.get(sessionId);
|
|
155
|
-
if (!session) return undefined;
|
|
156
|
-
return {
|
|
157
|
-
id: session.id,
|
|
158
|
-
name: session.name,
|
|
159
|
-
cwd: session.cwd,
|
|
160
|
-
projectDir: session.projectDir,
|
|
161
|
-
status: session.status,
|
|
162
|
-
startedAt: session.startedAt,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
getAllSessions(): SessionInfo[] {
|
|
167
|
-
return Array.from(this.sessions.values()).map((s) => ({
|
|
168
|
-
id: s.id,
|
|
169
|
-
name: s.name,
|
|
170
|
-
cwd: s.cwd,
|
|
171
|
-
projectDir: s.projectDir,
|
|
172
|
-
status: s.status,
|
|
173
|
-
startedAt: s.startedAt,
|
|
174
|
-
}));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private async handleSessionMessage(socket: Socket<unknown>, message: any): Promise<void> {
|
|
178
|
-
switch (message.type) {
|
|
179
|
-
case 'session_start': {
|
|
180
|
-
// Snapshot existing JSONL files before creating session
|
|
181
|
-
const initialFileStats = await this.snapshotJsonlFiles(message.projectDir);
|
|
182
|
-
|
|
183
|
-
const session: InternalSession = {
|
|
184
|
-
id: message.id,
|
|
185
|
-
name: message.name || message.command?.join(' ') || 'Session',
|
|
186
|
-
cwd: message.cwd,
|
|
187
|
-
projectDir: message.projectDir,
|
|
188
|
-
socket,
|
|
189
|
-
status: 'running',
|
|
190
|
-
seenMessages: new Set(),
|
|
191
|
-
startedAt: new Date(),
|
|
192
|
-
slugFound: false,
|
|
193
|
-
lastTodosHash: '',
|
|
194
|
-
inPlanMode: false,
|
|
195
|
-
initialFileStats,
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
this.sessions.set(message.id, session);
|
|
199
|
-
console.log(`[SessionManager] Session started: ${message.id} - ${session.name}`);
|
|
200
|
-
console.log(`[SessionManager] Snapshot: ${initialFileStats.size} existing JSONL files`);
|
|
201
|
-
|
|
202
|
-
this.events.onSessionStart({
|
|
203
|
-
id: session.id,
|
|
204
|
-
name: session.name,
|
|
205
|
-
cwd: session.cwd,
|
|
206
|
-
projectDir: session.projectDir,
|
|
207
|
-
status: session.status,
|
|
208
|
-
startedAt: session.startedAt,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
this.startWatching(session);
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
case 'session_end': {
|
|
216
|
-
const session = this.sessions.get(message.sessionId);
|
|
217
|
-
if (session) {
|
|
218
|
-
console.log(`[SessionManager] Session ended: ${message.sessionId}`);
|
|
219
|
-
this.stopWatching(session);
|
|
220
|
-
this.sessions.delete(message.sessionId);
|
|
221
|
-
this.events.onSessionEnd(message.sessionId);
|
|
222
|
-
}
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private async snapshotJsonlFiles(projectDir: string): Promise<Map<string, number>> {
|
|
229
|
-
const stats = new Map<string, number>();
|
|
230
|
-
try {
|
|
231
|
-
const files = await readdir(projectDir);
|
|
232
|
-
for (const f of files) {
|
|
233
|
-
if (f.endsWith('.jsonl') && !f.startsWith('agent-')) {
|
|
234
|
-
const path = `${projectDir}/${f}`;
|
|
235
|
-
const stat = await Bun.file(path).stat();
|
|
236
|
-
const mtime = stat?.mtime instanceof Date ? stat.mtime.getTime() : Number(stat?.mtime || 0);
|
|
237
|
-
stats.set(path, mtime);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} catch {
|
|
241
|
-
// Directory might not exist yet
|
|
242
|
-
}
|
|
243
|
-
return stats;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private async findActiveJsonlFile(session: InternalSession): Promise<string | null> {
|
|
247
|
-
try {
|
|
248
|
-
const files = await readdir(session.projectDir);
|
|
249
|
-
const jsonlFiles = files.filter((f) => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
250
|
-
|
|
251
|
-
const allPaths = jsonlFiles
|
|
252
|
-
.map((f) => `${session.projectDir}/${f}`)
|
|
253
|
-
.filter((path) => !this.claimedFiles.has(path));
|
|
254
|
-
|
|
255
|
-
if (allPaths.length === 0) return null;
|
|
256
|
-
|
|
257
|
-
// Get current file stats
|
|
258
|
-
const fileStats = await Promise.all(
|
|
259
|
-
allPaths.map(async (path) => {
|
|
260
|
-
const stat = await Bun.file(path).stat();
|
|
261
|
-
const mtime = stat?.mtime instanceof Date ? stat.mtime.getTime() : Number(stat?.mtime || 0);
|
|
262
|
-
return { path, mtime };
|
|
263
|
-
})
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
// Look for files that are either:
|
|
267
|
-
// 1. New (didn't exist in our snapshot)
|
|
268
|
-
// 2. Modified since our snapshot (for --continue case)
|
|
269
|
-
for (const { path, mtime } of fileStats) {
|
|
270
|
-
const initialMtime = session.initialFileStats.get(path);
|
|
271
|
-
|
|
272
|
-
if (initialMtime === undefined) {
|
|
273
|
-
// New file that didn't exist when session started
|
|
274
|
-
console.log(`[SessionManager] Found new JSONL: ${path}`);
|
|
275
|
-
return path;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (mtime > initialMtime) {
|
|
279
|
-
// Existing file that was modified after session start (--continue case)
|
|
280
|
-
console.log(`[SessionManager] Found modified JSONL (--continue): ${path}`);
|
|
281
|
-
return path;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// No changes detected yet
|
|
286
|
-
return null;
|
|
287
|
-
} catch {
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private async processJsonlUpdates(session: InternalSession): Promise<void> {
|
|
293
|
-
if (!session.watchedFile) return;
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
const file = Bun.file(session.watchedFile);
|
|
297
|
-
const content = await file.text();
|
|
298
|
-
const lines = content.split('\n').filter(Boolean);
|
|
299
|
-
|
|
300
|
-
for (const line of lines) {
|
|
301
|
-
const lineHash = Bun.hash(line).toString();
|
|
302
|
-
if (session.seenMessages.has(lineHash)) continue;
|
|
303
|
-
session.seenMessages.add(lineHash);
|
|
304
|
-
|
|
305
|
-
// Extract session name (slug)
|
|
306
|
-
if (!session.slugFound) {
|
|
307
|
-
const slug = this.extractSlug(line);
|
|
308
|
-
if (slug) {
|
|
309
|
-
session.slugFound = true;
|
|
310
|
-
session.name = slug;
|
|
311
|
-
console.log(`[SessionManager] Session ${session.id} name: ${slug}`);
|
|
312
|
-
this.events.onSessionUpdate(session.id, slug);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Extract todos
|
|
317
|
-
const todos = this.extractTodos(line);
|
|
318
|
-
if (todos) {
|
|
319
|
-
const todosHash = Bun.hash(JSON.stringify(todos)).toString();
|
|
320
|
-
if (todosHash !== session.lastTodosHash) {
|
|
321
|
-
session.lastTodosHash = todosHash;
|
|
322
|
-
this.events.onTodos(session.id, todos);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Detect plan mode changes
|
|
327
|
-
const planModeStatus = this.detectPlanMode(line);
|
|
328
|
-
if (planModeStatus !== null && planModeStatus !== session.inPlanMode) {
|
|
329
|
-
session.inPlanMode = planModeStatus;
|
|
330
|
-
console.log(`[SessionManager] Session ${session.id} plan mode: ${planModeStatus}`);
|
|
331
|
-
this.events.onPlanModeChange(session.id, planModeStatus);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Extract tool calls from assistant messages
|
|
335
|
-
const toolCalls = this.extractToolCalls(line);
|
|
336
|
-
for (const tool of toolCalls) {
|
|
337
|
-
this.events.onToolCall(session.id, tool);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Extract tool results from user messages
|
|
341
|
-
const toolResults = this.extractToolResults(line);
|
|
342
|
-
for (const result of toolResults) {
|
|
343
|
-
this.events.onToolResult(session.id, result);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Parse and forward messages
|
|
347
|
-
const parsed = this.parseJsonlLine(line);
|
|
348
|
-
if (parsed) {
|
|
349
|
-
const messageTime = new Date(parsed.timestamp);
|
|
350
|
-
if (messageTime < session.startedAt) continue;
|
|
351
|
-
|
|
352
|
-
this.events.onMessage(session.id, parsed.role, parsed.content);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
} catch (err) {
|
|
356
|
-
console.error('[SessionManager] Error processing JSONL:', err);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
private async startWatching(session: InternalSession): Promise<void> {
|
|
361
|
-
const jsonlFile = await this.findActiveJsonlFile(session);
|
|
362
|
-
|
|
363
|
-
if (jsonlFile) {
|
|
364
|
-
session.watchedFile = jsonlFile;
|
|
365
|
-
this.claimedFiles.add(jsonlFile);
|
|
366
|
-
console.log(`[SessionManager] Watching: ${jsonlFile}`);
|
|
367
|
-
await this.processJsonlUpdates(session);
|
|
368
|
-
} else {
|
|
369
|
-
console.log(`[SessionManager] Waiting for JSONL changes in ${session.projectDir}`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Watch directory for changes
|
|
373
|
-
try {
|
|
374
|
-
session.watcher = watch(session.projectDir, { recursive: false }, async (_, filename) => {
|
|
375
|
-
if (!filename?.endsWith('.jsonl')) return;
|
|
376
|
-
|
|
377
|
-
if (!session.watchedFile) {
|
|
378
|
-
const newFile = await this.findActiveJsonlFile(session);
|
|
379
|
-
if (newFile) {
|
|
380
|
-
session.watchedFile = newFile;
|
|
381
|
-
this.claimedFiles.add(newFile);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const filePath = `${session.projectDir}/${filename}`;
|
|
386
|
-
if (session.watchedFile && filePath === session.watchedFile) {
|
|
387
|
-
await this.processJsonlUpdates(session);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
} catch (err) {
|
|
391
|
-
console.error('[SessionManager] Error setting up watcher:', err);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Poll as backup
|
|
395
|
-
const pollInterval = setInterval(async () => {
|
|
396
|
-
if (!this.sessions.has(session.id)) {
|
|
397
|
-
clearInterval(pollInterval);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (!session.watchedFile) {
|
|
402
|
-
const newFile = await this.findActiveJsonlFile(session);
|
|
403
|
-
if (newFile) {
|
|
404
|
-
session.watchedFile = newFile;
|
|
405
|
-
this.claimedFiles.add(newFile);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (session.watchedFile) {
|
|
410
|
-
await this.processJsonlUpdates(session);
|
|
411
|
-
}
|
|
412
|
-
}, 1000);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
private stopWatching(session: InternalSession): void {
|
|
416
|
-
if (session.watcher) {
|
|
417
|
-
session.watcher.close();
|
|
418
|
-
}
|
|
419
|
-
if (session.watchedFile) {
|
|
420
|
-
this.claimedFiles.delete(session.watchedFile);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private detectPlanMode(line: string): boolean | null {
|
|
425
|
-
try {
|
|
426
|
-
const data = JSON.parse(line);
|
|
427
|
-
if (data.type !== 'user') return null;
|
|
428
|
-
|
|
429
|
-
const content = data.message?.content;
|
|
430
|
-
if (typeof content !== 'string') return null;
|
|
431
|
-
|
|
432
|
-
// Check for plan mode activation
|
|
433
|
-
if (content.includes('<system-reminder>') && content.includes('Plan mode is active')) {
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Check for plan mode exit (ExitPlanMode was called)
|
|
438
|
-
if (content.includes('Exited Plan Mode') || content.includes('exited plan mode')) {
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return null;
|
|
443
|
-
} catch {
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
private extractToolCalls(line: string): ToolCallInfo[] {
|
|
449
|
-
try {
|
|
450
|
-
const data = JSON.parse(line);
|
|
451
|
-
if (data.type !== 'assistant') return [];
|
|
452
|
-
|
|
453
|
-
const content = data.message?.content;
|
|
454
|
-
if (!Array.isArray(content)) return [];
|
|
455
|
-
|
|
456
|
-
const tools: ToolCallInfo[] = [];
|
|
457
|
-
for (const block of content) {
|
|
458
|
-
if (block.type === 'tool_use' && block.id && block.name) {
|
|
459
|
-
tools.push({
|
|
460
|
-
id: block.id,
|
|
461
|
-
name: block.name,
|
|
462
|
-
input: block.input || {},
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return tools;
|
|
467
|
-
} catch {
|
|
468
|
-
return [];
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
private extractToolResults(line: string): ToolResultInfo[] {
|
|
473
|
-
try {
|
|
474
|
-
const data = JSON.parse(line);
|
|
475
|
-
if (data.type !== 'user') return [];
|
|
476
|
-
|
|
477
|
-
const content = data.message?.content;
|
|
478
|
-
if (!Array.isArray(content)) return [];
|
|
479
|
-
|
|
480
|
-
const results: ToolResultInfo[] = [];
|
|
481
|
-
for (const block of content) {
|
|
482
|
-
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
483
|
-
// Content can be string or array of text blocks
|
|
484
|
-
let text = '';
|
|
485
|
-
if (typeof block.content === 'string') {
|
|
486
|
-
text = block.content;
|
|
487
|
-
} else if (Array.isArray(block.content)) {
|
|
488
|
-
text = block.content
|
|
489
|
-
.filter((b: any) => b.type === 'text')
|
|
490
|
-
.map((b: any) => b.text)
|
|
491
|
-
.join('\n');
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
results.push({
|
|
495
|
-
toolUseId: block.tool_use_id,
|
|
496
|
-
content: text,
|
|
497
|
-
isError: block.is_error === true,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return results;
|
|
502
|
-
} catch {
|
|
503
|
-
return [];
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
private extractSlug(line: string): string | null {
|
|
508
|
-
try {
|
|
509
|
-
const data = JSON.parse(line);
|
|
510
|
-
if (data.slug && typeof data.slug === 'string') {
|
|
511
|
-
return data.slug;
|
|
512
|
-
}
|
|
513
|
-
return null;
|
|
514
|
-
} catch {
|
|
515
|
-
return null;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
private extractTodos(line: string): TodoItem[] | null {
|
|
520
|
-
try {
|
|
521
|
-
const data = JSON.parse(line);
|
|
522
|
-
if (data.todos && Array.isArray(data.todos) && data.todos.length > 0) {
|
|
523
|
-
return data.todos.map((t: any) => ({
|
|
524
|
-
content: t.content || '',
|
|
525
|
-
status: t.status || 'pending',
|
|
526
|
-
activeForm: t.activeForm,
|
|
527
|
-
}));
|
|
528
|
-
}
|
|
529
|
-
return null;
|
|
530
|
-
} catch {
|
|
531
|
-
return null;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
private parseJsonlLine(line: string): ChatMessage | null {
|
|
536
|
-
try {
|
|
537
|
-
const data = JSON.parse(line);
|
|
538
|
-
|
|
539
|
-
if (data.type !== 'user' && data.type !== 'assistant') return null;
|
|
540
|
-
if (data.isMeta || data.subtype) return null;
|
|
541
|
-
|
|
542
|
-
const message = data.message;
|
|
543
|
-
if (!message || !message.role) return null;
|
|
544
|
-
|
|
545
|
-
let content = '';
|
|
546
|
-
if (typeof message.content === 'string') {
|
|
547
|
-
content = message.content;
|
|
548
|
-
} else if (Array.isArray(message.content)) {
|
|
549
|
-
for (const block of message.content) {
|
|
550
|
-
if (block.type === 'text' && block.text) {
|
|
551
|
-
content += block.text;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (!content.trim()) return null;
|
|
557
|
-
|
|
558
|
-
return {
|
|
559
|
-
role: message.role as 'user' | 'assistant',
|
|
560
|
-
content: content.trim(),
|
|
561
|
-
timestamp: data.timestamp || new Date().toISOString(),
|
|
562
|
-
};
|
|
563
|
-
} catch {
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|