@cmdctrl/cursor-cli 0.2.1 → 0.2.2

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.
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Cursor CLI Session Watcher
3
+ *
4
+ * Polls cursor-agent JSONL transcript files for new messages and emits events.
5
+ * Used with the SDK's onWatchSession / onUnwatchSession hooks.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import { parseTranscriptFile, stableUuid } from './session-discovery';
10
+
11
+ const POLL_INTERVAL_MS = 500;
12
+ const COMPLETION_DELAY_MS = 5000;
13
+
14
+ export interface CursorSessionEvent {
15
+ type: 'USER_MESSAGE' | 'AGENT_RESPONSE';
16
+ sessionId: string;
17
+ uuid: string;
18
+ content: string;
19
+ }
20
+
21
+ export interface CursorCompletionEvent {
22
+ sessionId: string;
23
+ filePath: string;
24
+ lastMessage: string;
25
+ messageCount: number;
26
+ }
27
+
28
+ type EventCallback = (event: CursorSessionEvent) => void;
29
+ type CompletionCallback = (event: CursorCompletionEvent) => void;
30
+
31
+ interface WatchedSession {
32
+ sessionId: string;
33
+ filePath: string;
34
+ lastSize: number;
35
+ processedCount: number;
36
+ messageCount: number;
37
+ lastMessage: string;
38
+ }
39
+
40
+ export class CursorSessionWatcher {
41
+ private watchedSessions: Map<string, WatchedSession> = new Map();
42
+ private completionTimers: Map<string, NodeJS.Timeout> = new Map();
43
+ private pollTimer: NodeJS.Timeout | null = null;
44
+ private onEvent: EventCallback;
45
+ private onCompletion: CompletionCallback | null;
46
+
47
+ constructor(onEvent: EventCallback, onCompletion?: CompletionCallback) {
48
+ this.onEvent = onEvent;
49
+ this.onCompletion = onCompletion || null;
50
+ }
51
+
52
+ watchSession(sessionId: string, filePath: string): void {
53
+ if (this.watchedSessions.has(sessionId)) return;
54
+
55
+ if (!fs.existsSync(filePath)) {
56
+ console.warn(`[CursorWatcher] File not found: ${filePath}`);
57
+ return;
58
+ }
59
+
60
+ try {
61
+ const stat = fs.statSync(filePath);
62
+ const messages = parseTranscriptFile(filePath);
63
+ const lastAgent = [...messages].reverse().find(m => m.role === 'agent');
64
+
65
+ this.watchedSessions.set(sessionId, {
66
+ sessionId,
67
+ filePath,
68
+ lastSize: stat.size,
69
+ processedCount: messages.length,
70
+ messageCount: messages.length,
71
+ lastMessage: lastAgent?.content.slice(0, 200) || '',
72
+ });
73
+
74
+ console.log(`[CursorWatcher] Started watching session ${sessionId} (${messages.length} existing messages)`);
75
+
76
+ if (!this.pollTimer) this.startPolling();
77
+ } catch (err) {
78
+ console.error(`[CursorWatcher] Failed to watch ${filePath}:`, err);
79
+ }
80
+ }
81
+
82
+ unwatchSession(sessionId: string): void {
83
+ this.cancelCompletionTimer(sessionId);
84
+ if (this.watchedSessions.delete(sessionId)) {
85
+ console.log(`[CursorWatcher] Stopped watching session ${sessionId}`);
86
+ }
87
+ if (this.watchedSessions.size === 0 && this.pollTimer) {
88
+ clearInterval(this.pollTimer);
89
+ this.pollTimer = null;
90
+ }
91
+ }
92
+
93
+ unwatchAll(): void {
94
+ for (const timer of this.completionTimers.values()) clearTimeout(timer);
95
+ this.completionTimers.clear();
96
+ this.watchedSessions.clear();
97
+ if (this.pollTimer) {
98
+ clearInterval(this.pollTimer);
99
+ this.pollTimer = null;
100
+ }
101
+ }
102
+
103
+ get watchCount(): number {
104
+ return this.watchedSessions.size;
105
+ }
106
+
107
+ private startPolling(): void {
108
+ this.pollTimer = setInterval(() => {
109
+ for (const session of this.watchedSessions.values()) {
110
+ this.checkSession(session);
111
+ }
112
+ }, POLL_INTERVAL_MS);
113
+ }
114
+
115
+ private checkSession(session: WatchedSession): void {
116
+ try {
117
+ if (!fs.existsSync(session.filePath)) {
118
+ this.unwatchSession(session.sessionId);
119
+ return;
120
+ }
121
+
122
+ const stat = fs.statSync(session.filePath);
123
+ if (stat.size === session.lastSize) return;
124
+
125
+ session.lastSize = stat.size;
126
+
127
+ const allMessages = parseTranscriptFile(session.filePath);
128
+ const newMessages = allMessages.slice(session.processedCount);
129
+ if (newMessages.length === 0) return;
130
+
131
+ let sawAgent = false;
132
+
133
+ for (const msg of newMessages) {
134
+ const uuid = stableUuid(session.sessionId + ':' + msg.id);
135
+ this.onEvent({
136
+ type: msg.role === 'user' ? 'USER_MESSAGE' : 'AGENT_RESPONSE',
137
+ sessionId: session.sessionId,
138
+ uuid,
139
+ content: msg.content,
140
+ });
141
+
142
+ if (msg.role === 'agent') {
143
+ sawAgent = true;
144
+ session.lastMessage = msg.content.slice(0, 200);
145
+ }
146
+
147
+ session.messageCount++;
148
+ }
149
+
150
+ session.processedCount = allMessages.length;
151
+
152
+ if (sawAgent) this.startCompletionTimer(session);
153
+ } catch (err) {
154
+ console.error(`[CursorWatcher] Error checking session ${session.sessionId}:`, err);
155
+ }
156
+ }
157
+
158
+ private startCompletionTimer(session: WatchedSession): void {
159
+ this.cancelCompletionTimer(session.sessionId);
160
+ if (!this.onCompletion) return;
161
+
162
+ const timer = setTimeout(() => {
163
+ this.completionTimers.delete(session.sessionId);
164
+ this.onCompletion?.({
165
+ sessionId: session.sessionId,
166
+ filePath: session.filePath,
167
+ lastMessage: session.lastMessage,
168
+ messageCount: session.messageCount,
169
+ });
170
+ }, COMPLETION_DELAY_MS);
171
+
172
+ this.completionTimers.set(session.sessionId, timer);
173
+ }
174
+
175
+ private cancelCompletionTimer(sessionId: string): void {
176
+ const timer = this.completionTimers.get(sessionId);
177
+ if (timer) {
178
+ clearTimeout(timer);
179
+ this.completionTimers.delete(sessionId);
180
+ }
181
+ }
182
+ }