@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.
- package/dist/adapter/cdp-client.d.ts +66 -0
- package/dist/adapter/cdp-client.d.ts.map +1 -0
- package/dist/adapter/cdp-client.js +304 -0
- package/dist/adapter/cdp-client.js.map +1 -0
- package/dist/adapter/cursor-db.d.ts +114 -0
- package/dist/adapter/cursor-db.d.ts.map +1 -0
- package/dist/adapter/cursor-db.js +438 -0
- package/dist/adapter/cursor-db.js.map +1 -0
- package/dist/client/messages.d.ts +98 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +6 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/websocket.d.ts +103 -0
- package/dist/client/websocket.d.ts.map +1 -0
- package/dist/client/websocket.js +428 -0
- package/dist/client/websocket.js.map +1 -0
- package/dist/commands/register.d.ts +10 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +175 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/start.d.ts +9 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +86 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +75 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +5 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +59 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/config/config.d.ts +68 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +189 -0
- package/dist/config/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/session-discovery.d.ts +22 -0
- package/dist/session-discovery.d.ts.map +1 -0
- package/dist/session-discovery.js +90 -0
- package/dist/session-discovery.js.map +1 -0
- package/dist/session-watcher.d.ts +62 -0
- package/dist/session-watcher.d.ts.map +1 -0
- package/dist/session-watcher.js +210 -0
- package/dist/session-watcher.js.map +1 -0
- package/package.json +40 -0
- package/src/adapter/cdp-client.ts +296 -0
- package/src/adapter/cursor-db.ts +486 -0
- package/src/client/messages.ts +138 -0
- package/src/client/websocket.ts +486 -0
- package/src/commands/register.ts +201 -0
- package/src/commands/start.ts +106 -0
- package/src/commands/status.ts +83 -0
- package/src/commands/stop.ts +58 -0
- package/src/config/config.ts +167 -0
- package/src/index.ts +39 -0
- package/src/session-discovery.ts +115 -0
- package/src/session-watcher.ts +253 -0
- 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
|
+
}
|