@hexidecibel/companion 0.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.
Files changed (109) hide show
  1. package/dist/__tests__/task-parser.test.d.ts +2 -0
  2. package/dist/__tests__/task-parser.test.d.ts.map +1 -0
  3. package/dist/__tests__/task-parser.test.js +79 -0
  4. package/dist/__tests__/task-parser.test.js.map +1 -0
  5. package/dist/anthropic-usage.d.ts +5 -0
  6. package/dist/anthropic-usage.d.ts.map +1 -0
  7. package/dist/anthropic-usage.js +112 -0
  8. package/dist/anthropic-usage.js.map +1 -0
  9. package/dist/cert-generator.d.ts +15 -0
  10. package/dist/cert-generator.d.ts.map +1 -0
  11. package/dist/cert-generator.js +298 -0
  12. package/dist/cert-generator.js.map +1 -0
  13. package/dist/config.d.ts +4 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +122 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/encryption.d.ts +28 -0
  18. package/dist/encryption.d.ts.map +1 -0
  19. package/dist/encryption.js +95 -0
  20. package/dist/encryption.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +211 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/input-injector.d.ts +21 -0
  26. package/dist/input-injector.d.ts.map +1 -0
  27. package/dist/input-injector.js +126 -0
  28. package/dist/input-injector.js.map +1 -0
  29. package/dist/mdns.d.ts +11 -0
  30. package/dist/mdns.d.ts.map +1 -0
  31. package/dist/mdns.js +93 -0
  32. package/dist/mdns.js.map +1 -0
  33. package/dist/parser.d.ts +43 -0
  34. package/dist/parser.d.ts.map +1 -0
  35. package/dist/parser.js +800 -0
  36. package/dist/parser.js.map +1 -0
  37. package/dist/push.d.ts +38 -0
  38. package/dist/push.d.ts.map +1 -0
  39. package/dist/push.js +359 -0
  40. package/dist/push.js.map +1 -0
  41. package/dist/qr-server.d.ts +13 -0
  42. package/dist/qr-server.d.ts.map +1 -0
  43. package/dist/qr-server.js +421 -0
  44. package/dist/qr-server.js.map +1 -0
  45. package/dist/scaffold/generator.d.ts +11 -0
  46. package/dist/scaffold/generator.d.ts.map +1 -0
  47. package/dist/scaffold/generator.js +206 -0
  48. package/dist/scaffold/generator.js.map +1 -0
  49. package/dist/scaffold/templates/index.d.ts +5 -0
  50. package/dist/scaffold/templates/index.d.ts.map +1 -0
  51. package/dist/scaffold/templates/index.js +22 -0
  52. package/dist/scaffold/templates/index.js.map +1 -0
  53. package/dist/scaffold/templates/node-express.d.ts +3 -0
  54. package/dist/scaffold/templates/node-express.d.ts.map +1 -0
  55. package/dist/scaffold/templates/node-express.js +218 -0
  56. package/dist/scaffold/templates/node-express.js.map +1 -0
  57. package/dist/scaffold/templates/python-fastapi.d.ts +3 -0
  58. package/dist/scaffold/templates/python-fastapi.d.ts.map +1 -0
  59. package/dist/scaffold/templates/python-fastapi.js +302 -0
  60. package/dist/scaffold/templates/python-fastapi.js.map +1 -0
  61. package/dist/scaffold/templates/react-mui-website.d.ts +3 -0
  62. package/dist/scaffold/templates/react-mui-website.d.ts.map +1 -0
  63. package/dist/scaffold/templates/react-mui-website.js +405 -0
  64. package/dist/scaffold/templates/react-mui-website.js.map +1 -0
  65. package/dist/scaffold/templates/react-typescript.d.ts +3 -0
  66. package/dist/scaffold/templates/react-typescript.d.ts.map +1 -0
  67. package/dist/scaffold/templates/react-typescript.js +275 -0
  68. package/dist/scaffold/templates/react-typescript.js.map +1 -0
  69. package/dist/scaffold/types.d.ts +55 -0
  70. package/dist/scaffold/types.d.ts.map +1 -0
  71. package/dist/scaffold/types.js +3 -0
  72. package/dist/scaffold/types.js.map +1 -0
  73. package/dist/subagent-watcher.d.ts +24 -0
  74. package/dist/subagent-watcher.d.ts.map +1 -0
  75. package/dist/subagent-watcher.js +307 -0
  76. package/dist/subagent-watcher.js.map +1 -0
  77. package/dist/tls.d.ts +10 -0
  78. package/dist/tls.d.ts.map +1 -0
  79. package/dist/tls.js +77 -0
  80. package/dist/tls.js.map +1 -0
  81. package/dist/tmux-manager.d.ts +71 -0
  82. package/dist/tmux-manager.d.ts.map +1 -0
  83. package/dist/tmux-manager.js +243 -0
  84. package/dist/tmux-manager.js.map +1 -0
  85. package/dist/tool-config.d.ts +33 -0
  86. package/dist/tool-config.d.ts.map +1 -0
  87. package/dist/tool-config.js +211 -0
  88. package/dist/tool-config.js.map +1 -0
  89. package/dist/types.d.ts +218 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +3 -0
  92. package/dist/types.js.map +1 -0
  93. package/dist/watcher.d.ts +63 -0
  94. package/dist/watcher.d.ts.map +1 -0
  95. package/dist/watcher.js +596 -0
  96. package/dist/watcher.js.map +1 -0
  97. package/dist/watcher.test.d.ts +2 -0
  98. package/dist/watcher.test.d.ts.map +1 -0
  99. package/dist/watcher.test.js +110 -0
  100. package/dist/watcher.test.js.map +1 -0
  101. package/dist/websocket.d.ts +62 -0
  102. package/dist/websocket.d.ts.map +1 -0
  103. package/dist/websocket.js +1695 -0
  104. package/dist/websocket.js.map +1 -0
  105. package/package.json +71 -0
  106. package/scripts/build.sh +23 -0
  107. package/scripts/install-remote.sh +18 -0
  108. package/scripts/install.sh +558 -0
  109. package/scripts/uninstall.sh +113 -0
package/dist/parser.js ADDED
@@ -0,0 +1,800 @@
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.detectCurrentActivityFast = detectCurrentActivityFast;
37
+ exports.parseConversationFile = parseConversationFile;
38
+ exports.extractHighlights = extractHighlights;
39
+ exports.detectWaitingForInput = detectWaitingForInput;
40
+ exports.detectIdle = detectIdle;
41
+ exports.detectCurrentActivity = detectCurrentActivity;
42
+ exports.getRecentActivity = getRecentActivity;
43
+ exports.getSessionStatus = getSessionStatus;
44
+ exports.getPendingApprovalTools = getPendingApprovalTools;
45
+ exports.detectCompaction = detectCompaction;
46
+ exports.extractUsageFromFile = extractUsageFromFile;
47
+ exports.extractTasks = extractTasks;
48
+ const fs = __importStar(require("fs"));
49
+ const tool_config_1 = require("./tool-config");
50
+ const MAX_MESSAGES = 100; // Limit to most recent messages
51
+ // KNOWN_TOOLS alias for backward compatibility in this file
52
+ const KNOWN_TOOLS = tool_config_1.KNOWN_TOOL_NAMES;
53
+ // Rate-limit parser warnings: max one per key per 60s
54
+ const _warnedRecently = new Map();
55
+ function logParserWarning(type, details) {
56
+ const key = `${type}:${details.substring(0, 100)}`;
57
+ const now = Date.now();
58
+ const last = _warnedRecently.get(key);
59
+ if (last && now - last < 60000)
60
+ return;
61
+ _warnedRecently.set(key, now);
62
+ console.log(`[PARSER_WARN] ${type}: ${details}`);
63
+ }
64
+ /**
65
+ * Fast function to detect current activity by reading only the last few KB of a file.
66
+ * Much faster than parsing the entire conversation file.
67
+ * Tracks tool_result entries to avoid showing stale "pending" status for completed tools.
68
+ */
69
+ function detectCurrentActivityFast(filePath) {
70
+ if (!fs.existsSync(filePath)) {
71
+ return undefined;
72
+ }
73
+ try {
74
+ const stats = fs.statSync(filePath);
75
+ const fileSize = stats.size;
76
+ // Read last 32KB - enough to get recent messages
77
+ const readSize = Math.min(32 * 1024, fileSize);
78
+ const buffer = Buffer.alloc(readSize);
79
+ const fd = fs.openSync(filePath, 'r');
80
+ fs.readSync(fd, buffer, 0, readSize, Math.max(0, fileSize - readSize));
81
+ fs.closeSync(fd);
82
+ const tail = buffer.toString('utf-8');
83
+ const lines = tail.split('\n').filter(line => line.trim());
84
+ // Collect tool_result IDs from recent lines so we know which tools completed
85
+ const completedToolIds = new Set();
86
+ for (let i = lines.length - 1; i >= 0; i--) {
87
+ try {
88
+ const entry = JSON.parse(lines[i]);
89
+ if (entry.message?.content && Array.isArray(entry.message.content)) {
90
+ for (const block of entry.message.content) {
91
+ if (block.type === 'tool_result' && block.tool_use_id) {
92
+ completedToolIds.add(block.tool_use_id);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ }
101
+ // Walk backward to find the most recent meaningful entry
102
+ for (let i = lines.length - 1; i >= 0; i--) {
103
+ try {
104
+ const entry = JSON.parse(lines[i]);
105
+ if (entry.message?.role === 'user') {
106
+ return 'Processing...';
107
+ }
108
+ if (entry.message?.role === 'assistant' && entry.message.content) {
109
+ const entryContent = entry.message.content;
110
+ if (Array.isArray(entryContent)) {
111
+ // Find the last tool_use that hasn't been completed
112
+ for (let j = entryContent.length - 1; j >= 0; j--) {
113
+ const block = entryContent[j];
114
+ if (block.type === 'tool_use' && block.name && block.id) {
115
+ // Skip tools that already have results
116
+ if (completedToolIds.has(block.id)) {
117
+ continue;
118
+ }
119
+ // Warn about unknown tools
120
+ if (!(0, tool_config_1.isKnownTool)(block.name)) {
121
+ logParserWarning('unknown_tool', `Unrecognized tool: ${block.name}`);
122
+ }
123
+ // Check if this needs approval
124
+ if (tool_config_1.APPROVAL_TOOLS.includes(block.name)) {
125
+ const input = block.input;
126
+ if (block.name === 'Bash' && input?.command) {
127
+ const cmd = input.command.substring(0, 40);
128
+ return `Approve? ${cmd}${input.command.length > 40 ? '...' : ''}`;
129
+ }
130
+ if ((block.name === 'Edit' || block.name === 'Write') && input?.file_path) {
131
+ const fileName = input.file_path.split('/').pop() || input.file_path;
132
+ return `Approve ${block.name.toLowerCase()}: ${fileName}?`;
133
+ }
134
+ return `Approve ${block.name}?`;
135
+ }
136
+ return (0, tool_config_1.getToolDescription)(block.name);
137
+ }
138
+ }
139
+ }
140
+ return undefined; // Assistant message, all tools completed
141
+ }
142
+ }
143
+ catch {
144
+ continue;
145
+ }
146
+ }
147
+ return undefined;
148
+ }
149
+ catch (err) {
150
+ return undefined;
151
+ }
152
+ }
153
+ function parseConversationFile(filePath, limit = MAX_MESSAGES, preReadContent) {
154
+ const content = preReadContent ?? (fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '');
155
+ if (!content) {
156
+ return [];
157
+ }
158
+ const lines = content.split('\n').filter(line => line.trim());
159
+ // First pass: collect all tool results, start times, and completion times
160
+ const toolResults = new Map();
161
+ const toolStartTimes = new Map();
162
+ const toolCompleteTimes = new Map();
163
+ for (const line of lines) {
164
+ try {
165
+ const entry = JSON.parse(line);
166
+ const timestamp = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
167
+ if (entry.message?.content && Array.isArray(entry.message.content)) {
168
+ for (const block of entry.message.content) {
169
+ // Track tool_use start times
170
+ if (block.type === 'tool_use' && block.id) {
171
+ toolStartTimes.set(block.id, timestamp);
172
+ }
173
+ // Track tool_result completion times and outputs
174
+ if (block.type === 'tool_result' && block.tool_use_id) {
175
+ toolCompleteTimes.set(block.tool_use_id, timestamp);
176
+ // Extract output content - can be string or array of content blocks
177
+ let output = '';
178
+ if (typeof block.content === 'string') {
179
+ output = block.content;
180
+ }
181
+ else if (Array.isArray(block.content)) {
182
+ output = block.content
183
+ .filter(c => c.type === 'text' && c.text)
184
+ .map(c => c.text || '')
185
+ .join('\n');
186
+ }
187
+ toolResults.set(block.tool_use_id, output);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ catch {
193
+ // Skip malformed lines
194
+ }
195
+ }
196
+ const completedToolIds = new Set(toolResults.keys());
197
+ const messages = [];
198
+ // Process from the end to get most recent messages first
199
+ for (let i = lines.length - 1; i >= 0 && messages.length < limit * 2; i--) {
200
+ try {
201
+ const entry = JSON.parse(lines[i]);
202
+ if (entry.type === 'user' || entry.type === 'assistant') {
203
+ const message = parseEntry(entry, toolResults, toolStartTimes, toolCompleteTimes);
204
+ if (message) {
205
+ messages.unshift(message); // Add to beginning to maintain order
206
+ }
207
+ }
208
+ else if (entry.type && entry.type !== 'summary') {
209
+ logParserWarning('unknown_entry_type', `Unexpected JSONL entry type: ${entry.type}`);
210
+ }
211
+ }
212
+ catch {
213
+ // Skip malformed lines
214
+ }
215
+ }
216
+ // Return only the limit number of messages
217
+ return messages.slice(-limit);
218
+ }
219
+ function parseEntry(entry, toolResults, toolStartTimes, toolCompleteTimes) {
220
+ const message = entry.message;
221
+ if (!message)
222
+ return null;
223
+ let content = '';
224
+ const toolCalls = [];
225
+ let options;
226
+ let questions;
227
+ let isWaitingForChoice = false;
228
+ let multiSelect = false;
229
+ if (typeof message.content === 'string') {
230
+ content = message.content;
231
+ }
232
+ else if (Array.isArray(message.content)) {
233
+ for (const block of message.content) {
234
+ if (block.type === 'text' && block.text) {
235
+ content += block.text;
236
+ }
237
+ else if (block.type === 'tool_use') {
238
+ if (!block.name) {
239
+ logParserWarning('missing_tool_name', `tool_use block without name, id: ${block.id}`);
240
+ continue;
241
+ }
242
+ if (!KNOWN_TOOLS.has(block.name)) {
243
+ logParserWarning('unknown_tool', `Unrecognized tool in parseEntry: ${block.name}`);
244
+ }
245
+ const toolId = block.id || entry.uuid || '';
246
+ const output = toolResults.get(toolId);
247
+ const isPending = !output && output !== '';
248
+ const startedAt = toolStartTimes.get(toolId);
249
+ const completedAt = toolCompleteTimes.get(toolId);
250
+ toolCalls.push({
251
+ id: toolId,
252
+ name: block.name,
253
+ input: block.input || {},
254
+ output: output,
255
+ status: isPending ? 'pending' : 'completed',
256
+ startedAt,
257
+ completedAt,
258
+ });
259
+ // Extract options from AskUserQuestion tool (only if still pending)
260
+ if (block.name === 'AskUserQuestion' && isPending) {
261
+ const input = block.input;
262
+ console.log(`Parser: Found AskUserQuestion tool, questions count: ${input.questions?.length || 0}`);
263
+ if (input.questions && input.questions.length > 0) {
264
+ // Extract all questions
265
+ questions = input.questions.map(q => ({
266
+ question: q.question,
267
+ header: q.header,
268
+ options: q.options.map(opt => ({
269
+ label: opt.label,
270
+ description: opt.description,
271
+ })),
272
+ multiSelect: q.multiSelect || false,
273
+ }));
274
+ // Set content to first question for backward compat / message bubble text
275
+ const firstQuestion = input.questions[0];
276
+ content = firstQuestion.question;
277
+ // Set options from first question for backward compat (single-question case)
278
+ options = firstQuestion.options.map(opt => ({
279
+ label: opt.label,
280
+ description: opt.description,
281
+ }));
282
+ isWaitingForChoice = true;
283
+ multiSelect = firstQuestion.multiSelect || false;
284
+ console.log(`Parser: Extracted ${questions.length} questions, first has ${options.length} options: "${content.substring(0, 50)}..." (multiSelect: ${multiSelect})`);
285
+ }
286
+ }
287
+ else if (block.name === 'AskUserQuestion' && !isPending) {
288
+ // Show the question content but no options (already answered)
289
+ const input = block.input;
290
+ if (input.questions && input.questions.length > 0) {
291
+ content = input.questions[0].question;
292
+ }
293
+ }
294
+ // Add Yes/No options for pending approval tools
295
+ // But NOT for Task tools - they run in background and stay "pending" for a long time
296
+ else if (isPending && tool_config_1.APPROVAL_TOOLS.includes(block.name) && block.name !== 'Task') {
297
+ const input = block.input;
298
+ let description = '';
299
+ // Build a helpful description based on tool type
300
+ if (block.name === 'Bash' && input.command) {
301
+ description = `Run: ${input.command.substring(0, 100)}`;
302
+ }
303
+ else if ((block.name === 'Edit' || block.name === 'Write') && input.file_path) {
304
+ description = `${block.name}: ${input.file_path}`;
305
+ }
306
+ else if (block.name === 'Task' && input.description) {
307
+ description = `Task: ${input.description}`;
308
+ }
309
+ else {
310
+ description = `Allow ${block.name}?`;
311
+ }
312
+ options = [
313
+ { label: 'yes', description: `Approve: ${description}` },
314
+ { label: 'no', description: 'Reject this action' },
315
+ ];
316
+ isWaitingForChoice = true;
317
+ console.log(`Parser: Pending ${block.name} tool needs approval: "${description.substring(0, 50)}..."`);
318
+ }
319
+ }
320
+ else if (block.type === 'tool_result') {
321
+ // Skip tool results entirely - they're internal assistant responses
322
+ // We only want to show actual user-typed messages
323
+ }
324
+ }
325
+ }
326
+ const timestamp = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
327
+ return {
328
+ id: entry.uuid || String(timestamp),
329
+ type: entry.type,
330
+ content,
331
+ timestamp,
332
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
333
+ options,
334
+ questions,
335
+ isWaitingForChoice,
336
+ multiSelect: multiSelect || undefined,
337
+ };
338
+ }
339
+ function extractHighlights(messages) {
340
+ // Find the index of the last user message - anything before this has been "responded to"
341
+ let lastUserMessageIndex = -1;
342
+ for (let i = messages.length - 1; i >= 0; i--) {
343
+ if (messages[i].type === 'user') {
344
+ lastUserMessageIndex = i;
345
+ break;
346
+ }
347
+ }
348
+ const rawHighlights = messages
349
+ .filter(msg => {
350
+ // Include user messages with content
351
+ if (msg.type === 'user' && msg.content && msg.content.trim())
352
+ return true;
353
+ // Include assistant messages with content OR toolCalls
354
+ if (msg.type === 'assistant') {
355
+ const hasContent = msg.content && msg.content.trim();
356
+ const hasToolCalls = msg.toolCalls && msg.toolCalls.length > 0;
357
+ return hasContent || hasToolCalls;
358
+ }
359
+ return false;
360
+ })
361
+ .map((msg, index, arr) => {
362
+ const isLastMessage = index === arr.length - 1;
363
+ const originalIndex = messages.indexOf(msg);
364
+ // Check if this message has pending approval tools
365
+ const hasPendingApprovalTools = msg.toolCalls?.some(tc => tc.status === 'pending' && tool_config_1.APPROVAL_TOOLS.includes(tc.name) && tc.name !== 'Task') ?? false;
366
+ // Check if all tools in this message are already completed/errored
367
+ const allToolsCompleted = (msg.toolCalls?.length ?? 0) > 0 && msg.toolCalls?.every(tc => tc.status === 'completed' || tc.status === 'error' || tc.output !== undefined);
368
+ // Check if user already responded after this message (tool is running, not waiting)
369
+ const userRespondedAfter = originalIndex < messages.length - 1 &&
370
+ messages.slice(originalIndex + 1).some(m => m.type === 'user');
371
+ // Show options if:
372
+ // 1. This message has options AND
373
+ // 2. Either it's the last message OR it has pending approval tools AND
374
+ // 3. Tools haven't all completed AND
375
+ // 4. User hasn't already responded (tool would be running, not waiting)
376
+ const showOptions = msg.options && msg.options.length > 0 &&
377
+ (isLastMessage || hasPendingApprovalTools) && !allToolsCompleted && !userRespondedAfter;
378
+ // If user responded after this message, pending tools are now running (not waiting for approval)
379
+ const toolCalls = userRespondedAfter && msg.toolCalls
380
+ ? msg.toolCalls.map(tc => tc.status === 'pending' ? { ...tc, status: 'running' } : tc)
381
+ : msg.toolCalls;
382
+ return {
383
+ id: msg.id,
384
+ type: msg.type,
385
+ content: msg.content,
386
+ timestamp: msg.timestamp,
387
+ options: showOptions ? msg.options : undefined,
388
+ questions: showOptions ? msg.questions : undefined,
389
+ isWaitingForChoice: showOptions ? msg.isWaitingForChoice : false,
390
+ multiSelect: showOptions ? msg.multiSelect : undefined,
391
+ toolCalls,
392
+ };
393
+ });
394
+ // Merge consecutive assistant messages that are tool-only (no text content)
395
+ // into a single message so the UI can collapse them together
396
+ const highlights = [];
397
+ for (const h of rawHighlights) {
398
+ const prev = highlights[highlights.length - 1];
399
+ const isToolOnly = h.type === 'assistant' && (!h.content || !h.content.trim()) && h.toolCalls && h.toolCalls.length > 0;
400
+ const prevIsToolOnly = prev && prev.type === 'assistant' && (!prev.content || !prev.content.trim()) && prev.toolCalls && prev.toolCalls.length > 0;
401
+ if (isToolOnly && prevIsToolOnly && !h.options && !prev.options) {
402
+ // Merge: append tool calls to previous message
403
+ prev.toolCalls = [...(prev.toolCalls || []), ...(h.toolCalls || [])];
404
+ prev.timestamp = h.timestamp; // Use latest timestamp
405
+ }
406
+ else {
407
+ highlights.push(h);
408
+ }
409
+ }
410
+ // Log if the last highlight has options
411
+ const lastHighlight = highlights[highlights.length - 1];
412
+ if (lastHighlight?.options && lastHighlight.options.length > 0) {
413
+ console.log(`Parser: Last message has ${lastHighlight.options.length} options`);
414
+ }
415
+ return highlights;
416
+ }
417
+ function detectWaitingForInput(messages) {
418
+ if (messages.length === 0)
419
+ return false;
420
+ const lastMessage = messages[messages.length - 1];
421
+ // If the last message is from the assistant
422
+ if (lastMessage.type === 'assistant') {
423
+ // Check for pending tool calls that need approval
424
+ if (lastMessage.toolCalls) {
425
+ const hasPendingApproval = lastMessage.toolCalls.some(tc => tc.status === 'pending' && tool_config_1.APPROVAL_TOOLS.includes(tc.name));
426
+ if (hasPendingApproval) {
427
+ return true;
428
+ }
429
+ }
430
+ // Check for text content that looks like a question or prompt
431
+ if (lastMessage.content.trim()) {
432
+ const questionPatterns = [
433
+ /\?$/,
434
+ /would you like/i,
435
+ /do you want/i,
436
+ /should I/i,
437
+ /let me know/i,
438
+ /please confirm/i,
439
+ /please provide/i,
440
+ ];
441
+ for (const pattern of questionPatterns) {
442
+ if (pattern.test(lastMessage.content)) {
443
+ return true;
444
+ }
445
+ }
446
+ }
447
+ }
448
+ return false;
449
+ }
450
+ // Detect if the assistant has finished working and is idle (not actively expecting a response)
451
+ function detectIdle(messages) {
452
+ if (messages.length === 0)
453
+ return false;
454
+ const lastMessage = messages[messages.length - 1];
455
+ // If the last message is from the assistant with all tools completed and no question
456
+ if (lastMessage.type === 'assistant') {
457
+ // Still has running/pending tools = not idle
458
+ if (lastMessage.toolCalls?.some(tc => tc.status === 'pending' || tc.status === 'running')) {
459
+ return false;
460
+ }
461
+ // All tools completed, no question pattern = idle (finished task)
462
+ if (!lastMessage.toolCalls || lastMessage.toolCalls.every(tc => tc.status === 'completed')) {
463
+ return !detectWaitingForInput(messages);
464
+ }
465
+ }
466
+ return false;
467
+ }
468
+ function detectCurrentActivity(messages) {
469
+ if (messages.length === 0)
470
+ return undefined;
471
+ const lastMessage = messages[messages.length - 1];
472
+ // If last message is from user, the assistant is processing
473
+ if (lastMessage.type === 'user') {
474
+ return 'Processing...';
475
+ }
476
+ // Check for tool calls in the last assistant message
477
+ if (lastMessage.type === 'assistant' && lastMessage.toolCalls && lastMessage.toolCalls.length > 0) {
478
+ const lastTool = lastMessage.toolCalls[lastMessage.toolCalls.length - 1];
479
+ // Check if this is a pending approval
480
+ if (lastTool.status === 'pending' && tool_config_1.APPROVAL_TOOLS.includes(lastTool.name)) {
481
+ const input = lastTool.input;
482
+ if (lastTool.name === 'Bash' && input.command) {
483
+ const cmd = input.command.substring(0, 40);
484
+ return `Approve? ${cmd}${input.command.length > 40 ? '...' : ''}`;
485
+ }
486
+ if ((lastTool.name === 'Edit' || lastTool.name === 'Write') && input.file_path) {
487
+ const filePath = input.file_path;
488
+ const fileName = filePath.split('/').pop() || filePath;
489
+ return `Approve ${lastTool.name.toLowerCase()}: ${fileName}?`;
490
+ }
491
+ return `Approve ${lastTool.name}?`;
492
+ }
493
+ const description = (0, tool_config_1.getToolDescription)(lastTool.name);
494
+ // Add file path info if available
495
+ if (lastTool.input) {
496
+ const input = lastTool.input;
497
+ if (input.file_path) {
498
+ const filePath = input.file_path;
499
+ const fileName = filePath.split('/').pop() || filePath;
500
+ return `${description}: ${fileName}`;
501
+ }
502
+ if (input.command) {
503
+ const cmd = input.command.substring(0, 30);
504
+ return `${description}: ${cmd}${input.command.length > 30 ? '...' : ''}`;
505
+ }
506
+ }
507
+ return description;
508
+ }
509
+ // Don't show "waiting for input" - there's already a separate indicator for that
510
+ return undefined;
511
+ }
512
+ function getRecentActivity(messages, limit = 5) {
513
+ const activities = [];
514
+ // Go through messages in reverse to get recent activity
515
+ for (let i = messages.length - 1; i >= 0 && activities.length < limit; i--) {
516
+ const msg = messages[i];
517
+ if (msg.type === 'assistant' && msg.toolCalls) {
518
+ for (const tool of msg.toolCalls) {
519
+ if (activities.length >= limit)
520
+ break;
521
+ const input = tool.input;
522
+ let inputStr = '';
523
+ let outputStr = tool.output || '';
524
+ // Format input based on tool type
525
+ if (input.file_path) {
526
+ inputStr = input.file_path;
527
+ }
528
+ else if (input.command) {
529
+ inputStr = input.command;
530
+ }
531
+ else if (input.pattern) {
532
+ inputStr = `Pattern: ${input.pattern}`;
533
+ }
534
+ else if (input.query) {
535
+ inputStr = input.query;
536
+ }
537
+ activities.push({
538
+ summary: `${tool.name}${inputStr ? `: ${inputStr.substring(0, 100)}` : ''}`,
539
+ toolName: tool.name,
540
+ input: inputStr,
541
+ output: outputStr.substring(0, 2000), // Limit output size
542
+ timestamp: msg.timestamp,
543
+ });
544
+ }
545
+ }
546
+ }
547
+ return activities.reverse(); // Return in chronological order
548
+ }
549
+ function getSessionStatus(conversationPath, isProcessRunning) {
550
+ const messages = parseConversationFile(conversationPath);
551
+ const lastMessage = messages[messages.length - 1];
552
+ return {
553
+ isRunning: isProcessRunning,
554
+ isWaitingForInput: isProcessRunning && detectWaitingForInput(messages),
555
+ lastActivity: lastMessage?.timestamp || 0,
556
+ conversationId: conversationPath,
557
+ currentActivity: isProcessRunning ? detectCurrentActivity(messages) : undefined,
558
+ };
559
+ }
560
+ /**
561
+ * Get list of pending tools that need approval from the last message
562
+ */
563
+ function getPendingApprovalTools(messages) {
564
+ if (messages.length === 0)
565
+ return [];
566
+ const lastMessage = messages[messages.length - 1];
567
+ if (lastMessage.type !== 'assistant' || !lastMessage.toolCalls)
568
+ return [];
569
+ return lastMessage.toolCalls
570
+ .filter(tc => tc.status === 'pending')
571
+ .map(tc => tc.name);
572
+ }
573
+ /**
574
+ * Detect compaction events in a conversation file
575
+ * Returns the most recent compaction summary if found
576
+ */
577
+ function detectCompaction(filePath, sessionId, sessionName, projectPath, lastCheckedLine = 0, preReadContent) {
578
+ const content = preReadContent ?? (fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '');
579
+ if (!content) {
580
+ return { event: null, lastLine: 0 };
581
+ }
582
+ const lines = content.split('\n').filter(line => line.trim());
583
+ let compactionEvent = null;
584
+ // Only check lines after lastCheckedLine to avoid re-detecting old compactions
585
+ for (let i = lastCheckedLine; i < lines.length; i++) {
586
+ try {
587
+ const entry = JSON.parse(lines[i]);
588
+ // Look for summary type entries (context compaction)
589
+ if (entry.type === 'summary' && entry.summary) {
590
+ const timestamp = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
591
+ compactionEvent = {
592
+ sessionId,
593
+ sessionName,
594
+ projectPath,
595
+ summary: entry.summary,
596
+ timestamp,
597
+ };
598
+ }
599
+ }
600
+ catch {
601
+ // Skip malformed lines
602
+ }
603
+ }
604
+ return { event: compactionEvent, lastLine: lines.length };
605
+ }
606
+ /**
607
+ * Extract usage data from a conversation JSONL file
608
+ */
609
+ function extractUsageFromFile(filePath, sessionName) {
610
+ const result = {
611
+ sessionId: filePath,
612
+ sessionName,
613
+ totalInputTokens: 0,
614
+ totalOutputTokens: 0,
615
+ totalCacheCreationTokens: 0,
616
+ totalCacheReadTokens: 0,
617
+ messageCount: 0,
618
+ currentContextTokens: 0,
619
+ };
620
+ if (!fs.existsSync(filePath)) {
621
+ return result;
622
+ }
623
+ const content = fs.readFileSync(filePath, 'utf-8');
624
+ const lines = content.split('\n').filter(line => line.trim());
625
+ const seenMessageIds = new Set();
626
+ for (const line of lines) {
627
+ try {
628
+ const entry = JSON.parse(line);
629
+ // Only count assistant messages with usage data
630
+ if (entry.type === 'assistant' && entry.message?.usage) {
631
+ const msgId = entry.message?.id;
632
+ // Skip duplicate message IDs (same message can appear multiple times as it streams)
633
+ if (msgId && seenMessageIds.has(msgId)) {
634
+ continue;
635
+ }
636
+ if (msgId) {
637
+ seenMessageIds.add(msgId);
638
+ }
639
+ const usage = entry.message.usage;
640
+ // Only add non-zero usage (final message has the totals)
641
+ if (usage.input_tokens && usage.input_tokens > 0) {
642
+ result.totalInputTokens += usage.input_tokens;
643
+ result.messageCount++;
644
+ }
645
+ if (usage.output_tokens && usage.output_tokens > 0) {
646
+ result.totalOutputTokens += usage.output_tokens;
647
+ }
648
+ if (usage.cache_creation_input_tokens && usage.cache_creation_input_tokens > 0) {
649
+ result.totalCacheCreationTokens += usage.cache_creation_input_tokens;
650
+ }
651
+ if (usage.cache_read_input_tokens && usage.cache_read_input_tokens > 0) {
652
+ result.totalCacheReadTokens += usage.cache_read_input_tokens;
653
+ }
654
+ // Track current context size from the most recent message
655
+ if (usage.input_tokens) {
656
+ result.currentContextTokens = usage.input_tokens;
657
+ }
658
+ }
659
+ }
660
+ catch {
661
+ // Skip malformed lines
662
+ }
663
+ }
664
+ return result;
665
+ }
666
+ /**
667
+ * Extract tasks from JSONL content (from TaskCreate/TaskUpdate tool calls)
668
+ */
669
+ function extractTasks(content) {
670
+ const lines = content.split('\n').filter(line => line.trim());
671
+ // Track tasks by temporary ID (toolu_xxx) until we get real ID from result
672
+ const pendingTasks = new Map();
673
+ // Map toolu_xxx to real task ID
674
+ const toolIdToTaskId = new Map();
675
+ // Final tasks by real ID
676
+ const tasks = new Map();
677
+ for (const line of lines) {
678
+ try {
679
+ const entry = JSON.parse(line);
680
+ if (entry.message?.content && Array.isArray(entry.message.content)) {
681
+ const timestamp = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
682
+ for (const block of entry.message.content) {
683
+ // Handle TaskCreate
684
+ if (block.type === 'tool_use' && block.name === 'TaskCreate') {
685
+ const input = block.input;
686
+ const toolId = block.id;
687
+ pendingTasks.set(toolId, {
688
+ task: {
689
+ subject: input.subject,
690
+ description: input.description,
691
+ activeForm: input.activeForm,
692
+ status: 'pending',
693
+ blockedBy: [],
694
+ blocks: [],
695
+ createdAt: timestamp,
696
+ updatedAt: timestamp,
697
+ },
698
+ timestamp,
699
+ });
700
+ }
701
+ // Handle TaskUpdate
702
+ if (block.type === 'tool_use' && block.name === 'TaskUpdate') {
703
+ const input = block.input;
704
+ const taskId = input.taskId;
705
+ // Find existing task
706
+ const existingTask = tasks.get(taskId);
707
+ if (existingTask) {
708
+ // Handle deletion
709
+ if (input.status === 'deleted') {
710
+ tasks.delete(taskId);
711
+ continue;
712
+ }
713
+ // Apply updates
714
+ if (input.status) {
715
+ existingTask.status = input.status;
716
+ }
717
+ if (input.subject) {
718
+ existingTask.subject = input.subject;
719
+ }
720
+ if (input.description) {
721
+ existingTask.description = input.description;
722
+ }
723
+ if (input.activeForm) {
724
+ existingTask.activeForm = input.activeForm;
725
+ }
726
+ else if (input.status === 'completed') {
727
+ // Clear activeForm when completed
728
+ existingTask.activeForm = undefined;
729
+ }
730
+ if (input.owner) {
731
+ existingTask.owner = input.owner;
732
+ }
733
+ if (input.addBlockedBy) {
734
+ existingTask.blockedBy = [
735
+ ...(existingTask.blockedBy || []),
736
+ ...input.addBlockedBy,
737
+ ];
738
+ }
739
+ if (input.addBlocks) {
740
+ existingTask.blocks = [
741
+ ...(existingTask.blocks || []),
742
+ ...input.addBlocks,
743
+ ];
744
+ }
745
+ existingTask.updatedAt = timestamp;
746
+ }
747
+ }
748
+ // Handle tool_result to get real task IDs
749
+ if (block.type === 'tool_result' && block.tool_use_id) {
750
+ const toolId = block.tool_use_id;
751
+ const pending = pendingTasks.get(toolId);
752
+ if (pending) {
753
+ // Extract task ID from result content
754
+ let resultContent = '';
755
+ if (typeof block.content === 'string') {
756
+ resultContent = block.content;
757
+ }
758
+ else if (Array.isArray(block.content)) {
759
+ resultContent = block.content
760
+ .filter((c) => c.type === 'text' && c.text)
761
+ .map((c) => c.text || '')
762
+ .join('\n');
763
+ }
764
+ // Try to extract task ID from "Task created with ID: X"
765
+ const idMatch = resultContent.match(/(?:Task created with ID:|id[:\s]+)(\d+)/i);
766
+ if (idMatch) {
767
+ const realId = idMatch[1];
768
+ toolIdToTaskId.set(toolId, realId);
769
+ // Create the task with real ID
770
+ tasks.set(realId, {
771
+ id: realId,
772
+ subject: pending.task.subject || '',
773
+ description: pending.task.description || '',
774
+ status: pending.task.status || 'pending',
775
+ activeForm: pending.task.activeForm,
776
+ owner: pending.task.owner,
777
+ blockedBy: pending.task.blockedBy,
778
+ blocks: pending.task.blocks,
779
+ createdAt: pending.task.createdAt || timestamp,
780
+ updatedAt: pending.task.updatedAt || timestamp,
781
+ });
782
+ }
783
+ pendingTasks.delete(toolId);
784
+ }
785
+ }
786
+ }
787
+ }
788
+ }
789
+ catch {
790
+ // Skip malformed lines
791
+ }
792
+ }
793
+ // Return tasks sorted by ID (numeric order)
794
+ return Array.from(tasks.values()).sort((a, b) => {
795
+ const aNum = parseInt(a.id, 10);
796
+ const bNum = parseInt(b.id, 10);
797
+ return aNum - bNum;
798
+ });
799
+ }
800
+ //# sourceMappingURL=parser.js.map