@cmdctrl/claude-code 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 (83) hide show
  1. package/dist/adapter/claude-cli.d.ts +41 -0
  2. package/dist/adapter/claude-cli.d.ts.map +1 -0
  3. package/dist/adapter/claude-cli.js +525 -0
  4. package/dist/adapter/claude-cli.js.map +1 -0
  5. package/dist/adapter/events.d.ts +52 -0
  6. package/dist/adapter/events.d.ts.map +1 -0
  7. package/dist/adapter/events.js +134 -0
  8. package/dist/adapter/events.js.map +1 -0
  9. package/dist/client/messages.d.ts +140 -0
  10. package/dist/client/messages.d.ts.map +1 -0
  11. package/dist/client/messages.js +6 -0
  12. package/dist/client/messages.js.map +1 -0
  13. package/dist/client/websocket.d.ts +115 -0
  14. package/dist/client/websocket.d.ts.map +1 -0
  15. package/dist/client/websocket.js +434 -0
  16. package/dist/client/websocket.js.map +1 -0
  17. package/dist/commands/register.d.ts +10 -0
  18. package/dist/commands/register.d.ts.map +1 -0
  19. package/dist/commands/register.js +175 -0
  20. package/dist/commands/register.js.map +1 -0
  21. package/dist/commands/start.d.ts +9 -0
  22. package/dist/commands/start.d.ts.map +1 -0
  23. package/dist/commands/start.js +54 -0
  24. package/dist/commands/start.js.map +1 -0
  25. package/dist/commands/status.d.ts +5 -0
  26. package/dist/commands/status.d.ts.map +1 -0
  27. package/dist/commands/status.js +38 -0
  28. package/dist/commands/status.js.map +1 -0
  29. package/dist/commands/stop.d.ts +5 -0
  30. package/dist/commands/stop.d.ts.map +1 -0
  31. package/dist/commands/stop.js +59 -0
  32. package/dist/commands/stop.js.map +1 -0
  33. package/dist/commands/unregister.d.ts +5 -0
  34. package/dist/commands/unregister.d.ts.map +1 -0
  35. package/dist/commands/unregister.js +28 -0
  36. package/dist/commands/unregister.js.map +1 -0
  37. package/dist/config/config.d.ts +68 -0
  38. package/dist/config/config.d.ts.map +1 -0
  39. package/dist/config/config.js +193 -0
  40. package/dist/config/config.js.map +1 -0
  41. package/dist/handlers/context-handler.d.ts +37 -0
  42. package/dist/handlers/context-handler.d.ts.map +1 -0
  43. package/dist/handlers/context-handler.js +303 -0
  44. package/dist/handlers/context-handler.js.map +1 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +39 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/message-reader.d.ts +25 -0
  50. package/dist/message-reader.d.ts.map +1 -0
  51. package/dist/message-reader.js +454 -0
  52. package/dist/message-reader.js.map +1 -0
  53. package/dist/session-discovery.d.ts +48 -0
  54. package/dist/session-discovery.d.ts.map +1 -0
  55. package/dist/session-discovery.js +496 -0
  56. package/dist/session-discovery.js.map +1 -0
  57. package/dist/session-watcher.d.ts +92 -0
  58. package/dist/session-watcher.d.ts.map +1 -0
  59. package/dist/session-watcher.js +494 -0
  60. package/dist/session-watcher.js.map +1 -0
  61. package/dist/session-watcher.test.d.ts +9 -0
  62. package/dist/session-watcher.test.d.ts.map +1 -0
  63. package/dist/session-watcher.test.js +149 -0
  64. package/dist/session-watcher.test.js.map +1 -0
  65. package/jest.config.js +8 -0
  66. package/package.json +42 -0
  67. package/src/adapter/claude-cli.ts +591 -0
  68. package/src/adapter/events.ts +186 -0
  69. package/src/client/messages.ts +193 -0
  70. package/src/client/websocket.ts +509 -0
  71. package/src/commands/register.ts +201 -0
  72. package/src/commands/start.ts +70 -0
  73. package/src/commands/status.ts +47 -0
  74. package/src/commands/stop.ts +58 -0
  75. package/src/commands/unregister.ts +30 -0
  76. package/src/config/config.ts +163 -0
  77. package/src/handlers/context-handler.ts +337 -0
  78. package/src/index.ts +45 -0
  79. package/src/message-reader.ts +485 -0
  80. package/src/session-discovery.ts +557 -0
  81. package/src/session-watcher.test.ts +141 -0
  82. package/src/session-watcher.ts +560 -0
  83. package/tsconfig.json +19 -0
@@ -0,0 +1,560 @@
1
+ /**
2
+ * Session file watcher for monitoring JSONL session files
3
+ *
4
+ * Watches JSONL files and emits typed events for each new entry:
5
+ * - AGENT_RESPONSE: assistant entries with text content
6
+ * - VERBOSE: tool_use, thinking, tool_result entries
7
+ * - USER_MESSAGE: user entries (for passive observers)
8
+ *
9
+ * This is the single source of truth for session content events.
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+
14
+ // Event types emitted by SessionWatcher
15
+ export interface SessionEvent {
16
+ type: 'AGENT_RESPONSE' | 'VERBOSE' | 'USER_MESSAGE';
17
+ sessionId: string;
18
+ uuid: string;
19
+ content: string;
20
+ timestamp: string;
21
+ // For USER_MESSAGE events
22
+ isToolResult?: boolean;
23
+ }
24
+
25
+ interface WatchedSession {
26
+ sessionId: string;
27
+ filePath: string;
28
+ lastSize: number;
29
+ processedUuids: Set<string>;
30
+ lastLineCount: number;
31
+ messageCount: number;
32
+ lastMessage: string;
33
+ }
34
+
35
+ type EventCallback = (event: SessionEvent) => void;
36
+
37
+ // Completion event includes session metadata for push notifications
38
+ export interface CompletionEvent {
39
+ sessionId: string;
40
+ filePath: string;
41
+ lastMessage: string;
42
+ messageCount: number;
43
+ }
44
+
45
+ type CompletionCallback = (event: CompletionEvent) => void;
46
+
47
+ // Polling interval (500ms)
48
+ const POLL_INTERVAL_MS = 500;
49
+
50
+ // Time to wait after AGENT_RESPONSE before declaring completion
51
+ // If a tool call (VERBOSE) arrives within this window, completion is cancelled
52
+ // Must be long enough to account for Claude Code writing text and tool_use as
53
+ // SEPARATE entries. Claude often takes 2-4 seconds between writing "Let me do X"
54
+ // and actually writing the tool_use block.
55
+ const COMPLETION_DELAY_MS = 5000;
56
+
57
+ export class SessionWatcher {
58
+ private watchedSessions: Map<string, WatchedSession> = new Map();
59
+ private completionTimers: Map<string, NodeJS.Timeout> = new Map();
60
+ private onEvent: EventCallback;
61
+ private onCompletion: CompletionCallback | null = null;
62
+ private pollTimer: NodeJS.Timeout | null = null;
63
+
64
+ constructor(onEvent: EventCallback, onCompletion?: CompletionCallback) {
65
+ this.onEvent = onEvent;
66
+ this.onCompletion = onCompletion || null;
67
+ }
68
+
69
+ /**
70
+ * Start watching a session file for changes
71
+ */
72
+ watchSession(sessionId: string, filePath: string): void {
73
+ if (this.watchedSessions.has(sessionId)) {
74
+ console.log(`[SessionWatcher] Already watching session ${sessionId}`);
75
+ return;
76
+ }
77
+
78
+ if (!fs.existsSync(filePath)) {
79
+ console.warn(`[SessionWatcher] File not found: ${filePath}`);
80
+ return;
81
+ }
82
+
83
+ try {
84
+ const stats = fs.statSync(filePath);
85
+ const { processedUuids, lineCount, messageCount, lastMessage } = this.initializeFromFile(filePath);
86
+
87
+ this.watchedSessions.set(sessionId, {
88
+ sessionId,
89
+ filePath,
90
+ lastSize: stats.size,
91
+ processedUuids,
92
+ lastLineCount: lineCount,
93
+ messageCount,
94
+ lastMessage,
95
+ });
96
+
97
+ console.log(`[SessionWatcher] Started watching session ${sessionId} (${processedUuids.size} entries, ${messageCount} messages)`);
98
+
99
+ // Start polling if not already running
100
+ if (!this.pollTimer) {
101
+ this.startPolling();
102
+ }
103
+ } catch (err) {
104
+ console.error(`[SessionWatcher] Failed to watch ${filePath}:`, err);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Stop watching a session file
110
+ */
111
+ unwatchSession(sessionId: string): void {
112
+ // Cancel any pending completion timer
113
+ this.cancelCompletionTimer(sessionId);
114
+
115
+ if (this.watchedSessions.delete(sessionId)) {
116
+ console.log(`[SessionWatcher] Stopped watching session ${sessionId}`);
117
+ }
118
+
119
+ // Stop polling if no sessions left
120
+ if (this.watchedSessions.size === 0 && this.pollTimer) {
121
+ clearInterval(this.pollTimer);
122
+ this.pollTimer = null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Stop watching all sessions
128
+ */
129
+ unwatchAll(): void {
130
+ // Cancel all completion timers
131
+ for (const timer of this.completionTimers.values()) {
132
+ clearTimeout(timer);
133
+ }
134
+ this.completionTimers.clear();
135
+
136
+ this.watchedSessions.clear();
137
+ if (this.pollTimer) {
138
+ clearInterval(this.pollTimer);
139
+ this.pollTimer = null;
140
+ }
141
+ console.log('[SessionWatcher] Stopped watching all sessions');
142
+ }
143
+
144
+ /**
145
+ * Initialize processed UUIDs from existing file content
146
+ * This prevents emitting events for entries that existed before we started watching
147
+ */
148
+ private initializeFromFile(filePath: string): { processedUuids: Set<string>; lineCount: number; messageCount: number; lastMessage: string } {
149
+ const processedUuids = new Set<string>();
150
+ let messageCount = 0;
151
+ let lastMessage = '';
152
+
153
+ try {
154
+ const content = fs.readFileSync(filePath, 'utf-8');
155
+ const lines = content.split('\n').filter(line => line.trim());
156
+
157
+ for (const line of lines) {
158
+ try {
159
+ const entry = JSON.parse(line);
160
+ if (entry.uuid) {
161
+ processedUuids.add(entry.uuid);
162
+ messageCount++;
163
+
164
+ // Track last message content for session_activity
165
+ const entryMessage = entry.message as Record<string, unknown> | undefined;
166
+ const content = entryMessage?.content;
167
+ if (typeof content === 'string') {
168
+ lastMessage = content.slice(0, 200);
169
+ } else if (Array.isArray(content)) {
170
+ const textBlocks = content.filter((b: Record<string, unknown>) => b.type === 'text');
171
+ if (textBlocks.length > 0) {
172
+ lastMessage = (textBlocks[0].text as string || '').slice(0, 200);
173
+ }
174
+ }
175
+ }
176
+ } catch {
177
+ // Skip invalid JSON lines
178
+ }
179
+ }
180
+
181
+ return { processedUuids, lineCount: lines.length, messageCount, lastMessage };
182
+ } catch {
183
+ return { processedUuids, lineCount: 0, messageCount: 0, lastMessage: '' };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Start the polling loop
189
+ */
190
+ private startPolling(): void {
191
+ this.pollTimer = setInterval(() => {
192
+ this.pollAllSessions();
193
+ }, POLL_INTERVAL_MS);
194
+ }
195
+
196
+ /**
197
+ * Poll all watched sessions for changes
198
+ */
199
+ private pollAllSessions(): void {
200
+ for (const [, session] of this.watchedSessions) {
201
+ this.checkSession(session);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Check a single session for changes
207
+ */
208
+ private checkSession(session: WatchedSession): void {
209
+ try {
210
+ if (!fs.existsSync(session.filePath)) {
211
+ console.warn(`[SessionWatcher] File no longer exists: ${session.filePath}`);
212
+ this.unwatchSession(session.sessionId);
213
+ return;
214
+ }
215
+
216
+ const stats = fs.statSync(session.filePath);
217
+
218
+ // Only check if file size changed
219
+ if (stats.size === session.lastSize) {
220
+ return;
221
+ }
222
+
223
+ // Read and process new entries
224
+ const newEntries = this.readNewEntries(session);
225
+ session.lastSize = stats.size;
226
+
227
+ // First pass: emit events and track what we saw
228
+ let sawAgentResponse = false;
229
+ let sawToolCall = false;
230
+
231
+ for (const entry of newEntries) {
232
+ const event = this.entryToEvent(session.sessionId, entry);
233
+ if (event) {
234
+ console.log(`[SessionWatcher] Emitting ${event.type} for session ${session.sessionId.slice(-8)}: ${event.content.slice(0, 50)}...`);
235
+ this.onEvent(event);
236
+
237
+ // Track message count and last message
238
+ session.messageCount++;
239
+ session.lastMessage = event.content.slice(0, 200);
240
+
241
+ // Track what event types we saw in this batch
242
+ if (event.type === 'AGENT_RESPONSE') {
243
+ sawAgentResponse = true;
244
+ }
245
+
246
+ // Check for actual tool_use blocks (not thinking, not tool_result)
247
+ // This is the authoritative check for "agent is making a tool call"
248
+ if (this.entryHasToolUse(entry)) {
249
+ sawToolCall = true;
250
+ }
251
+ }
252
+ if (entry.uuid) {
253
+ session.processedUuids.add(entry.uuid as string);
254
+ }
255
+ }
256
+
257
+ // Second pass: completion detection based on entire batch
258
+ if (sawToolCall) {
259
+ // Tool call in this batch - cancel any pending timer, agent is still working
260
+ this.cancelCompletionTimer(session.sessionId);
261
+ } else if (sawAgentResponse) {
262
+ // Agent responded with no tool call in this batch - start completion timer
263
+ this.startCompletionTimer(session);
264
+ }
265
+
266
+ } catch (err) {
267
+ console.error(`[SessionWatcher] Error checking session ${session.sessionId}:`, err);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Read only NEW entries appended since lastSize
273
+ * Reads from lastSize offset forward, avoiding full file reads
274
+ */
275
+ private readNewEntries(session: WatchedSession): Array<Record<string, unknown>> {
276
+ const newEntries: Array<Record<string, unknown>> = [];
277
+
278
+ try {
279
+ const stats = fs.statSync(session.filePath);
280
+ const newBytes = stats.size - session.lastSize;
281
+
282
+ if (newBytes <= 0) {
283
+ return newEntries;
284
+ }
285
+
286
+ // Read only the new bytes from the end
287
+ const fd = fs.openSync(session.filePath, 'r');
288
+ const buffer = Buffer.alloc(newBytes);
289
+ fs.readSync(fd, buffer, 0, newBytes, session.lastSize);
290
+ fs.closeSync(fd);
291
+
292
+ const content = buffer.toString('utf-8');
293
+ const lines = content.split('\n').filter(line => line.trim());
294
+
295
+ for (const line of lines) {
296
+ try {
297
+ const entry = JSON.parse(line);
298
+ // Skip entries we've already processed (safety check)
299
+ if (entry.uuid && session.processedUuids.has(entry.uuid)) {
300
+ continue;
301
+ }
302
+ // Skip internal entries (no uuid)
303
+ if (!entry.uuid) {
304
+ continue;
305
+ }
306
+ newEntries.push(entry);
307
+ } catch {
308
+ // Skip invalid JSON lines (could be partial line at boundary)
309
+ }
310
+ }
311
+ } catch (err) {
312
+ console.error(`[SessionWatcher] Error reading file:`, err);
313
+ }
314
+
315
+ return newEntries;
316
+ }
317
+
318
+ /**
319
+ * Convert a JSONL entry to a SessionEvent
320
+ */
321
+ private entryToEvent(sessionId: string, entry: Record<string, unknown>): SessionEvent | null {
322
+ const entryType = entry.type as string;
323
+ const uuid = entry.uuid as string;
324
+ const timestamp = (entry.timestamp as string) || new Date().toISOString();
325
+ const message = entry.message as Record<string, unknown> | undefined;
326
+ const content = message?.content;
327
+
328
+ // Handle user entries
329
+ if (entryType === 'user') {
330
+ // Check if this is a tool_result (internal, but we emit as VERBOSE)
331
+ if (Array.isArray(content)) {
332
+ const hasToolResult = content.some(
333
+ (block: Record<string, unknown>) => block.type === 'tool_result'
334
+ );
335
+ if (hasToolResult) {
336
+ // Extract tool result content
337
+ const toolResultBlock = content.find(
338
+ (block: Record<string, unknown>) => block.type === 'tool_result'
339
+ ) as Record<string, unknown>;
340
+
341
+ // Content can be a string, array (for images), or other types
342
+ const rawContent = toolResultBlock?.content;
343
+ const resultContent = typeof rawContent === 'string'
344
+ ? rawContent
345
+ : (Array.isArray(rawContent) ? JSON.stringify(rawContent) : String(rawContent || ''));
346
+
347
+ // Skip empty tool results - no value in showing "(empty output)"
348
+ if (!resultContent.trim()) {
349
+ return null;
350
+ }
351
+
352
+ return {
353
+ type: 'VERBOSE',
354
+ sessionId,
355
+ uuid,
356
+ content: resultContent.length > 200 ? resultContent.slice(0, 200) + '...' : resultContent,
357
+ timestamp,
358
+ isToolResult: true,
359
+ };
360
+ }
361
+ }
362
+
363
+ // Regular user message
364
+ const textContent = typeof content === 'string'
365
+ ? content
366
+ : Array.isArray(content)
367
+ ? content
368
+ .filter((block: Record<string, unknown>) => block.type === 'text')
369
+ .map((block: Record<string, unknown>) => block.text)
370
+ .join('\n')
371
+ : '';
372
+
373
+ if (!textContent) {
374
+ return null;
375
+ }
376
+
377
+ return {
378
+ type: 'USER_MESSAGE',
379
+ sessionId,
380
+ uuid,
381
+ content: textContent,
382
+ timestamp,
383
+ };
384
+ }
385
+
386
+ // Handle assistant entries
387
+ if (entryType === 'assistant') {
388
+ if (!Array.isArray(content)) {
389
+ console.log(`[SessionWatcher] Assistant entry ${uuid?.slice(-8)} has non-array content:`, typeof content);
390
+ return null;
391
+ }
392
+
393
+ // Log what block types are present for debugging
394
+ const blockTypes = content.map((b: Record<string, unknown>) => b.type);
395
+ console.log(`[SessionWatcher] Assistant entry ${uuid?.slice(-8)} has blocks:`, blockTypes);
396
+
397
+ // Check for text content (AGENT_RESPONSE)
398
+ const textBlocks = content.filter(
399
+ (block: Record<string, unknown>) => block.type === 'text'
400
+ );
401
+ if (textBlocks.length > 0) {
402
+ const textContent = textBlocks
403
+ .map((block: Record<string, unknown>) => block.text as string)
404
+ .join('\n')
405
+ .trim();
406
+
407
+ // Skip very short responses that are likely cursor indicators (e.g., "\", "|")
408
+ // Also skip if content is ONLY whitespace or special characters
409
+ const isLikelyCursor = textContent.length <= 2 && /^[\s\\|/_-]*$/.test(textContent);
410
+
411
+ if (textContent && !isLikelyCursor) {
412
+ console.log(`[SessionWatcher] Emitting AGENT_RESPONSE for ${uuid?.slice(-8)}: "${textContent.slice(0, 50)}..."`);
413
+ return {
414
+ type: 'AGENT_RESPONSE',
415
+ sessionId,
416
+ uuid,
417
+ content: textContent,
418
+ timestamp,
419
+ };
420
+ } else if (isLikelyCursor) {
421
+ console.log(`[SessionWatcher] Skipping cursor-like content for ${uuid?.slice(-8)}: "${textContent}"`);
422
+ } else {
423
+ console.log(`[SessionWatcher] Text blocks found but textContent is empty for ${uuid?.slice(-8)}`);
424
+ }
425
+ }
426
+
427
+ // Check for tool_use (VERBOSE)
428
+ const toolUseBlocks = content.filter(
429
+ (block: Record<string, unknown>) => block.type === 'tool_use'
430
+ );
431
+ if (toolUseBlocks.length > 0) {
432
+ const toolBlock = toolUseBlocks[0] as Record<string, unknown>;
433
+ const toolName = toolBlock.name as string;
434
+ const toolInput = toolBlock.input as Record<string, unknown> | undefined;
435
+ const formattedTool = this.formatToolUse(toolName, toolInput);
436
+
437
+ return {
438
+ type: 'VERBOSE',
439
+ sessionId,
440
+ uuid,
441
+ content: formattedTool,
442
+ timestamp,
443
+ };
444
+ }
445
+
446
+ // Check for thinking (VERBOSE)
447
+ const thinkingBlocks = content.filter(
448
+ (block: Record<string, unknown>) => block.type === 'thinking'
449
+ );
450
+ if (thinkingBlocks.length > 0) {
451
+ const thinkingContent = thinkingBlocks
452
+ .map((block: Record<string, unknown>) => block.thinking as string)
453
+ .join('\n');
454
+ const truncated = thinkingContent.length > 200
455
+ ? thinkingContent.slice(0, 200) + '...'
456
+ : thinkingContent;
457
+
458
+ return {
459
+ type: 'VERBOSE',
460
+ sessionId,
461
+ uuid,
462
+ content: `🤔 ${truncated}`,
463
+ timestamp,
464
+ };
465
+ }
466
+
467
+ console.log(`[SessionWatcher] Assistant entry ${uuid?.slice(-8)} had no recognized content blocks`);
468
+ }
469
+
470
+ return null;
471
+ }
472
+
473
+ /**
474
+ * Format a tool_use block for verbose display
475
+ */
476
+ private formatToolUse(name: string, input?: Record<string, unknown>): string {
477
+ switch (name) {
478
+ case 'Read':
479
+ return `📖 Reading ${input?.file_path || 'file'}`;
480
+ case 'Write':
481
+ return `✏️ Writing ${input?.file_path || 'file'}`;
482
+ case 'Edit':
483
+ return `🔧 Editing ${input?.file_path || 'file'}`;
484
+ case 'Bash':
485
+ const cmd = ((input?.command as string) || '').slice(0, 60);
486
+ return `⚡ Running: ${cmd}`;
487
+ case 'Glob':
488
+ return `🔍 Searching: ${input?.pattern || ''}`;
489
+ case 'Grep':
490
+ return `🔎 Grepping: ${input?.pattern || ''}`;
491
+ case 'Task':
492
+ return `📋 Spawning task`;
493
+ case 'TodoWrite':
494
+ return `📝 Updating todos`;
495
+ case 'WebSearch':
496
+ return `🌐 Searching: ${input?.query || ''}`;
497
+ case 'WebFetch':
498
+ return `🌐 Fetching: ${input?.url || ''}`;
499
+ default:
500
+ return `🔧 ${name}`;
501
+ }
502
+ }
503
+
504
+ get watchCount(): number {
505
+ return this.watchedSessions.size;
506
+ }
507
+
508
+ /**
509
+ * Check if an entry contains tool_use blocks (agent is making a tool call)
510
+ */
511
+ private entryHasToolUse(entry: Record<string, unknown>): boolean {
512
+ const message = entry.message as Record<string, unknown> | undefined;
513
+ const content = message?.content;
514
+
515
+ if (!Array.isArray(content)) {
516
+ return false;
517
+ }
518
+
519
+ return content.some((block: Record<string, unknown>) => block.type === 'tool_use');
520
+ }
521
+
522
+ /**
523
+ * Start a completion timer for a session
524
+ * If no tool call arrives within COMPLETION_DELAY_MS, fire the completion callback
525
+ */
526
+ private startCompletionTimer(session: WatchedSession): void {
527
+ // Cancel any existing timer first
528
+ this.cancelCompletionTimer(session.sessionId);
529
+
530
+ if (!this.onCompletion) {
531
+ return;
532
+ }
533
+
534
+ const timer = setTimeout(() => {
535
+ this.completionTimers.delete(session.sessionId);
536
+
537
+ if (this.onCompletion) {
538
+ this.onCompletion({
539
+ sessionId: session.sessionId,
540
+ filePath: session.filePath,
541
+ lastMessage: session.lastMessage,
542
+ messageCount: session.messageCount,
543
+ });
544
+ }
545
+ }, COMPLETION_DELAY_MS);
546
+
547
+ this.completionTimers.set(session.sessionId, timer);
548
+ }
549
+
550
+ /**
551
+ * Cancel a pending completion timer for a session
552
+ */
553
+ private cancelCompletionTimer(sessionId: string): void {
554
+ const timer = this.completionTimers.get(sessionId);
555
+ if (timer) {
556
+ clearTimeout(timer);
557
+ this.completionTimers.delete(sessionId);
558
+ }
559
+ }
560
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }