renote-server 1.0.1
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/dist/__tests__/auth.test.js +49 -0
- package/dist/__tests__/watcher.test.js +58 -0
- package/dist/claude/sessionBrowser.js +689 -0
- package/dist/claude/watcher.js +242 -0
- package/dist/config.js +17 -0
- package/dist/files/browser.js +127 -0
- package/dist/files/reader.js +159 -0
- package/dist/files/search.js +124 -0
- package/dist/git/gitHandler.js +95 -0
- package/dist/git/gitService.js +237 -0
- package/dist/git/index.js +8 -0
- package/dist/http/server.js +28 -0
- package/dist/index.js +77 -0
- package/dist/ssh/index.js +9 -0
- package/dist/ssh/sshHandler.js +205 -0
- package/dist/ssh/sshManager.js +329 -0
- package/dist/terminal/index.js +11 -0
- package/dist/terminal/localTerminalHandler.js +144 -0
- package/dist/terminal/localTerminalManager.js +465 -0
- package/dist/terminal/terminalWebSocket.js +128 -0
- package/dist/types.js +2 -0
- package/dist/utils/logger.js +42 -0
- package/dist/websocket/auth.js +18 -0
- package/dist/websocket/server.js +512 -0
- package/package.json +64 -0
|
@@ -0,0 +1,689 @@
|
|
|
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
|
+
async function listWorkspaces() {
|
|
167
|
+
try {
|
|
168
|
+
const entries = await fs.promises.readdir(PROJECTS_DIR, { withFileTypes: true });
|
|
169
|
+
const workspaces = [];
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (!entry.isDirectory())
|
|
172
|
+
continue;
|
|
173
|
+
const workspaceDir = path.join(PROJECTS_DIR, entry.name);
|
|
174
|
+
const indexPath = path.join(workspaceDir, 'sessions-index.json');
|
|
175
|
+
let indexCount = 0;
|
|
176
|
+
let lastModified = 0;
|
|
177
|
+
// Read sessions-index.json if available
|
|
178
|
+
try {
|
|
179
|
+
const raw = await fs.promises.readFile(indexPath, 'utf-8');
|
|
180
|
+
const index = JSON.parse(raw);
|
|
181
|
+
const sessions = index.entries || [];
|
|
182
|
+
indexCount = sessions.length;
|
|
183
|
+
for (const s of sessions) {
|
|
184
|
+
const mtime = s.fileMtime || new Date(s.modified).getTime() || 0;
|
|
185
|
+
if (mtime > lastModified)
|
|
186
|
+
lastModified = mtime;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// No sessions-index.json or parse error, continue with file scan
|
|
191
|
+
}
|
|
192
|
+
// Also scan for .jsonl files to get accurate count
|
|
193
|
+
let jsonlCount = 0;
|
|
194
|
+
try {
|
|
195
|
+
const files = await fs.promises.readdir(workspaceDir);
|
|
196
|
+
jsonlCount = files.filter(f => f.endsWith('.jsonl')).length;
|
|
197
|
+
// Update lastModified from .jsonl files if needed
|
|
198
|
+
if (lastModified === 0 && jsonlCount > 0) {
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
if (!file.endsWith('.jsonl'))
|
|
201
|
+
continue;
|
|
202
|
+
try {
|
|
203
|
+
const stat = await fs.promises.stat(path.join(workspaceDir, file));
|
|
204
|
+
const mtime = stat.mtimeMs;
|
|
205
|
+
if (mtime > lastModified)
|
|
206
|
+
lastModified = mtime;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Skip files we can't stat
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Can't read directory, use index count
|
|
216
|
+
}
|
|
217
|
+
// Use the larger of the two counts to ensure accuracy
|
|
218
|
+
const sessionCount = Math.max(indexCount, jsonlCount);
|
|
219
|
+
// Only add workspace if it has sessions
|
|
220
|
+
if (sessionCount > 0) {
|
|
221
|
+
workspaces.push({
|
|
222
|
+
dirName: entry.name,
|
|
223
|
+
displayPath: decodeWorkspacePath(entry.name),
|
|
224
|
+
sessionCount,
|
|
225
|
+
lastModified,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
workspaces.sort((a, b) => b.lastModified - a.lastModified);
|
|
230
|
+
return workspaces;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
logger_1.logger.error('Error listing workspaces:', error);
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function listSessions(workspace) {
|
|
238
|
+
const workspaceDir = path.join(PROJECTS_DIR, workspace);
|
|
239
|
+
const indexPath = path.join(workspaceDir, 'sessions-index.json');
|
|
240
|
+
// Build a map from sessionId to SessionInfo from the index
|
|
241
|
+
const sessionMap = new Map();
|
|
242
|
+
try {
|
|
243
|
+
const raw = await fs.promises.readFile(indexPath, 'utf-8');
|
|
244
|
+
const index = JSON.parse(raw);
|
|
245
|
+
const entries = index.entries || [];
|
|
246
|
+
for (const e of entries) {
|
|
247
|
+
sessionMap.set(e.sessionId, {
|
|
248
|
+
sessionId: e.sessionId,
|
|
249
|
+
firstPrompt: e.firstPrompt || '',
|
|
250
|
+
summary: e.summary || '',
|
|
251
|
+
messageCount: e.messageCount || 0,
|
|
252
|
+
created: e.created || '',
|
|
253
|
+
modified: e.modified || '',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Index file doesn't exist or parse error, continue with file scan
|
|
259
|
+
}
|
|
260
|
+
// Scan for .jsonl files not in the index
|
|
261
|
+
try {
|
|
262
|
+
const files = await fs.promises.readdir(workspaceDir);
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
if (!file.endsWith('.jsonl'))
|
|
265
|
+
continue;
|
|
266
|
+
const sessionId = file.replace('.jsonl', '');
|
|
267
|
+
if (sessionMap.has(sessionId))
|
|
268
|
+
continue;
|
|
269
|
+
// Extract basic info from file
|
|
270
|
+
const filePath = path.join(workspaceDir, file);
|
|
271
|
+
const stat = await fs.promises.stat(filePath);
|
|
272
|
+
const info = await extractSessionInfo(filePath, sessionId, stat);
|
|
273
|
+
if (info)
|
|
274
|
+
sessionMap.set(sessionId, info);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
logger_1.logger.error('Error scanning workspace directory:', error);
|
|
279
|
+
}
|
|
280
|
+
const sessions = Array.from(sessionMap.values());
|
|
281
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
282
|
+
return sessions;
|
|
283
|
+
}
|
|
284
|
+
async function extractSessionInfo(filePath, sessionId, stat) {
|
|
285
|
+
try {
|
|
286
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
287
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
288
|
+
let firstPrompt = '';
|
|
289
|
+
let messageCount = 0;
|
|
290
|
+
for await (const line of rl) {
|
|
291
|
+
if (!line.trim())
|
|
292
|
+
continue;
|
|
293
|
+
try {
|
|
294
|
+
const entry = JSON.parse(line);
|
|
295
|
+
if (entry.type === 'user' && entry.message) {
|
|
296
|
+
messageCount++;
|
|
297
|
+
if (!firstPrompt && typeof entry.message.content === 'string') {
|
|
298
|
+
firstPrompt = entry.message.content.substring(0, 200);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (entry.type === 'assistant') {
|
|
302
|
+
messageCount++;
|
|
303
|
+
}
|
|
304
|
+
// Stop after reading enough to get firstPrompt and some count
|
|
305
|
+
if (firstPrompt && messageCount >= 10) {
|
|
306
|
+
rl.close();
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Skip malformed lines
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
sessionId,
|
|
316
|
+
firstPrompt: firstPrompt || 'No prompt',
|
|
317
|
+
summary: '',
|
|
318
|
+
messageCount,
|
|
319
|
+
created: stat.birthtime.toISOString(),
|
|
320
|
+
modified: stat.mtime.toISOString(),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function getSessionMessages(workspace, sessionId) {
|
|
328
|
+
const filePath = path.join(PROJECTS_DIR, workspace, `${sessionId}.jsonl`);
|
|
329
|
+
const messages = [];
|
|
330
|
+
try {
|
|
331
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
332
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
333
|
+
for await (const line of rl) {
|
|
334
|
+
if (!line.trim())
|
|
335
|
+
continue;
|
|
336
|
+
try {
|
|
337
|
+
const entry = JSON.parse(line);
|
|
338
|
+
const parsed = parseEntry(entry);
|
|
339
|
+
if (parsed)
|
|
340
|
+
messages.push(...parsed);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Skip malformed lines
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
logger_1.logger.error('Error reading session messages:', error);
|
|
349
|
+
}
|
|
350
|
+
return messages;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get a page of session messages, reading from the end of the file (newest first).
|
|
354
|
+
* This enables IM-style pagination where the latest messages are loaded first.
|
|
355
|
+
*
|
|
356
|
+
* @param workspace - The workspace directory name
|
|
357
|
+
* @param sessionId - The session ID
|
|
358
|
+
* @param limit - Maximum number of messages to return (default 50)
|
|
359
|
+
* @param beforeIndex - Only return messages with index < beforeIndex (for pagination)
|
|
360
|
+
* @returns A page of messages in chronological order (oldest first within the page)
|
|
361
|
+
*/
|
|
362
|
+
async function getSessionMessagesPage(workspace, sessionId, limit = 50, beforeIndex) {
|
|
363
|
+
const filePath = path.join(PROJECTS_DIR, workspace, `${sessionId}.jsonl`);
|
|
364
|
+
try {
|
|
365
|
+
// Read all messages from file
|
|
366
|
+
const allMessages = [];
|
|
367
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
368
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
369
|
+
for await (const line of rl) {
|
|
370
|
+
if (!line.trim())
|
|
371
|
+
continue;
|
|
372
|
+
try {
|
|
373
|
+
const entry = JSON.parse(line);
|
|
374
|
+
const parsed = parseEntry(entry);
|
|
375
|
+
if (parsed)
|
|
376
|
+
allMessages.push(...parsed);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// Skip malformed lines
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const totalCount = allMessages.length;
|
|
383
|
+
// Determine the slice range
|
|
384
|
+
// beforeIndex is the index in the full array (0-based)
|
|
385
|
+
// If not provided, start from the end
|
|
386
|
+
const endIndex = beforeIndex !== undefined ? beforeIndex : totalCount;
|
|
387
|
+
const startIndex = Math.max(0, endIndex - limit);
|
|
388
|
+
// Slice and return in chronological order
|
|
389
|
+
const pageMessages = allMessages.slice(startIndex, endIndex);
|
|
390
|
+
return {
|
|
391
|
+
messages: pageMessages,
|
|
392
|
+
hasMore: startIndex > 0,
|
|
393
|
+
oldestIndex: startIndex,
|
|
394
|
+
totalCount,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
logger_1.logger.error('Error reading session messages page:', error);
|
|
399
|
+
return {
|
|
400
|
+
messages: [],
|
|
401
|
+
hasMore: false,
|
|
402
|
+
oldestIndex: 0,
|
|
403
|
+
totalCount: 0,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function parseEntry(entry) {
|
|
408
|
+
const timestamp = entry.timestamp || '';
|
|
409
|
+
const uuid = entry.uuid || '';
|
|
410
|
+
if (entry.type === 'user' && entry.message) {
|
|
411
|
+
const content = entry.message.content;
|
|
412
|
+
// content is string → real user message
|
|
413
|
+
if (typeof content === 'string') {
|
|
414
|
+
return [{ uuid, type: 'user', content, timestamp }];
|
|
415
|
+
}
|
|
416
|
+
// content is array → may contain tool_result blocks or text blocks
|
|
417
|
+
if (Array.isArray(content)) {
|
|
418
|
+
const results = [];
|
|
419
|
+
for (const block of content) {
|
|
420
|
+
if (block.type === 'tool_result') {
|
|
421
|
+
const text = typeof block.content === 'string'
|
|
422
|
+
? block.content
|
|
423
|
+
: Array.isArray(block.content)
|
|
424
|
+
? block.content
|
|
425
|
+
.filter((c) => c.type === 'text')
|
|
426
|
+
.map((c) => c.text)
|
|
427
|
+
.join('\n')
|
|
428
|
+
: 'Tool completed';
|
|
429
|
+
results.push({
|
|
430
|
+
uuid: block.tool_use_id || uuid + '_result',
|
|
431
|
+
type: 'tool_result',
|
|
432
|
+
content: text.substring(0, 500),
|
|
433
|
+
timestamp,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
else if (block.type === 'text' && block.text) {
|
|
437
|
+
results.push({ uuid: uuid + '_text', type: 'user', content: block.text, timestamp });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return results.length > 0 ? results : null;
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (entry.type === 'assistant' && entry.message) {
|
|
445
|
+
const contentBlocks = entry.message.content;
|
|
446
|
+
if (!Array.isArray(contentBlocks))
|
|
447
|
+
return null;
|
|
448
|
+
const results = [];
|
|
449
|
+
for (const block of contentBlocks) {
|
|
450
|
+
if (block.type === 'text' && block.text) {
|
|
451
|
+
results.push({ uuid: uuid + '_text', type: 'assistant', content: block.text, timestamp });
|
|
452
|
+
}
|
|
453
|
+
else if (block.type === 'tool_use') {
|
|
454
|
+
results.push({
|
|
455
|
+
uuid: block.id || uuid + '_tool',
|
|
456
|
+
type: 'tool_use',
|
|
457
|
+
content: `Tool: ${block.name}`,
|
|
458
|
+
timestamp,
|
|
459
|
+
toolName: block.name,
|
|
460
|
+
toolInput: block.input,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
else if (block.type === 'tool_result') {
|
|
464
|
+
const text = typeof block.content === 'string'
|
|
465
|
+
? block.content
|
|
466
|
+
: Array.isArray(block.content)
|
|
467
|
+
? block.content
|
|
468
|
+
.filter((c) => c.type === 'text')
|
|
469
|
+
.map((c) => c.text)
|
|
470
|
+
.join('\n')
|
|
471
|
+
: 'Tool completed';
|
|
472
|
+
results.push({
|
|
473
|
+
uuid: block.tool_use_id || uuid + '_result',
|
|
474
|
+
type: 'tool_result',
|
|
475
|
+
content: text.substring(0, 500),
|
|
476
|
+
timestamp,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return results.length > 0 ? results : null;
|
|
481
|
+
}
|
|
482
|
+
if (entry.type === 'result' && entry.result) {
|
|
483
|
+
// Tool result entries at top level
|
|
484
|
+
const content = typeof entry.result === 'string'
|
|
485
|
+
? entry.result.substring(0, 500)
|
|
486
|
+
: JSON.stringify(entry.result).substring(0, 500);
|
|
487
|
+
return [{ uuid, type: 'tool_result', content, timestamp }];
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* List all subagents for a session
|
|
493
|
+
*/
|
|
494
|
+
async function listSubagents(workspace, sessionId) {
|
|
495
|
+
const subagentsDir = path.join(PROJECTS_DIR, workspace, sessionId, 'subagents');
|
|
496
|
+
const subagents = [];
|
|
497
|
+
try {
|
|
498
|
+
const files = await fs.promises.readdir(subagentsDir);
|
|
499
|
+
for (const file of files) {
|
|
500
|
+
if (!file.endsWith('.jsonl'))
|
|
501
|
+
continue;
|
|
502
|
+
const filePath = path.join(subagentsDir, file);
|
|
503
|
+
const stat = await fs.promises.stat(filePath);
|
|
504
|
+
// Extract info from first line
|
|
505
|
+
const info = await extractSubagentInfo(filePath, sessionId, stat);
|
|
506
|
+
if (info) {
|
|
507
|
+
subagents.push(info);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Sort by modified time (newest first)
|
|
511
|
+
subagents.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
512
|
+
return subagents;
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
// Directory doesn't exist or other error
|
|
516
|
+
if (error.code !== 'ENOENT') {
|
|
517
|
+
logger_1.logger.error('Error listing subagents:', error);
|
|
518
|
+
}
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function extractSubagentInfo(filePath, parentSessionId, stat) {
|
|
523
|
+
try {
|
|
524
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
525
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
526
|
+
let agentId = '';
|
|
527
|
+
let slug = '';
|
|
528
|
+
let firstPrompt = '';
|
|
529
|
+
let messageCount = 0;
|
|
530
|
+
let created = '';
|
|
531
|
+
for await (const line of rl) {
|
|
532
|
+
if (!line.trim())
|
|
533
|
+
continue;
|
|
534
|
+
try {
|
|
535
|
+
const entry = JSON.parse(line);
|
|
536
|
+
messageCount++;
|
|
537
|
+
// Get agent info from first entry
|
|
538
|
+
if (!agentId && entry.agentId) {
|
|
539
|
+
agentId = entry.agentId;
|
|
540
|
+
slug = entry.slug || '';
|
|
541
|
+
created = entry.timestamp || '';
|
|
542
|
+
}
|
|
543
|
+
// Get first user prompt
|
|
544
|
+
if (!firstPrompt && entry.type === 'user' && entry.message) {
|
|
545
|
+
const content = entry.message.content;
|
|
546
|
+
if (typeof content === 'string') {
|
|
547
|
+
firstPrompt = content.substring(0, 200);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Stop after reading enough
|
|
551
|
+
if (agentId && firstPrompt && messageCount >= 20) {
|
|
552
|
+
rl.close();
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// Skip malformed lines
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (!agentId)
|
|
561
|
+
return null;
|
|
562
|
+
return {
|
|
563
|
+
agentId,
|
|
564
|
+
slug,
|
|
565
|
+
filePath,
|
|
566
|
+
messageCount,
|
|
567
|
+
created: created || stat.birthtime.toISOString(),
|
|
568
|
+
modified: stat.mtime.toISOString(),
|
|
569
|
+
firstPrompt: firstPrompt || 'No prompt',
|
|
570
|
+
parentSessionId,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get messages from a subagent
|
|
579
|
+
*/
|
|
580
|
+
async function getSubagentMessages(workspace, sessionId, agentId) {
|
|
581
|
+
const subagentsDir = path.join(PROJECTS_DIR, workspace, sessionId, 'subagents');
|
|
582
|
+
const messages = [];
|
|
583
|
+
try {
|
|
584
|
+
const files = await fs.promises.readdir(subagentsDir);
|
|
585
|
+
// Find the file that matches this agentId
|
|
586
|
+
const agentFile = files.find(f => f.includes(agentId) && f.endsWith('.jsonl'));
|
|
587
|
+
if (!agentFile) {
|
|
588
|
+
logger_1.logger.warn(`Subagent file not found for agentId: ${agentId}`);
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
const filePath = path.join(subagentsDir, agentFile);
|
|
592
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
593
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
594
|
+
for await (const line of rl) {
|
|
595
|
+
if (!line.trim())
|
|
596
|
+
continue;
|
|
597
|
+
try {
|
|
598
|
+
const entry = JSON.parse(line);
|
|
599
|
+
const parsed = parseEntry(entry);
|
|
600
|
+
if (parsed)
|
|
601
|
+
messages.push(...parsed);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Skip malformed lines
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
logger_1.logger.error('Error reading subagent messages:', error);
|
|
610
|
+
}
|
|
611
|
+
return messages;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* List tool result files for a session
|
|
615
|
+
*/
|
|
616
|
+
async function listToolResults(workspace, sessionId) {
|
|
617
|
+
const toolResultsDir = path.join(PROJECTS_DIR, workspace, sessionId, 'tool-results');
|
|
618
|
+
const results = [];
|
|
619
|
+
try {
|
|
620
|
+
const files = await fs.promises.readdir(toolResultsDir);
|
|
621
|
+
for (const file of files) {
|
|
622
|
+
if (!file.endsWith('.txt'))
|
|
623
|
+
continue;
|
|
624
|
+
const filePath = path.join(toolResultsDir, file);
|
|
625
|
+
const stat = await fs.promises.stat(filePath);
|
|
626
|
+
// Extract tool_use_id from filename (e.g., toolu_018uDQVKJXngdcuQvtx35fRV.txt)
|
|
627
|
+
const toolUseId = file.replace('.txt', '');
|
|
628
|
+
results.push({
|
|
629
|
+
toolUseId,
|
|
630
|
+
filePath,
|
|
631
|
+
size: stat.size,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return results;
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
if (error.code !== 'ENOENT') {
|
|
638
|
+
logger_1.logger.error('Error listing tool results:', error);
|
|
639
|
+
}
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Read content of a tool result file
|
|
645
|
+
*/
|
|
646
|
+
async function getToolResultContent(workspace, sessionId, toolUseId, maxSize = 50000) {
|
|
647
|
+
const filePath = path.join(PROJECTS_DIR, workspace, sessionId, 'tool-results', `${toolUseId}.txt`);
|
|
648
|
+
try {
|
|
649
|
+
const stat = await fs.promises.stat(filePath);
|
|
650
|
+
if (stat.size <= maxSize) {
|
|
651
|
+
return await fs.promises.readFile(filePath, 'utf-8');
|
|
652
|
+
}
|
|
653
|
+
// If file is too large, read only the beginning
|
|
654
|
+
const buffer = Buffer.alloc(maxSize);
|
|
655
|
+
const fd = await fs.promises.open(filePath, 'r');
|
|
656
|
+
await fd.read(buffer, 0, maxSize, 0);
|
|
657
|
+
await fd.close();
|
|
658
|
+
return buffer.toString('utf-8') + `\n\n... [truncated, total size: ${stat.size} bytes]`;
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
logger_1.logger.error('Error reading tool result:', error);
|
|
662
|
+
return '';
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get session folder info (subagents count, tool-results count)
|
|
667
|
+
*/
|
|
668
|
+
async function getSessionFolderInfo(workspace, sessionId) {
|
|
669
|
+
const sessionDir = path.join(PROJECTS_DIR, workspace, sessionId);
|
|
670
|
+
let subagentCount = 0;
|
|
671
|
+
let toolResultCount = 0;
|
|
672
|
+
try {
|
|
673
|
+
const subagentsDir = path.join(sessionDir, 'subagents');
|
|
674
|
+
const files = await fs.promises.readdir(subagentsDir);
|
|
675
|
+
subagentCount = files.filter(f => f.endsWith('.jsonl')).length;
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
// Directory doesn't exist
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
const toolResultsDir = path.join(sessionDir, 'tool-results');
|
|
682
|
+
const files = await fs.promises.readdir(toolResultsDir);
|
|
683
|
+
toolResultCount = files.filter(f => f.endsWith('.txt')).length;
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
// Directory doesn't exist
|
|
687
|
+
}
|
|
688
|
+
return { subagentCount, toolResultCount };
|
|
689
|
+
}
|