orc-server 1.0.5

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.
Files changed (37) hide show
  1. package/dist/__tests__/auth.test.js +49 -0
  2. package/dist/__tests__/watcher.test.js +58 -0
  3. package/dist/claude/chatService.js +175 -0
  4. package/dist/claude/sessionBrowser.js +743 -0
  5. package/dist/claude/watcher.js +242 -0
  6. package/dist/config.js +44 -0
  7. package/dist/files/browser.js +227 -0
  8. package/dist/files/reader.js +159 -0
  9. package/dist/files/search.js +124 -0
  10. package/dist/git/gitHandler.js +177 -0
  11. package/dist/git/gitService.js +299 -0
  12. package/dist/git/index.js +8 -0
  13. package/dist/http/server.js +96 -0
  14. package/dist/index.js +77 -0
  15. package/dist/ssh/index.js +9 -0
  16. package/dist/ssh/sshHandler.js +205 -0
  17. package/dist/ssh/sshManager.js +329 -0
  18. package/dist/terminal/index.js +11 -0
  19. package/dist/terminal/localTerminalHandler.js +176 -0
  20. package/dist/terminal/localTerminalManager.js +497 -0
  21. package/dist/terminal/terminalWebSocket.js +136 -0
  22. package/dist/types.js +2 -0
  23. package/dist/utils/logger.js +42 -0
  24. package/dist/websocket/auth.js +18 -0
  25. package/dist/websocket/server.js +631 -0
  26. package/package.json +66 -0
  27. package/web-dist/assets/highlight-l0sNRNKZ.js +1 -0
  28. package/web-dist/assets/index-C8TJGN-T.css +41 -0
  29. package/web-dist/assets/index-DjLLxjMD.js +39 -0
  30. package/web-dist/assets/markdown-C_j0ZeeY.js +51 -0
  31. package/web-dist/assets/react-vendor-CqP5oCk4.js +9 -0
  32. package/web-dist/assets/xterm-BCk906R6.js +9 -0
  33. package/web-dist/icon-192.png +0 -0
  34. package/web-dist/icon-512.png +0 -0
  35. package/web-dist/index.html +23 -0
  36. package/web-dist/manifest.json +24 -0
  37. package/web-dist/sw.js +35 -0
@@ -0,0 +1,743 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getSessionFilePath = getSessionFilePath;
37
+ exports.watchSession = watchSession;
38
+ exports.unwatchSession = unwatchSession;
39
+ exports.listWorkspaces = listWorkspaces;
40
+ exports.listSessions = listSessions;
41
+ exports.getSessionMessages = getSessionMessages;
42
+ exports.getSessionMessagesPage = getSessionMessagesPage;
43
+ exports.listSubagents = listSubagents;
44
+ exports.getSubagentMessages = getSubagentMessages;
45
+ exports.listToolResults = listToolResults;
46
+ exports.getToolResultContent = getToolResultContent;
47
+ exports.getSessionFolderInfo = getSessionFolderInfo;
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const os = __importStar(require("os"));
51
+ const readline = __importStar(require("readline"));
52
+ const chokidar_1 = require("chokidar");
53
+ const logger_1 = require("../utils/logger");
54
+ const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
55
+ const sessionWatchers = new Map();
56
+ function getSessionFilePath(workspace, sessionId) {
57
+ return path.join(PROJECTS_DIR, workspace, `${sessionId}.jsonl`);
58
+ }
59
+ async function watchSession(clientId, workspace, sessionId, onUpdate) {
60
+ // 先停止之前的监听
61
+ unwatchSession(clientId);
62
+ const filePath = getSessionFilePath(workspace, sessionId);
63
+ try {
64
+ const stat = await fs.promises.stat(filePath);
65
+ const lastSize = stat.size;
66
+ const watcher = (0, chokidar_1.watch)(filePath, {
67
+ persistent: true,
68
+ usePolling: true,
69
+ interval: 2000, // Reduced from 500ms to save mobile battery
70
+ });
71
+ // Debounce change handler to prevent rapid-fire updates
72
+ let debounceTimer = null;
73
+ watcher.on('change', async () => {
74
+ if (debounceTimer)
75
+ clearTimeout(debounceTimer);
76
+ debounceTimer = setTimeout(async () => {
77
+ const watcherInfo = sessionWatchers.get(clientId);
78
+ if (!watcherInfo)
79
+ return;
80
+ try {
81
+ const newStat = await fs.promises.stat(filePath);
82
+ // Handle file truncation (e.g. log rotation)
83
+ if (newStat.size < watcherInfo.lastSize) {
84
+ watcherInfo.lastSize = 0;
85
+ }
86
+ if (newStat.size <= watcherInfo.lastSize)
87
+ return;
88
+ // 读取新增的内容
89
+ const newMessages = await readNewMessages(filePath, watcherInfo.lastSize);
90
+ watcherInfo.lastSize = newStat.size;
91
+ if (newMessages.length > 0) {
92
+ onUpdate(newMessages);
93
+ }
94
+ }
95
+ catch (error) {
96
+ logger_1.logger.error('Error reading session update:', error);
97
+ }
98
+ }, 200);
99
+ });
100
+ sessionWatchers.set(clientId, {
101
+ watcher,
102
+ filePath,
103
+ lastSize,
104
+ onUpdate,
105
+ });
106
+ logger_1.logger.info(`Started watching session: ${sessionId} for client: ${clientId}`);
107
+ }
108
+ catch (error) {
109
+ logger_1.logger.error('Error starting session watcher:', error);
110
+ }
111
+ }
112
+ function unwatchSession(clientId) {
113
+ const watcherInfo = sessionWatchers.get(clientId);
114
+ if (watcherInfo) {
115
+ watcherInfo.watcher.close();
116
+ sessionWatchers.delete(clientId);
117
+ logger_1.logger.info(`Stopped watching session for client: ${clientId}`);
118
+ }
119
+ }
120
+ async function readNewMessages(filePath, fromPosition) {
121
+ return new Promise((resolve, reject) => {
122
+ const messages = [];
123
+ const stream = fs.createReadStream(filePath, {
124
+ encoding: 'utf-8',
125
+ start: fromPosition,
126
+ });
127
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
128
+ rl.on('line', (line) => {
129
+ if (!line.trim())
130
+ return;
131
+ try {
132
+ const entry = JSON.parse(line);
133
+ const parsed = parseEntry(entry);
134
+ if (parsed)
135
+ messages.push(...parsed);
136
+ }
137
+ catch {
138
+ // Skip malformed lines
139
+ }
140
+ });
141
+ rl.on('close', () => resolve(messages));
142
+ rl.on('error', reject);
143
+ });
144
+ }
145
+ function decodeWorkspacePath(dirName) {
146
+ // -Users-lincyaw-workspace-DevSpace → ~/workspace/DevSpace
147
+ const home = os.homedir(); // /Users/lincyaw
148
+ const homeParts = home.split(path.sep).filter(Boolean); // ['Users', 'lincyaw']
149
+ const parts = dirName.split('-').filter(Boolean);
150
+ // Check if the parts start with the home directory components
151
+ let matchLen = 0;
152
+ for (let i = 0; i < homeParts.length && i < parts.length; i++) {
153
+ if (parts[i] === homeParts[i]) {
154
+ matchLen++;
155
+ }
156
+ else {
157
+ break;
158
+ }
159
+ }
160
+ if (matchLen === homeParts.length) {
161
+ const rest = parts.slice(matchLen).join('/');
162
+ return rest ? `~/${rest}` : '~';
163
+ }
164
+ return '/' + parts.join('/');
165
+ }
166
+ /**
167
+ * Resolve a Claude projects dirName (e.g. "-home-nn-workspace-proj-rcabench-paper")
168
+ * back to an actual filesystem path. The encoding replaces "/" (and "_") with "-",
169
+ * making it ambiguous. We probe the filesystem trying "-", "_", and "/" as joiners
170
+ * between segments to find the real path.
171
+ */
172
+ async function resolveDirNameToPath(dirName) {
173
+ const segments = dirName.split('-').filter(Boolean);
174
+ if (segments.length === 0)
175
+ return '/';
176
+ async function isDir(p) {
177
+ try {
178
+ return (await fs.promises.stat(p)).isDirectory();
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ // Try joining adjacent segments with each joiner and probe the filesystem
185
+ const JOINERS = ['-', '_'];
186
+ async function probe(current, remaining) {
187
+ if (remaining.length === 0) {
188
+ return await isDir(current) ? current : null;
189
+ }
190
+ // Try consuming 1..N remaining segments as a single directory component
191
+ for (let take = remaining.length; take >= 1; take--) {
192
+ const parts = remaining.slice(0, take);
193
+ // For multi-segment chunks, try all joiner characters
194
+ const joinVariants = take === 1
195
+ ? [parts[0]]
196
+ : JOINERS.map(j => parts.join(j));
197
+ for (const name of joinVariants) {
198
+ const candidate = current + '/' + name;
199
+ if (await isDir(candidate)) {
200
+ const result = await probe(candidate, remaining.slice(take));
201
+ if (result)
202
+ return result;
203
+ }
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ const result = await probe('', segments);
209
+ return result || ('/' + segments.join('/'));
210
+ }
211
+ async function listWorkspaces() {
212
+ try {
213
+ const entries = await fs.promises.readdir(PROJECTS_DIR, { withFileTypes: true });
214
+ const workspaces = [];
215
+ for (const entry of entries) {
216
+ if (!entry.isDirectory())
217
+ continue;
218
+ const workspaceDir = path.join(PROJECTS_DIR, entry.name);
219
+ const indexPath = path.join(workspaceDir, 'sessions-index.json');
220
+ let indexCount = 0;
221
+ let lastModified = 0;
222
+ let originalPath = '';
223
+ // Read sessions-index.json if available
224
+ try {
225
+ const raw = await fs.promises.readFile(indexPath, 'utf-8');
226
+ const index = JSON.parse(raw);
227
+ originalPath = index.originalPath || '';
228
+ const sessions = index.entries || [];
229
+ indexCount = sessions.length;
230
+ for (const s of sessions) {
231
+ const mtime = s.fileMtime || new Date(s.modified).getTime() || 0;
232
+ if (mtime > lastModified)
233
+ lastModified = mtime;
234
+ }
235
+ }
236
+ catch {
237
+ // No sessions-index.json or parse error, continue with file scan
238
+ }
239
+ // Also scan for .jsonl files to get accurate count
240
+ let jsonlCount = 0;
241
+ try {
242
+ const files = await fs.promises.readdir(workspaceDir);
243
+ jsonlCount = files.filter(f => f.endsWith('.jsonl')).length;
244
+ // Update lastModified from .jsonl files if needed
245
+ if (lastModified === 0 && jsonlCount > 0) {
246
+ for (const file of files) {
247
+ if (!file.endsWith('.jsonl'))
248
+ continue;
249
+ try {
250
+ const stat = await fs.promises.stat(path.join(workspaceDir, file));
251
+ const mtime = stat.mtimeMs;
252
+ if (mtime > lastModified)
253
+ lastModified = mtime;
254
+ }
255
+ catch {
256
+ // Skip files we can't stat
257
+ }
258
+ }
259
+ }
260
+ }
261
+ catch {
262
+ // Can't read directory, use index count
263
+ }
264
+ // Use the larger of the two counts to ensure accuracy
265
+ const sessionCount = Math.max(indexCount, jsonlCount);
266
+ // Only add workspace if it has sessions
267
+ if (sessionCount > 0) {
268
+ // fullPath: prefer originalPath from index, fall back to probing filesystem
269
+ const fullPath = originalPath || await resolveDirNameToPath(entry.name);
270
+ const home = os.homedir();
271
+ const displayPath = fullPath.startsWith(home + '/')
272
+ ? '~' + fullPath.slice(home.length)
273
+ : fullPath;
274
+ workspaces.push({
275
+ dirName: entry.name,
276
+ displayPath,
277
+ fullPath,
278
+ sessionCount,
279
+ lastModified,
280
+ });
281
+ }
282
+ }
283
+ workspaces.sort((a, b) => b.lastModified - a.lastModified);
284
+ return workspaces;
285
+ }
286
+ catch (error) {
287
+ logger_1.logger.error('Error listing workspaces:', error);
288
+ return [];
289
+ }
290
+ }
291
+ async function listSessions(workspace) {
292
+ const workspaceDir = path.join(PROJECTS_DIR, workspace);
293
+ const indexPath = path.join(workspaceDir, 'sessions-index.json');
294
+ // Build a map from sessionId to SessionInfo from the index
295
+ const sessionMap = new Map();
296
+ try {
297
+ const raw = await fs.promises.readFile(indexPath, 'utf-8');
298
+ const index = JSON.parse(raw);
299
+ const entries = index.entries || [];
300
+ for (const e of entries) {
301
+ sessionMap.set(e.sessionId, {
302
+ sessionId: e.sessionId,
303
+ firstPrompt: e.firstPrompt || '',
304
+ summary: e.summary || '',
305
+ messageCount: e.messageCount || 0,
306
+ created: e.created || '',
307
+ modified: e.modified || '',
308
+ });
309
+ }
310
+ }
311
+ catch {
312
+ // Index file doesn't exist or parse error, continue with file scan
313
+ }
314
+ // Scan for .jsonl files not in the index
315
+ try {
316
+ const files = await fs.promises.readdir(workspaceDir);
317
+ for (const file of files) {
318
+ if (!file.endsWith('.jsonl'))
319
+ continue;
320
+ const sessionId = file.replace('.jsonl', '');
321
+ if (sessionMap.has(sessionId))
322
+ continue;
323
+ // Extract basic info from file
324
+ const filePath = path.join(workspaceDir, file);
325
+ const stat = await fs.promises.stat(filePath);
326
+ const info = await extractSessionInfo(filePath, sessionId, stat);
327
+ if (info)
328
+ sessionMap.set(sessionId, info);
329
+ }
330
+ }
331
+ catch (error) {
332
+ logger_1.logger.error('Error scanning workspace directory:', error);
333
+ }
334
+ const sessions = Array.from(sessionMap.values());
335
+ sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
336
+ return sessions;
337
+ }
338
+ async function extractSessionInfo(filePath, sessionId, stat) {
339
+ try {
340
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
341
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
342
+ let firstPrompt = '';
343
+ let messageCount = 0;
344
+ for await (const line of rl) {
345
+ if (!line.trim())
346
+ continue;
347
+ try {
348
+ const entry = JSON.parse(line);
349
+ if (entry.type === 'user' && entry.message) {
350
+ messageCount++;
351
+ if (!firstPrompt && typeof entry.message.content === 'string') {
352
+ firstPrompt = entry.message.content.substring(0, 200);
353
+ }
354
+ }
355
+ else if (entry.type === 'assistant') {
356
+ messageCount++;
357
+ }
358
+ // Stop after reading enough to get firstPrompt and some count
359
+ if (firstPrompt && messageCount >= 10) {
360
+ rl.close();
361
+ break;
362
+ }
363
+ }
364
+ catch {
365
+ // Skip malformed lines
366
+ }
367
+ }
368
+ return {
369
+ sessionId,
370
+ firstPrompt: firstPrompt || 'No prompt',
371
+ summary: '',
372
+ messageCount,
373
+ created: stat.birthtime.toISOString(),
374
+ modified: stat.mtime.toISOString(),
375
+ };
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ async function getSessionMessages(workspace, sessionId) {
382
+ const filePath = path.join(PROJECTS_DIR, workspace, `${sessionId}.jsonl`);
383
+ const messages = [];
384
+ try {
385
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
386
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
387
+ for await (const line of rl) {
388
+ if (!line.trim())
389
+ continue;
390
+ try {
391
+ const entry = JSON.parse(line);
392
+ const parsed = parseEntry(entry);
393
+ if (parsed)
394
+ messages.push(...parsed);
395
+ }
396
+ catch {
397
+ // Skip malformed lines
398
+ }
399
+ }
400
+ }
401
+ catch (error) {
402
+ logger_1.logger.error('Error reading session messages:', error);
403
+ }
404
+ return messages;
405
+ }
406
+ /**
407
+ * Get a page of session messages, reading from the end of the file (newest first).
408
+ * This enables IM-style pagination where the latest messages are loaded first.
409
+ *
410
+ * @param workspace - The workspace directory name
411
+ * @param sessionId - The session ID
412
+ * @param limit - Maximum number of messages to return (default 50)
413
+ * @param beforeIndex - Only return messages with index < beforeIndex (for pagination)
414
+ * @returns A page of messages in chronological order (oldest first within the page)
415
+ */
416
+ async function getSessionMessagesPage(workspace, sessionId, limit = 50, beforeIndex) {
417
+ const filePath = path.join(PROJECTS_DIR, workspace, `${sessionId}.jsonl`);
418
+ try {
419
+ // Read all messages from file
420
+ const allMessages = [];
421
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
422
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
423
+ for await (const line of rl) {
424
+ if (!line.trim())
425
+ continue;
426
+ try {
427
+ const entry = JSON.parse(line);
428
+ const parsed = parseEntry(entry);
429
+ if (parsed)
430
+ allMessages.push(...parsed);
431
+ }
432
+ catch {
433
+ // Skip malformed lines
434
+ }
435
+ }
436
+ const totalCount = allMessages.length;
437
+ // Determine the slice range
438
+ // beforeIndex is the index in the full array (0-based)
439
+ // If not provided, start from the end
440
+ const endIndex = beforeIndex !== undefined ? beforeIndex : totalCount;
441
+ const startIndex = Math.max(0, endIndex - limit);
442
+ // Slice and return in chronological order
443
+ const pageMessages = allMessages.slice(startIndex, endIndex);
444
+ return {
445
+ messages: pageMessages,
446
+ hasMore: startIndex > 0,
447
+ oldestIndex: startIndex,
448
+ totalCount,
449
+ };
450
+ }
451
+ catch (error) {
452
+ logger_1.logger.error('Error reading session messages page:', error);
453
+ return {
454
+ messages: [],
455
+ hasMore: false,
456
+ oldestIndex: 0,
457
+ totalCount: 0,
458
+ };
459
+ }
460
+ }
461
+ function parseEntry(entry) {
462
+ const timestamp = entry.timestamp || '';
463
+ const uuid = entry.uuid || '';
464
+ if (entry.type === 'user' && entry.message) {
465
+ const content = entry.message.content;
466
+ // content is string → real user message
467
+ if (typeof content === 'string') {
468
+ return [{ uuid, type: 'user', content, timestamp }];
469
+ }
470
+ // content is array → may contain tool_result blocks or text blocks
471
+ if (Array.isArray(content)) {
472
+ const results = [];
473
+ for (const block of content) {
474
+ if (block.type === 'tool_result') {
475
+ const text = typeof block.content === 'string'
476
+ ? block.content
477
+ : Array.isArray(block.content)
478
+ ? block.content
479
+ .filter((c) => c.type === 'text')
480
+ .map((c) => c.text)
481
+ .join('\n')
482
+ : 'Tool completed';
483
+ results.push({
484
+ uuid: block.tool_use_id || uuid + '_result',
485
+ type: 'tool_result',
486
+ content: text.substring(0, 500),
487
+ timestamp,
488
+ });
489
+ }
490
+ else if (block.type === 'text' && block.text) {
491
+ results.push({ uuid: uuid + '_text', type: 'user', content: block.text, timestamp });
492
+ }
493
+ }
494
+ return results.length > 0 ? results : null;
495
+ }
496
+ return null;
497
+ }
498
+ if (entry.type === 'assistant' && entry.message) {
499
+ const contentBlocks = entry.message.content;
500
+ if (!Array.isArray(contentBlocks))
501
+ return null;
502
+ const results = [];
503
+ for (const block of contentBlocks) {
504
+ if (block.type === 'text' && block.text) {
505
+ results.push({ uuid: uuid + '_text', type: 'assistant', content: block.text, timestamp });
506
+ }
507
+ else if (block.type === 'tool_use') {
508
+ results.push({
509
+ uuid: block.id || uuid + '_tool',
510
+ type: 'tool_use',
511
+ content: `Tool: ${block.name}`,
512
+ timestamp,
513
+ toolName: block.name,
514
+ toolInput: block.input,
515
+ });
516
+ }
517
+ else if (block.type === 'tool_result') {
518
+ const text = typeof block.content === 'string'
519
+ ? block.content
520
+ : Array.isArray(block.content)
521
+ ? block.content
522
+ .filter((c) => c.type === 'text')
523
+ .map((c) => c.text)
524
+ .join('\n')
525
+ : 'Tool completed';
526
+ results.push({
527
+ uuid: block.tool_use_id || uuid + '_result',
528
+ type: 'tool_result',
529
+ content: text.substring(0, 500),
530
+ timestamp,
531
+ });
532
+ }
533
+ }
534
+ return results.length > 0 ? results : null;
535
+ }
536
+ if (entry.type === 'result' && entry.result) {
537
+ // Tool result entries at top level
538
+ const content = typeof entry.result === 'string'
539
+ ? entry.result.substring(0, 500)
540
+ : JSON.stringify(entry.result).substring(0, 500);
541
+ return [{ uuid, type: 'tool_result', content, timestamp }];
542
+ }
543
+ return null;
544
+ }
545
+ /**
546
+ * List all subagents for a session
547
+ */
548
+ async function listSubagents(workspace, sessionId) {
549
+ const subagentsDir = path.join(PROJECTS_DIR, workspace, sessionId, 'subagents');
550
+ const subagents = [];
551
+ try {
552
+ const files = await fs.promises.readdir(subagentsDir);
553
+ for (const file of files) {
554
+ if (!file.endsWith('.jsonl'))
555
+ continue;
556
+ const filePath = path.join(subagentsDir, file);
557
+ const stat = await fs.promises.stat(filePath);
558
+ // Extract info from first line
559
+ const info = await extractSubagentInfo(filePath, sessionId, stat);
560
+ if (info) {
561
+ subagents.push(info);
562
+ }
563
+ }
564
+ // Sort by modified time (newest first)
565
+ subagents.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
566
+ return subagents;
567
+ }
568
+ catch (error) {
569
+ // Directory doesn't exist or other error
570
+ if (error.code !== 'ENOENT') {
571
+ logger_1.logger.error('Error listing subagents:', error);
572
+ }
573
+ return [];
574
+ }
575
+ }
576
+ async function extractSubagentInfo(filePath, parentSessionId, stat) {
577
+ try {
578
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
579
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
580
+ let agentId = '';
581
+ let slug = '';
582
+ let firstPrompt = '';
583
+ let messageCount = 0;
584
+ let created = '';
585
+ for await (const line of rl) {
586
+ if (!line.trim())
587
+ continue;
588
+ try {
589
+ const entry = JSON.parse(line);
590
+ messageCount++;
591
+ // Get agent info from first entry
592
+ if (!agentId && entry.agentId) {
593
+ agentId = entry.agentId;
594
+ slug = entry.slug || '';
595
+ created = entry.timestamp || '';
596
+ }
597
+ // Get first user prompt
598
+ if (!firstPrompt && entry.type === 'user' && entry.message) {
599
+ const content = entry.message.content;
600
+ if (typeof content === 'string') {
601
+ firstPrompt = content.substring(0, 200);
602
+ }
603
+ }
604
+ // Stop after reading enough
605
+ if (agentId && firstPrompt && messageCount >= 20) {
606
+ rl.close();
607
+ break;
608
+ }
609
+ }
610
+ catch {
611
+ // Skip malformed lines
612
+ }
613
+ }
614
+ if (!agentId)
615
+ return null;
616
+ return {
617
+ agentId,
618
+ slug,
619
+ filePath,
620
+ messageCount,
621
+ created: created || stat.birthtime.toISOString(),
622
+ modified: stat.mtime.toISOString(),
623
+ firstPrompt: firstPrompt || 'No prompt',
624
+ parentSessionId,
625
+ };
626
+ }
627
+ catch {
628
+ return null;
629
+ }
630
+ }
631
+ /**
632
+ * Get messages from a subagent
633
+ */
634
+ async function getSubagentMessages(workspace, sessionId, agentId) {
635
+ const subagentsDir = path.join(PROJECTS_DIR, workspace, sessionId, 'subagents');
636
+ const messages = [];
637
+ try {
638
+ const files = await fs.promises.readdir(subagentsDir);
639
+ // Find the file that matches this agentId
640
+ const agentFile = files.find(f => f.includes(agentId) && f.endsWith('.jsonl'));
641
+ if (!agentFile) {
642
+ logger_1.logger.warn(`Subagent file not found for agentId: ${agentId}`);
643
+ return [];
644
+ }
645
+ const filePath = path.join(subagentsDir, agentFile);
646
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
647
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
648
+ for await (const line of rl) {
649
+ if (!line.trim())
650
+ continue;
651
+ try {
652
+ const entry = JSON.parse(line);
653
+ const parsed = parseEntry(entry);
654
+ if (parsed)
655
+ messages.push(...parsed);
656
+ }
657
+ catch {
658
+ // Skip malformed lines
659
+ }
660
+ }
661
+ }
662
+ catch (error) {
663
+ logger_1.logger.error('Error reading subagent messages:', error);
664
+ }
665
+ return messages;
666
+ }
667
+ /**
668
+ * List tool result files for a session
669
+ */
670
+ async function listToolResults(workspace, sessionId) {
671
+ const toolResultsDir = path.join(PROJECTS_DIR, workspace, sessionId, 'tool-results');
672
+ const results = [];
673
+ try {
674
+ const files = await fs.promises.readdir(toolResultsDir);
675
+ for (const file of files) {
676
+ if (!file.endsWith('.txt'))
677
+ continue;
678
+ const filePath = path.join(toolResultsDir, file);
679
+ const stat = await fs.promises.stat(filePath);
680
+ // Extract tool_use_id from filename (e.g., toolu_018uDQVKJXngdcuQvtx35fRV.txt)
681
+ const toolUseId = file.replace('.txt', '');
682
+ results.push({
683
+ toolUseId,
684
+ filePath,
685
+ size: stat.size,
686
+ });
687
+ }
688
+ return results;
689
+ }
690
+ catch (error) {
691
+ if (error.code !== 'ENOENT') {
692
+ logger_1.logger.error('Error listing tool results:', error);
693
+ }
694
+ return [];
695
+ }
696
+ }
697
+ /**
698
+ * Read content of a tool result file
699
+ */
700
+ async function getToolResultContent(workspace, sessionId, toolUseId, maxSize = 50000) {
701
+ const filePath = path.join(PROJECTS_DIR, workspace, sessionId, 'tool-results', `${toolUseId}.txt`);
702
+ try {
703
+ const stat = await fs.promises.stat(filePath);
704
+ if (stat.size <= maxSize) {
705
+ return await fs.promises.readFile(filePath, 'utf-8');
706
+ }
707
+ // If file is too large, read only the beginning
708
+ const buffer = Buffer.alloc(maxSize);
709
+ const fd = await fs.promises.open(filePath, 'r');
710
+ await fd.read(buffer, 0, maxSize, 0);
711
+ await fd.close();
712
+ return buffer.toString('utf-8') + `\n\n... [truncated, total size: ${stat.size} bytes]`;
713
+ }
714
+ catch (error) {
715
+ logger_1.logger.error('Error reading tool result:', error);
716
+ return '';
717
+ }
718
+ }
719
+ /**
720
+ * Get session folder info (subagents count, tool-results count)
721
+ */
722
+ async function getSessionFolderInfo(workspace, sessionId) {
723
+ const sessionDir = path.join(PROJECTS_DIR, workspace, sessionId);
724
+ let subagentCount = 0;
725
+ let toolResultCount = 0;
726
+ try {
727
+ const subagentsDir = path.join(sessionDir, 'subagents');
728
+ const files = await fs.promises.readdir(subagentsDir);
729
+ subagentCount = files.filter(f => f.endsWith('.jsonl')).length;
730
+ }
731
+ catch {
732
+ // Directory doesn't exist
733
+ }
734
+ try {
735
+ const toolResultsDir = path.join(sessionDir, 'tool-results');
736
+ const files = await fs.promises.readdir(toolResultsDir);
737
+ toolResultCount = files.filter(f => f.endsWith('.txt')).length;
738
+ }
739
+ catch {
740
+ // Directory doesn't exist
741
+ }
742
+ return { subagentCount, toolResultCount };
743
+ }