@cmdctrl/cursor-ide 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 (62) hide show
  1. package/dist/adapter/cdp-client.d.ts +66 -0
  2. package/dist/adapter/cdp-client.d.ts.map +1 -0
  3. package/dist/adapter/cdp-client.js +304 -0
  4. package/dist/adapter/cdp-client.js.map +1 -0
  5. package/dist/adapter/cursor-db.d.ts +114 -0
  6. package/dist/adapter/cursor-db.d.ts.map +1 -0
  7. package/dist/adapter/cursor-db.js +438 -0
  8. package/dist/adapter/cursor-db.js.map +1 -0
  9. package/dist/client/messages.d.ts +98 -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 +103 -0
  14. package/dist/client/websocket.d.ts.map +1 -0
  15. package/dist/client/websocket.js +428 -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 +86 -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 +75 -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/config/config.d.ts +68 -0
  34. package/dist/config/config.d.ts.map +1 -0
  35. package/dist/config/config.js +189 -0
  36. package/dist/config/config.js.map +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +34 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/session-discovery.d.ts +22 -0
  42. package/dist/session-discovery.d.ts.map +1 -0
  43. package/dist/session-discovery.js +90 -0
  44. package/dist/session-discovery.js.map +1 -0
  45. package/dist/session-watcher.d.ts +62 -0
  46. package/dist/session-watcher.d.ts.map +1 -0
  47. package/dist/session-watcher.js +210 -0
  48. package/dist/session-watcher.js.map +1 -0
  49. package/package.json +40 -0
  50. package/src/adapter/cdp-client.ts +296 -0
  51. package/src/adapter/cursor-db.ts +486 -0
  52. package/src/client/messages.ts +138 -0
  53. package/src/client/websocket.ts +486 -0
  54. package/src/commands/register.ts +201 -0
  55. package/src/commands/start.ts +106 -0
  56. package/src/commands/status.ts +83 -0
  57. package/src/commands/stop.ts +58 -0
  58. package/src/config/config.ts +167 -0
  59. package/src/index.ts +39 -0
  60. package/src/session-discovery.ts +115 -0
  61. package/src/session-watcher.ts +253 -0
  62. package/tsconfig.json +19 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Session watcher for monitoring Cursor session activity
3
+ *
4
+ * Simple polling-based watcher that checks the SQLite database at regular intervals.
5
+ * More reliable than fs.watch across different file systems.
6
+ */
7
+
8
+ import { CURSOR_GLOBAL_STORAGE } from './config/config';
9
+ import { getCursorDB } from './adapter/cursor-db';
10
+
11
+ export interface SessionActivityEvent {
12
+ session_id: string;
13
+ file_path: string;
14
+ last_message: string;
15
+ message_count: number;
16
+ is_completion: boolean;
17
+ user_message_uuid?: string; // UUID/ID of the triggering user message (for positioning verbose output)
18
+ }
19
+
20
+ export type SessionActivityCallback = (event: SessionActivityEvent) => void;
21
+
22
+ interface WatchedSession {
23
+ sessionId: string;
24
+ lastMessageCount: number;
25
+ lastNotifyTime?: number;
26
+ pendingAgentBubbleId?: string; // Track empty AGENT bubble waiting for content
27
+ }
28
+
29
+ // Polling interval for checking SQLite database
30
+ const POLL_INTERVAL_MS = 500;
31
+
32
+ // Minimum time between notifications for the same session (5 seconds)
33
+ const NOTIFY_COOLDOWN_MS = 5000;
34
+
35
+ /**
36
+ * Watch Cursor's SQLite database for changes using polling.
37
+ * More reliable than fs.watch on macOS.
38
+ */
39
+ export class SessionWatcher {
40
+ private watchedSessions: Map<string, WatchedSession> = new Map();
41
+ private pollTimer: NodeJS.Timeout | null = null;
42
+ private callback: SessionActivityCallback | null = null;
43
+
44
+ /**
45
+ * Start watching the database (starts the polling loop when first session is added)
46
+ */
47
+ start(callback: SessionActivityCallback): void {
48
+ this.callback = callback;
49
+ console.log('[SessionWatcher] Started watching database');
50
+ }
51
+
52
+ /**
53
+ * Stop watching
54
+ */
55
+ stop(): void {
56
+ if (this.pollTimer) {
57
+ clearInterval(this.pollTimer);
58
+ this.pollTimer = null;
59
+ }
60
+ this.watchedSessions.clear();
61
+ this.callback = null;
62
+ console.log('[SessionWatcher] Stopped watching');
63
+ }
64
+
65
+ /**
66
+ * Add a session to watch for changes
67
+ */
68
+ watchSession(sessionId: string): void {
69
+ if (this.watchedSessions.has(sessionId)) {
70
+ console.log(`[SessionWatcher] Already watching session ${sessionId}`);
71
+ return;
72
+ }
73
+
74
+ const cursorDb = getCursorDB();
75
+ const count = cursorDb.getBubbleCount(sessionId);
76
+
77
+ this.watchedSessions.set(sessionId, {
78
+ sessionId,
79
+ lastMessageCount: count,
80
+ });
81
+
82
+ console.log(`[SessionWatcher] Now watching session ${sessionId} (${count} messages)`);
83
+
84
+ // Start polling if not already running
85
+ if (!this.pollTimer) {
86
+ this.startPolling();
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Remove a session from watch list
92
+ */
93
+ unwatchSession(sessionId: string): void {
94
+ if (this.watchedSessions.delete(sessionId)) {
95
+ console.log(`[SessionWatcher] Stopped watching session ${sessionId}`);
96
+ }
97
+
98
+ // Stop polling if no sessions left
99
+ if (this.watchedSessions.size === 0 && this.pollTimer) {
100
+ clearInterval(this.pollTimer);
101
+ this.pollTimer = null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get list of watched session IDs
107
+ */
108
+ getWatchedSessions(): string[] {
109
+ return Array.from(this.watchedSessions.keys());
110
+ }
111
+
112
+ /**
113
+ * Start the polling loop
114
+ */
115
+ private startPolling(): void {
116
+ this.pollTimer = setInterval(() => {
117
+ this.pollAllSessions();
118
+ }, POLL_INTERVAL_MS);
119
+ }
120
+
121
+ /**
122
+ * Poll all watched sessions for changes
123
+ */
124
+ private pollAllSessions(): void {
125
+ if (!this.callback || this.watchedSessions.size === 0) return;
126
+
127
+ const cursorDb = getCursorDB();
128
+
129
+ for (const [sessionId, session] of this.watchedSessions) {
130
+ this.checkSession(cursorDb, session);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Check a single session for changes
136
+ */
137
+ private checkSession(cursorDb: ReturnType<typeof getCursorDB>, session: WatchedSession): void {
138
+ const currentCount = cursorDb.getBubbleCount(session.sessionId);
139
+
140
+ // Check if we're waiting for content on a pending AGENT bubble
141
+ if (session.pendingAgentBubbleId) {
142
+ const latestBubble = cursorDb.getLatestBubble(session.sessionId);
143
+ if (latestBubble && latestBubble.bubbleId === session.pendingAgentBubbleId) {
144
+ const hasContent = !!latestBubble.text?.trim();
145
+ if (hasContent) {
146
+ // Content arrived! Send completion notification
147
+ let userMessageUuid: string | undefined;
148
+ const bubbles = cursorDb.getBubbles(session.sessionId);
149
+ for (let i = bubbles.length - 1; i >= 0; i--) {
150
+ if (bubbles[i].type === 1) {
151
+ userMessageUuid = bubbles[i].bubbleId;
152
+ break;
153
+ }
154
+ }
155
+
156
+ const event: SessionActivityEvent = {
157
+ session_id: session.sessionId,
158
+ file_path: CURSOR_GLOBAL_STORAGE,
159
+ last_message: latestBubble.text?.substring(0, 100) || '',
160
+ message_count: currentCount,
161
+ is_completion: true,
162
+ user_message_uuid: userMessageUuid,
163
+ };
164
+
165
+ console.log(`[SessionWatcher] Pending AGENT bubble now has content: ${session.sessionId} (bubble: ${latestBubble.bubbleId.substring(0, 8)})`);
166
+ session.lastNotifyTime = Date.now();
167
+ session.pendingAgentBubbleId = undefined;
168
+ this.callback!(event);
169
+ return;
170
+ }
171
+ } else {
172
+ // Different bubble or bubble gone - clear pending
173
+ session.pendingAgentBubbleId = undefined;
174
+ }
175
+ }
176
+
177
+ // Only notify if there are new messages
178
+ if (currentCount > session.lastMessageCount) {
179
+ // Get latest bubble for details
180
+ const latestBubble = cursorDb.getLatestBubble(session.sessionId);
181
+ if (!latestBubble) {
182
+ session.lastMessageCount = currentCount;
183
+ return;
184
+ }
185
+
186
+ // Determine if this is a completion (assistant message with non-empty content)
187
+ // For Cursor: type 1 = user, type 2 = assistant
188
+ // Cursor creates empty assistant bubbles first, then fills them in
189
+ const hasContent = !!latestBubble.text?.trim();
190
+ const isCompletion = latestBubble.type === 2 && hasContent;
191
+ const isUserMessage = latestBubble.type === 1;
192
+
193
+ // If empty AGENT bubble, track it for later but still update count
194
+ if (latestBubble.type === 2 && !hasContent) {
195
+ console.log(`[SessionWatcher] Empty AGENT bubble detected, tracking for content: ${session.sessionId} (bubble: ${latestBubble.bubbleId.substring(0, 8)})`);
196
+ session.pendingAgentBubbleId = latestBubble.bubbleId;
197
+ session.lastMessageCount = currentCount;
198
+ return;
199
+ }
200
+
201
+ const now = Date.now();
202
+ const timeSinceLastNotify = session.lastNotifyTime ? now - session.lastNotifyTime : Infinity;
203
+
204
+ // Always notify for completions (assistant responses), cooldown only for user messages
205
+ if (isCompletion || (isUserMessage && timeSinceLastNotify >= NOTIFY_COOLDOWN_MS)) {
206
+ // Find the last USER message's bubble ID for positioning verbose output
207
+ let userMessageUuid: string | undefined;
208
+ const bubbles = cursorDb.getBubbles(session.sessionId);
209
+ for (let i = bubbles.length - 1; i >= 0; i--) {
210
+ if (bubbles[i].type === 1) { // type 1 = user
211
+ userMessageUuid = bubbles[i].bubbleId;
212
+ break;
213
+ }
214
+ }
215
+
216
+ const event: SessionActivityEvent = {
217
+ session_id: session.sessionId,
218
+ file_path: CURSOR_GLOBAL_STORAGE,
219
+ last_message: latestBubble.text?.substring(0, 100) || '',
220
+ message_count: currentCount,
221
+ is_completion: isCompletion,
222
+ user_message_uuid: userMessageUuid,
223
+ };
224
+
225
+ console.log(`[SessionWatcher] Sending activity for session ${session.sessionId} (completion: ${isCompletion}, userUuid: ${userMessageUuid}, msg: "${event.last_message.substring(0, 30)}...")`);
226
+ session.lastNotifyTime = now;
227
+ this.callback!(event);
228
+ }
229
+
230
+ session.lastMessageCount = currentCount;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Force a check of all watched sessions (clears cooldowns)
236
+ */
237
+ forceCheck(): void {
238
+ for (const session of this.watchedSessions.values()) {
239
+ session.lastNotifyTime = undefined;
240
+ }
241
+ this.pollAllSessions();
242
+ }
243
+ }
244
+
245
+ // Singleton instance
246
+ let sessionWatcherInstance: SessionWatcher | null = null;
247
+
248
+ export function getSessionWatcher(): SessionWatcher {
249
+ if (!sessionWatcherInstance) {
250
+ sessionWatcherInstance = new SessionWatcher();
251
+ }
252
+ return sessionWatcherInstance;
253
+ }
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
+ }