agentstudio 0.1.0

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 (115) hide show
  1. package/.env +15 -0
  2. package/README.md +85 -0
  3. package/dist/bin/agentstudio.d.ts +3 -0
  4. package/dist/bin/agentstudio.d.ts.map +1 -0
  5. package/dist/bin/agentstudio.js +141 -0
  6. package/dist/bin/agentstudio.js.map +1 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +87 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/middleware/auth.d.ts +7 -0
  12. package/dist/middleware/auth.d.ts.map +1 -0
  13. package/dist/middleware/auth.js +21 -0
  14. package/dist/middleware/auth.js.map +1 -0
  15. package/dist/routes/agents.d.ts +4 -0
  16. package/dist/routes/agents.d.ts.map +1 -0
  17. package/dist/routes/agents.js +804 -0
  18. package/dist/routes/agents.js.map +1 -0
  19. package/dist/routes/auth.d.ts +4 -0
  20. package/dist/routes/auth.d.ts.map +1 -0
  21. package/dist/routes/auth.js +60 -0
  22. package/dist/routes/auth.js.map +1 -0
  23. package/dist/routes/files.d.ts +4 -0
  24. package/dist/routes/files.d.ts.map +1 -0
  25. package/dist/routes/files.js +301 -0
  26. package/dist/routes/files.js.map +1 -0
  27. package/dist/routes/mcp.d.ts +4 -0
  28. package/dist/routes/mcp.d.ts.map +1 -0
  29. package/dist/routes/mcp.js +652 -0
  30. package/dist/routes/mcp.js.map +1 -0
  31. package/dist/routes/media.d.ts +5 -0
  32. package/dist/routes/media.d.ts.map +1 -0
  33. package/dist/routes/media.js +117 -0
  34. package/dist/routes/media.js.map +1 -0
  35. package/dist/routes/slides.d.ts +4 -0
  36. package/dist/routes/slides.d.ts.map +1 -0
  37. package/dist/routes/slides.js +146 -0
  38. package/dist/routes/slides.js.map +1 -0
  39. package/dist/services/claudeSession.d.ts +83 -0
  40. package/dist/services/claudeSession.d.ts.map +1 -0
  41. package/dist/services/claudeSession.js +255 -0
  42. package/dist/services/claudeSession.js.map +1 -0
  43. package/dist/services/messageQueue.d.ts +31 -0
  44. package/dist/services/messageQueue.d.ts.map +1 -0
  45. package/dist/services/messageQueue.js +67 -0
  46. package/dist/services/messageQueue.js.map +1 -0
  47. package/dist/services/sessionManager.d.ts +132 -0
  48. package/dist/services/sessionManager.d.ts.map +1 -0
  49. package/dist/services/sessionManager.js +439 -0
  50. package/dist/services/sessionManager.js.map +1 -0
  51. package/dist/types/claude-history.d.ts +48 -0
  52. package/dist/types/claude-history.d.ts.map +1 -0
  53. package/dist/types/claude-history.js +2 -0
  54. package/dist/types/claude-history.js.map +1 -0
  55. package/dist/types/claude-versions.d.ts +31 -0
  56. package/dist/types/claude-versions.d.ts.map +1 -0
  57. package/dist/types/claude-versions.js +2 -0
  58. package/dist/types/claude-versions.js.map +1 -0
  59. package/dist/types/commands.d.ts +32 -0
  60. package/dist/types/commands.d.ts.map +1 -0
  61. package/dist/types/commands.js +2 -0
  62. package/dist/types/commands.js.map +1 -0
  63. package/dist/types/index.d.ts +81 -0
  64. package/dist/types/index.d.ts.map +1 -0
  65. package/dist/types/index.js +150 -0
  66. package/dist/types/index.js.map +1 -0
  67. package/dist/types/subagents.d.ts +88 -0
  68. package/dist/types/subagents.d.ts.map +1 -0
  69. package/dist/types/subagents.js +2 -0
  70. package/dist/types/subagents.js.map +1 -0
  71. package/dist/utils/agentStorage.d.ts +19 -0
  72. package/dist/utils/agentStorage.d.ts.map +1 -0
  73. package/dist/utils/agentStorage.js +110 -0
  74. package/dist/utils/agentStorage.js.map +1 -0
  75. package/dist/utils/claudeVersionStorage.d.ts +33 -0
  76. package/dist/utils/claudeVersionStorage.d.ts.map +1 -0
  77. package/dist/utils/claudeVersionStorage.js +168 -0
  78. package/dist/utils/claudeVersionStorage.js.map +1 -0
  79. package/dist/utils/jwt.d.ts +15 -0
  80. package/dist/utils/jwt.d.ts.map +1 -0
  81. package/dist/utils/jwt.js +28 -0
  82. package/dist/utils/jwt.js.map +1 -0
  83. package/dist/utils/projectMetadataStorage.d.ts +21 -0
  84. package/dist/utils/projectMetadataStorage.d.ts.map +1 -0
  85. package/dist/utils/projectMetadataStorage.js +68 -0
  86. package/dist/utils/projectMetadataStorage.js.map +1 -0
  87. package/frontend/dist/index.html +86 -0
  88. package/package.json +66 -0
  89. package/src/bin/agentstudio.ts +161 -0
  90. package/src/index.ts +100 -0
  91. package/src/middleware/auth.ts +26 -0
  92. package/src/routes/agents.ts +885 -0
  93. package/src/routes/auth.ts +73 -0
  94. package/src/routes/commands.ts.bak +441 -0
  95. package/src/routes/files.ts +352 -0
  96. package/src/routes/mcp.ts +751 -0
  97. package/src/routes/media.ts +140 -0
  98. package/src/routes/projects.ts.bak +601 -0
  99. package/src/routes/sessions.ts.bak +809 -0
  100. package/src/routes/settings.ts.bak +718 -0
  101. package/src/routes/slides.ts +170 -0
  102. package/src/routes/subagents.ts.bak +364 -0
  103. package/src/services/claudeSession.ts +293 -0
  104. package/src/services/messageQueue.ts +71 -0
  105. package/src/services/sessionManager.ts +532 -0
  106. package/src/types/claude-history.ts +50 -0
  107. package/src/types/claude-versions.ts +33 -0
  108. package/src/types/commands.ts +35 -0
  109. package/src/types/index.ts +248 -0
  110. package/src/types/subagents.ts +106 -0
  111. package/src/utils/agentStorage.ts +126 -0
  112. package/src/utils/claudeVersionStorage.ts +199 -0
  113. package/src/utils/jwt.ts +36 -0
  114. package/src/utils/projectMetadataStorage.ts +86 -0
  115. package/tsconfig.json +26 -0
@@ -0,0 +1,809 @@
1
+ import express from 'express';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import { AgentStorage } from '../utils/agentStorage.js';
6
+ import { ClaudeHistoryMessage, ClaudeHistorySession } from '../types/claude-history.js';
7
+ import { sessionManager } from '../services/sessionManager.js';
8
+
9
+ const router: express.Router = express.Router();
10
+
11
+ // Storage instances
12
+ const globalAgentStorage = new AgentStorage();
13
+
14
+ // Helper functions for reading Claude Code history from ~/.claude/projects
15
+ function convertProjectPathToClaudeFormat(projectPath: string): string {
16
+ // Convert path like /Users/kongjie/claude-code-projects/ppt-editor-project-2025-08-27-00-12
17
+ // to: -Users-kongjie-claude-code-projects-ppt-editor-project-2025-08-27-00-12
18
+ return projectPath.replace(/\//g, '-');
19
+ }
20
+
21
+ // Function to get AgentStorage instance for specific project directory
22
+ const getAgentStorageForRequest = (req: express.Request): AgentStorage => {
23
+ const projectPath = req.query.projectPath as string || req.body?.projectPath as string;
24
+ const workingDir = projectPath || process.cwd();
25
+ return new AgentStorage(workingDir);
26
+ };
27
+
28
+ // Process compact context messages - detect and convert the 4-message or 5-message pattern
29
+ function processCompactContextMessages(messages: ClaudeHistoryMessage[]): ClaudeHistoryMessage[] {
30
+ const processedMessages: ClaudeHistoryMessage[] = [];
31
+ let i = 0;
32
+
33
+ while (i < messages.length) {
34
+ const currentMsg = messages[i];
35
+
36
+ // Case 1a: New format (5-message pattern) - Check for compact_boundary message first
37
+ if ((currentMsg as any).type === 'system' &&
38
+ (currentMsg as any).subtype === 'compact_boundary' &&
39
+ currentMsg.parentUuid === null &&
40
+ i + 4 < messages.length) {
41
+
42
+ const summaryMsg = messages[i + 1];
43
+ const metaMsg = messages[i + 2];
44
+ const commandMsg = messages[i + 3];
45
+ const outputMsg = messages[i + 4];
46
+
47
+ // Verify this is the new 5-message compact pattern
48
+ if (summaryMsg.isCompactSummary &&
49
+ summaryMsg.parentUuid === currentMsg.uuid &&
50
+ metaMsg.isMeta === true &&
51
+ commandMsg.type === 'user' &&
52
+ commandMsg.message?.content &&
53
+ typeof commandMsg.message.content === 'string' &&
54
+ commandMsg.message.content.includes('<command-name>/compact</command-name>') &&
55
+ commandMsg.parentUuid === metaMsg.uuid &&
56
+ outputMsg.type === 'user' &&
57
+ outputMsg.message?.content &&
58
+ typeof outputMsg.message.content === 'string' &&
59
+ outputMsg.message.content.includes('<local-command-stdout>') &&
60
+ outputMsg.parentUuid === commandMsg.uuid) {
61
+
62
+ // Create synthetic user command message
63
+ const userCommandMessage: ClaudeHistoryMessage = {
64
+ type: 'user',
65
+ uuid: `synthetic_cmd_${commandMsg.uuid}`,
66
+ timestamp: commandMsg.timestamp,
67
+ sessionId: commandMsg.sessionId,
68
+ parentUuid: currentMsg.parentUuid,
69
+ message: {
70
+ role: 'user',
71
+ content: '/compact'
72
+ },
73
+ isCompactCommand: true
74
+ };
75
+
76
+ // Create synthetic AI response with compressed content
77
+ const aiResponseMessage: ClaudeHistoryMessage = {
78
+ type: 'assistant',
79
+ uuid: `synthetic_ai_${summaryMsg.uuid}`,
80
+ timestamp: summaryMsg.timestamp,
81
+ sessionId: summaryMsg.sessionId,
82
+ parentUuid: userCommandMessage.uuid,
83
+ message: {
84
+ role: 'assistant',
85
+ content: extractContentFromClaudeMessage(summaryMsg, messages) || '会话上下文已压缩'
86
+ },
87
+ isCompactSummary: true
88
+ };
89
+
90
+ processedMessages.push(userCommandMessage, aiResponseMessage);
91
+ i += 5; // Skip all 5 messages
92
+ continue;
93
+ }
94
+ }
95
+
96
+ // Case 1b: Old format (4-message pattern) - Check for isCompactSummary with parentUuid === null
97
+ if (currentMsg.isCompactSummary && currentMsg.parentUuid === null && i + 3 < messages.length) {
98
+ const metaMsg = messages[i + 1];
99
+ const commandMsg = messages[i + 2];
100
+ const outputMsg = messages[i + 3];
101
+
102
+ // Verify this is the old 4-message manual compact pattern
103
+ if (metaMsg.isMeta === true &&
104
+ commandMsg.type === 'user' &&
105
+ commandMsg.message?.content &&
106
+ typeof commandMsg.message.content === 'string' &&
107
+ commandMsg.message.content.includes('<command-name>/compact</command-name>') &&
108
+ commandMsg.parentUuid === metaMsg.uuid &&
109
+ outputMsg.type === 'user' &&
110
+ outputMsg.message?.content &&
111
+ typeof outputMsg.message.content === 'string' &&
112
+ outputMsg.message.content.includes('<local-command-stdout>') &&
113
+ outputMsg.parentUuid === commandMsg.uuid) {
114
+
115
+ // Create synthetic user command message
116
+ const userCommandMessage: ClaudeHistoryMessage = {
117
+ type: 'user',
118
+ uuid: `synthetic_cmd_${commandMsg.uuid}`,
119
+ timestamp: commandMsg.timestamp,
120
+ sessionId: commandMsg.sessionId,
121
+ parentUuid: currentMsg.parentUuid,
122
+ message: {
123
+ role: 'user',
124
+ content: '/compact'
125
+ },
126
+ isCompactCommand: true
127
+ };
128
+
129
+ // Create synthetic AI response with compressed content
130
+ const aiResponseMessage: ClaudeHistoryMessage = {
131
+ type: 'assistant',
132
+ uuid: `synthetic_ai_${currentMsg.uuid}`,
133
+ timestamp: currentMsg.timestamp,
134
+ sessionId: currentMsg.sessionId,
135
+ parentUuid: userCommandMessage.uuid,
136
+ message: {
137
+ role: 'assistant',
138
+ content: extractContentFromClaudeMessage(currentMsg, messages) || '会话上下文已压缩'
139
+ },
140
+ isCompactSummary: true
141
+ };
142
+
143
+ processedMessages.push(userCommandMessage, aiResponseMessage);
144
+ i += 4; // Skip all 4 messages
145
+ continue;
146
+ }
147
+ }
148
+
149
+ // Case 2: Auto compact - Single message with isCompactSummary
150
+ if (currentMsg.isCompactSummary && currentMsg.parentUuid === null &&
151
+ !(i + 1 < messages.length && messages[i + 1].isMeta === true)) {
152
+
153
+ // Create synthetic AI response for auto-compressed content
154
+ const aiResponseMessage: ClaudeHistoryMessage = {
155
+ type: 'assistant',
156
+ uuid: `synthetic_auto_${currentMsg.uuid}`,
157
+ timestamp: currentMsg.timestamp,
158
+ sessionId: currentMsg.sessionId,
159
+ parentUuid: currentMsg.parentUuid,
160
+ message: {
161
+ role: 'assistant',
162
+ content: extractContentFromClaudeMessage(currentMsg, messages) || '会话上下文已自动压缩'
163
+ },
164
+ isCompactSummary: true
165
+ };
166
+
167
+ processedMessages.push(aiResponseMessage);
168
+ i++;
169
+ continue;
170
+ }
171
+
172
+ // Regular message - pass through
173
+ processedMessages.push(currentMsg);
174
+ i++;
175
+ }
176
+
177
+ return processedMessages;
178
+ }
179
+
180
+ function readClaudeHistorySessions(projectPath: string): ClaudeHistorySession[] {
181
+ try {
182
+ const claudeProjectPath = convertProjectPathToClaudeFormat(projectPath);
183
+ const historyDir = path.join(os.homedir(), '.claude', 'projects', claudeProjectPath);
184
+
185
+ if (!fs.existsSync(historyDir)) {
186
+ console.log('Claude history directory not found:', historyDir);
187
+ return [];
188
+ }
189
+
190
+ const jsonlFiles = fs.readdirSync(historyDir)
191
+ .filter(file => file.endsWith('.jsonl'))
192
+ .filter(file => !file.startsWith('.'));
193
+
194
+ const sessions: ClaudeHistorySession[] = [];
195
+
196
+ for (const filename of jsonlFiles) {
197
+ const sessionId = filename.replace('.jsonl', '');
198
+ const filePath = path.join(historyDir, filename);
199
+
200
+ try {
201
+ const content = fs.readFileSync(filePath, 'utf-8');
202
+ const lines = content.trim().split('\n').filter(line => line.trim());
203
+
204
+ if (lines.length === 0) continue;
205
+
206
+ const messages: ClaudeHistoryMessage[] = lines.map(line => JSON.parse(line));
207
+
208
+ // Find summary message for session title
209
+ const summaryMessage = messages.find(msg => msg.type === 'summary');
210
+ const title = summaryMessage?.summary || `会话 ${sessionId.slice(0, 8)}`;
211
+
212
+ // Process compact context messages before filtering
213
+ const processedMessages = processCompactContextMessages(messages);
214
+
215
+ // Filter user and assistant messages, but exclude tool_result-only user messages, isMeta messages,
216
+ const conversationMessages = processedMessages.filter(msg => {
217
+ // Filter out isMeta messages (rule 1)
218
+ if ((msg as any).isMeta === true) {
219
+ return false;
220
+ }
221
+
222
+ // Filter out /clear command messages and its related output messages
223
+ if (msg.type === 'user' && msg.message?.content && typeof msg.message.content === 'string') {
224
+ // Check for /clear command format
225
+ if (msg.message.content.includes('<command-name>/clear</command-name>')) {
226
+ return false;
227
+ }
228
+ // Check for /clear command's output (local-command-stdout that follows /clear command)
229
+ if (msg.message.content.includes('<local-command-stdout></local-command-stdout>') &&
230
+ msg.parentUuid) {
231
+ // Find the parent message to check if it was a /clear command
232
+ const parentMsg = messages.find(m => m.uuid === msg.parentUuid);
233
+ if (parentMsg && parentMsg.message?.content && typeof parentMsg.message.content === 'string' &&
234
+ parentMsg.message.content.includes('<command-name>/clear</command-name>')) {
235
+ return false;
236
+ }
237
+ }
238
+ }
239
+
240
+ if (msg.type === 'assistant') return true;
241
+ if (msg.type === 'user') {
242
+ // Check if this user message contains only tool_result
243
+ if (msg.message?.content && Array.isArray(msg.message.content)) {
244
+ const hasNonToolResult = msg.message.content.some((block: any) => block.type !== 'tool_result');
245
+ return hasNonToolResult; // Only include if it has content other than tool_result
246
+ }
247
+ // Include user messages with string content or no content array
248
+ return typeof msg.message?.content === 'string' || !msg.message?.content;
249
+ }
250
+ return false;
251
+ });
252
+
253
+ if (conversationMessages.length === 0) continue;
254
+
255
+ // Convert messages to our format and group consecutive assistant messages
256
+ const convertedMessages: any[] = [];
257
+ let i = 0;
258
+
259
+ while (i < conversationMessages.length) {
260
+ const msg = conversationMessages[i];
261
+
262
+ if (msg.type === 'user') {
263
+ // Check if this is a local-command-stdout message
264
+ const content = msg.message?.content;
265
+ if (typeof content === 'string' && content.includes('<local-command-stdout>')) {
266
+ // Extract content from local-command-stdout tag
267
+ const outputMatch = content.match(/<local-command-stdout>([^<]*)<\/local-command-stdout>/);
268
+ const displayOutput = outputMatch ? outputMatch[1] : '';
269
+
270
+ // Create AI response message
271
+ convertedMessages.push({
272
+ id: `msg_${convertedMessages.length}_${msg.uuid}`,
273
+ role: 'assistant',
274
+ content: displayOutput,
275
+ timestamp: new Date(msg.timestamp).getTime(),
276
+ messageParts: [{
277
+ id: `part_0_${msg.uuid}`,
278
+ type: 'text',
279
+ content: displayOutput,
280
+ order: 0
281
+ }]
282
+ });
283
+ } else {
284
+ // Regular user message
285
+ convertedMessages.push({
286
+ id: `msg_${convertedMessages.length}_${msg.uuid}`,
287
+ role: msg.message?.role || msg.type,
288
+ content: extractContentFromClaudeMessage(msg, messages),
289
+ timestamp: new Date(msg.timestamp).getTime(),
290
+ messageParts: convertClaudeMessageToMessageParts(msg, messages)
291
+ });
292
+ }
293
+ i++;
294
+ } else if (msg.type === 'assistant') {
295
+ // Find all consecutive assistant messages and combine them
296
+ const assistantMessages = [msg];
297
+ let j = i + 1;
298
+
299
+ // Collect all consecutive assistant messages
300
+ while (j < conversationMessages.length && conversationMessages[j].type === 'assistant') {
301
+ assistantMessages.push(conversationMessages[j]);
302
+ j++;
303
+ }
304
+
305
+ // Create combined assistant message
306
+ const combinedMessage = {
307
+ id: `msg_${convertedMessages.length}_${msg.uuid}`,
308
+ role: 'assistant',
309
+ content: '',
310
+ timestamp: new Date(msg.timestamp).getTime(),
311
+ messageParts: [] as any[]
312
+ };
313
+
314
+ // Combine all assistant message parts
315
+ assistantMessages.forEach((assistantMsg) => {
316
+ const textContent = extractContentFromClaudeMessage(assistantMsg, messages);
317
+ const msgParts = convertClaudeMessageToMessageParts(assistantMsg, messages);
318
+
319
+ combinedMessage.content += textContent;
320
+ combinedMessage.messageParts.push(...msgParts.map(part => ({
321
+ ...part,
322
+ order: combinedMessage.messageParts.length + part.order
323
+ })));
324
+ });
325
+
326
+ convertedMessages.push(combinedMessage);
327
+ i = j; // Skip to next non-assistant message
328
+ } else {
329
+ i++;
330
+ }
331
+ }
332
+
333
+ // Process tool results - find tool_result messages and associate them with tool_use
334
+ for (let i = 0; i < messages.length; i++) {
335
+ const msg = messages[i];
336
+ if (msg.type === 'user' && msg.message?.content && Array.isArray(msg.message.content)) {
337
+ for (const block of msg.message.content) {
338
+ if (block.type === 'tool_result' && block.tool_use_id) {
339
+ // Find the assistant message that contains the matching tool_use
340
+ // Look backwards through conversation messages (not all messages)
341
+ for (let j = convertedMessages.length - 1; j >= 0; j--) {
342
+ const assistantMsg = convertedMessages[j];
343
+ if (assistantMsg && assistantMsg.role === 'assistant') {
344
+ // Find the tool part with matching claudeId
345
+ const toolPart = assistantMsg.messageParts.find((part: any) =>
346
+ part.type === 'tool' &&
347
+ part.toolData &&
348
+ part.toolData.claudeId === block.tool_use_id
349
+ );
350
+
351
+ if (toolPart && toolPart.toolData) {
352
+ toolPart.toolData.toolResult = typeof block.content === 'string'
353
+ ? block.content
354
+ : JSON.stringify(block.content);
355
+
356
+ // Check if the original message has toolUseResult (from Claude Code SDK)
357
+ if (msg.toolUseResult) {
358
+ toolPart.toolData.toolUseResult = msg.toolUseResult;
359
+ }
360
+
361
+ toolPart.toolData.isError = block.is_error || false;
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ // Get timestamps
372
+ const timestamps = conversationMessages
373
+ .map(msg => new Date(msg.timestamp).getTime())
374
+ .filter(t => !isNaN(t));
375
+
376
+ const createdAt = timestamps.length > 0 ? Math.min(...timestamps) : Date.now();
377
+ const lastUpdated = timestamps.length > 0 ? Math.max(...timestamps) : Date.now();
378
+
379
+ sessions.push({
380
+ id: sessionId,
381
+ title,
382
+ createdAt: new Date(createdAt).toISOString(),
383
+ lastUpdated: new Date(lastUpdated).toISOString(),
384
+ messages: convertedMessages
385
+ });
386
+
387
+ } catch (error) {
388
+ console.error(`Failed to parse Claude history file ${filename}:`, error);
389
+ continue;
390
+ }
391
+ }
392
+
393
+ return sessions.sort((a, b) => new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime());
394
+
395
+ } catch (error) {
396
+ console.error('Failed to read Claude history sessions:', error);
397
+ return [];
398
+ }
399
+ }
400
+
401
+ function extractContentFromClaudeMessage(msg: ClaudeHistoryMessage, allMessages: ClaudeHistoryMessage[] = []): string {
402
+ if (!msg.message?.content) return '';
403
+
404
+ // Handle both array and string content
405
+ if (typeof msg.message.content === 'string') {
406
+ const commandMatch = msg.message.content.match(/<command-name>(.+?)<\/command-name>/);
407
+ if (commandMatch) {
408
+ // Check for two different patterns:
409
+ // Pattern 1: Parent is meta message (3-message local-command pattern)
410
+ const parentMessage = msg.parentUuid ? allMessages.find(m => m.uuid === msg.parentUuid) : null;
411
+ const isMetaParent = parentMessage && (parentMessage as any).isMeta === true;
412
+
413
+ // Pattern 2: Child is meta message (2-message user-custom-command pattern)
414
+ const childMessage = allMessages.find(m => m.parentUuid === msg.uuid);
415
+ const isMetaChild = childMessage && (childMessage as any).isMeta === true;
416
+
417
+ if (isMetaParent) {
418
+ // 3-message pattern: return only command name
419
+ return commandMatch[1];
420
+ } else if (isMetaChild) {
421
+ // 2-message pattern: return command name + args
422
+ const argsMatch = msg.message.content.match(/<command-args>([^<]*)<\/command-args>/);
423
+ const args = argsMatch ? argsMatch[1].trim() : '';
424
+ return args ? `${commandMatch[1]} ${args}` : commandMatch[1];
425
+ }
426
+ }
427
+ return msg.message.content;
428
+ }
429
+
430
+ if (Array.isArray(msg.message.content)) {
431
+ return msg.message.content
432
+ .filter((block: any) => block.type === 'text' || block.type === 'thinking')
433
+ .map((block: any) => block.text || block.thinking || '')
434
+ .join('');
435
+ }
436
+
437
+ return '';
438
+ }
439
+
440
+ function convertClaudeMessageToMessageParts(msg: ClaudeHistoryMessage, allMessages: ClaudeHistoryMessage[] = []): any[] {
441
+ if (!msg.message?.content) return [];
442
+
443
+ // Handle compact command messages
444
+ if (msg.isCompactCommand) {
445
+ return [{
446
+ id: `part_0_${msg.uuid}`,
447
+ type: 'command',
448
+ content: '/compact',
449
+ order: 0
450
+ }];
451
+ }
452
+
453
+ // Handle compact summary messages
454
+ if (msg.isCompactSummary) {
455
+ return [{
456
+ id: `part_0_${msg.uuid}`,
457
+ type: 'compactSummary',
458
+ content: msg.message.content,
459
+ order: 0
460
+ }];
461
+ }
462
+
463
+ // Handle string content
464
+ if (typeof msg.message.content === 'string') {
465
+ // Rule 2: Check for command message format and create command-specific part
466
+ const commandMatch = msg.message.content.match(/<command-name>(.+?)<\/command-name>/);
467
+ if (commandMatch) {
468
+ // Check for two different patterns:
469
+ // Pattern 1: Parent is meta message (3-message local-command pattern)
470
+ const parentMessage = msg.parentUuid ? allMessages.find(m => m.uuid === msg.parentUuid) : null;
471
+ const isMetaParent = parentMessage && (parentMessage as any).isMeta === true;
472
+
473
+ // Pattern 2: Child is meta message (2-message user-custom-command pattern)
474
+ const childMessage = allMessages.find(m => m.parentUuid === msg.uuid);
475
+ const isMetaChild = childMessage && (childMessage as any).isMeta === true;
476
+
477
+ if (isMetaParent) {
478
+ // 3-message pattern: show only command name
479
+ return [{
480
+ id: `part_0_${msg.uuid}`,
481
+ type: 'command',
482
+ content: commandMatch[1], // Only the command name
483
+ originalContent: msg.message.content, // Keep original for reference
484
+ order: 0
485
+ }];
486
+ } else if (isMetaChild) {
487
+ // 2-message pattern: show command name + args
488
+ const argsMatch = msg.message.content.match(/<command-args>([^<]*)<\/command-args>/);
489
+ const args = argsMatch ? argsMatch[1].trim() : '';
490
+ const displayContent = args ? `${commandMatch[1]} ${args}` : commandMatch[1];
491
+
492
+ return [{
493
+ id: `part_0_${msg.uuid}`,
494
+ type: 'command',
495
+ content: displayContent, // Command name + args
496
+ originalContent: msg.message.content, // Keep original for reference
497
+ order: 0
498
+ }];
499
+ }
500
+ }
501
+
502
+ return [{
503
+ id: `part_0_${msg.uuid}`,
504
+ type: 'text',
505
+ content: msg.message.content,
506
+ order: 0
507
+ }];
508
+ }
509
+
510
+ // Handle array content
511
+ if (Array.isArray(msg.message.content)) {
512
+ return msg.message.content.map((block: any, index: number) => {
513
+ if (block.type === 'text') {
514
+ return {
515
+ id: `part_${index}_${msg.uuid}`,
516
+ type: 'text',
517
+ content: block.text,
518
+ order: index
519
+ };
520
+ } else if (block.type === 'tool_use') {
521
+ return {
522
+ id: `part_${index}_${msg.uuid}`,
523
+ type: 'tool',
524
+ toolData: {
525
+ id: `tool_${index}_${msg.uuid}`,
526
+ claudeId: block.id,
527
+ toolName: block.name,
528
+ toolInput: block.input || {},
529
+ toolResult: '', // Will be filled by tool_result if available
530
+ isExecuting: false, // Historical data is not executing
531
+ isError: false
532
+ },
533
+ order: index
534
+ };
535
+ } else if (block.type === 'tool_result') {
536
+ // Skip tool_result blocks as they will be merged with tool_use blocks
537
+ return null;
538
+ } else if (block.type === 'image') {
539
+ // Handle image content blocks
540
+ return {
541
+ id: `part_${index}_${msg.uuid}`,
542
+ type: 'image',
543
+ imageData: {
544
+ id: `img_${index}_${msg.uuid}`,
545
+ data: block.source?.data || '',
546
+ mediaType: block.source?.media_type || 'image/jpeg',
547
+ filename: `image_${index}.jpg` // Default filename since Claude history may not store original filename
548
+ },
549
+ order: index
550
+ };
551
+ } else if (block.type === 'thinking') {
552
+ // Handle thinking content blocks
553
+ return {
554
+ id: `part_${index}_${msg.uuid}`,
555
+ type: 'thinking',
556
+ content: block.thinking || '',
557
+ order: index
558
+ };
559
+ }
560
+ // Handle other content types
561
+ return {
562
+ id: `part_${index}_${msg.uuid}`,
563
+ type: 'unknown',
564
+ content: JSON.stringify(block),
565
+ order: index
566
+ };
567
+ }).filter((part: any) => part !== null);
568
+ }
569
+
570
+ return [];
571
+ }
572
+
573
+ // GET /api/sessions/_status - Get all sessions status (for monitoring)
574
+ router.get('/_status', (req, res) => {
575
+ try {
576
+ const sessionsInfo = sessionManager.getSessionsInfo();
577
+ res.json({
578
+ activeSessionCount: sessionManager.getActiveSessionCount(),
579
+ sessions: sessionsInfo
580
+ });
581
+ } catch (error) {
582
+ console.error('Failed to get sessions status:', error);
583
+ res.status(500).json({ error: 'Failed to get sessions status' });
584
+ }
585
+ });
586
+
587
+ // GET /api/sessions/:agentId - Get agent sessions
588
+ router.get('/:agentId', (req, res) => {
589
+ try {
590
+ const { agentId } = req.params;
591
+ const { search } = req.query;
592
+ const projectPath = req.query.projectPath as string;
593
+
594
+ // Verify agent exists
595
+ const agent = globalAgentStorage.getAgent(agentId);
596
+ if (!agent) {
597
+ return res.status(404).json({ error: 'Agent not found' });
598
+ }
599
+
600
+ let sessions: any[] = [];
601
+
602
+ // If projectPath is provided, read from Claude Code history
603
+ if (projectPath) {
604
+ console.log('Reading Claude history sessions for project:', projectPath);
605
+ const claudeSessions = readClaudeHistorySessions(projectPath);
606
+ sessions = claudeSessions.map(session => ({
607
+ id: session.id,
608
+ agentId: agentId, // Associate with current agent
609
+ title: session.title,
610
+ createdAt: session.createdAt,
611
+ lastUpdated: session.lastUpdated,
612
+ messageCount: session.messages.length
613
+ }));
614
+ } else {
615
+ // Use project-specific AgentStorage for sessions (existing behavior)
616
+ const agentStorage = getAgentStorageForRequest(req);
617
+ const agentSessions = agentStorage.getAgentSessions(agentId, search as string);
618
+ sessions = agentSessions.map(session => ({
619
+ id: session.id,
620
+ agentId: session.agentId,
621
+ title: session.title,
622
+ createdAt: session.createdAt,
623
+ lastUpdated: session.lastUpdated,
624
+ messageCount: session.messages.length
625
+ }));
626
+ }
627
+
628
+ // Apply search filter if provided
629
+ if (search && typeof search === 'string' && search.trim()) {
630
+ const searchTerm = search.trim().toLowerCase();
631
+ sessions = sessions.filter(session =>
632
+ session.title.toLowerCase().includes(searchTerm)
633
+ );
634
+ }
635
+
636
+ res.json({ sessions });
637
+ } catch (error) {
638
+ console.error('Failed to get agent sessions:', error);
639
+ res.status(500).json({ error: 'Failed to retrieve agent sessions' });
640
+ }
641
+ });
642
+
643
+ // GET /api/sessions/:agentId/:sessionId/messages - Get session messages
644
+ router.get('/:agentId/:sessionId/messages', (req, res) => {
645
+ try {
646
+ const { agentId, sessionId } = req.params;
647
+ const projectPath = req.query.projectPath as string;
648
+
649
+ let session: any = null;
650
+
651
+ // If projectPath is provided, read from Claude Code history
652
+ if (projectPath) {
653
+ console.log('Reading Claude history messages for session:', sessionId, 'in project:', projectPath);
654
+ const claudeSessions = readClaudeHistorySessions(projectPath);
655
+ session = claudeSessions.find(s => s.id === sessionId);
656
+
657
+ if (session) {
658
+ console.log('📨 Found session with', session.messages?.length || 0, 'messages');
659
+ console.log('📨 First few messages:', session.messages?.slice(0, 3).map((msg: any) => ({
660
+ role: msg.role,
661
+ hasMessageParts: !!msg.messageParts,
662
+ messagePartsCount: msg.messageParts?.length || 0,
663
+ content: msg.content?.slice(0, 50) + '...'
664
+ })));
665
+
666
+ // Add agentId to match expected format
667
+ session = {
668
+ ...session,
669
+ agentId: agentId
670
+ };
671
+ }
672
+ } else {
673
+ // Use project-specific AgentStorage for sessions (existing behavior)
674
+ const agentStorage = getAgentStorageForRequest(req);
675
+ session = agentStorage.getSession(agentId, sessionId);
676
+ }
677
+
678
+ if (!session) {
679
+ return res.status(404).json({ error: 'Session not found' });
680
+ }
681
+
682
+ res.json({
683
+ sessionId: session.id,
684
+ agentId: session.agentId,
685
+ title: session.title,
686
+ messages: session.messages
687
+ });
688
+ } catch (error) {
689
+ console.error('Failed to get session messages:', error);
690
+ res.status(500).json({ error: 'Failed to retrieve session messages' });
691
+ }
692
+ });
693
+
694
+ // POST /api/sessions/:agentId - Create new session
695
+ router.post('/:agentId', (req, res) => {
696
+ try {
697
+ const { agentId } = req.params;
698
+
699
+ // Verify agent exists
700
+ const agent = globalAgentStorage.getAgent(agentId);
701
+ if (!agent) {
702
+ return res.status(404).json({ error: 'Agent not found' });
703
+ }
704
+
705
+ // Use project-specific AgentStorage for sessions
706
+ const agentStorage = getAgentStorageForRequest(req);
707
+ const session = agentStorage.createSession(agentId, req.body.title);
708
+ res.json({ sessionId: session.id, session });
709
+ } catch (error) {
710
+ console.error('Failed to create agent session:', error);
711
+ res.status(500).json({ error: 'Failed to create agent session' });
712
+ }
713
+ });
714
+
715
+ // DELETE /api/sessions/:agentId/:sessionId - Delete session
716
+ router.delete('/:agentId/:sessionId', (req, res) => {
717
+ try {
718
+ const { agentId, sessionId } = req.params;
719
+
720
+ // Use project-specific AgentStorage for sessions
721
+ const agentStorage = getAgentStorageForRequest(req);
722
+ const deleted = agentStorage.deleteSession(agentId, sessionId);
723
+ res.json({ success: deleted });
724
+ } catch (error) {
725
+ console.error('Failed to delete agent session:', error);
726
+ res.status(500).json({ error: 'Failed to delete agent session' });
727
+ }
728
+ });
729
+
730
+ // POST /api/sessions/:agentId/:sessionId/heartbeat - Update session heartbeat
731
+ router.post('/:agentId/:sessionId/heartbeat', (req, res) => {
732
+ try {
733
+ const { sessionId } = req.params;
734
+
735
+ // Update heartbeat in SessionManager
736
+ const success = sessionManager.updateHeartbeat(sessionId);
737
+
738
+ if (success) {
739
+ res.json({
740
+ success: true,
741
+ timestamp: Date.now(),
742
+ message: 'Heartbeat updated successfully'
743
+ });
744
+ } else {
745
+ res.status(404).json({
746
+ success: false,
747
+ error: 'Session not found or not active'
748
+ });
749
+ }
750
+ } catch (error) {
751
+ console.error('Failed to update session heartbeat:', error);
752
+ res.status(500).json({
753
+ success: false,
754
+ error: 'Failed to update heartbeat'
755
+ });
756
+ }
757
+ });
758
+
759
+ // DELETE /api/sessions/:agentId/:sessionId/cleanup - Manual cleanup session
760
+ router.delete('/:agentId/:sessionId/cleanup', async (req, res) => {
761
+ try {
762
+ const { sessionId } = req.params;
763
+
764
+ // Manual cleanup session in SessionManager
765
+ const success = await sessionManager.manualCleanupSession(sessionId);
766
+
767
+ if (success) {
768
+ res.json({
769
+ success: true,
770
+ message: 'Session cleaned up successfully'
771
+ });
772
+ } else {
773
+ res.status(404).json({
774
+ success: false,
775
+ error: 'Session not found'
776
+ });
777
+ }
778
+ } catch (error) {
779
+ console.error('Failed to cleanup session:', error);
780
+ res.status(500).json({
781
+ success: false,
782
+ error: 'Failed to cleanup session'
783
+ });
784
+ }
785
+ });
786
+
787
+ // GET /api/sessions/:agentId/:sessionId/check - Check if session exists in SessionManager
788
+ router.get('/:agentId/:sessionId/check', (req, res) => {
789
+ try {
790
+ const { sessionId } = req.params;
791
+
792
+ // Check if session exists in SessionManager
793
+ const exists = sessionManager.hasActiveSession(sessionId);
794
+
795
+ res.json({
796
+ exists,
797
+ sessionId,
798
+ message: exists ? 'Session is active in SessionManager' : 'Session not found in SessionManager'
799
+ });
800
+ } catch (error) {
801
+ console.error('Failed to check session:', error);
802
+ res.status(500).json({
803
+ exists: false,
804
+ error: 'Failed to check session'
805
+ });
806
+ }
807
+ });
808
+
809
+ export default router;