@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.
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +30 -30
- package/dist/commands/start.js.map +1 -1
- package/dist/session-discovery.d.ts +62 -0
- package/dist/session-discovery.d.ts.map +1 -0
- package/dist/session-discovery.js +324 -0
- package/dist/session-discovery.js.map +1 -0
- package/dist/session-watcher.d.ts +38 -0
- package/dist/session-watcher.d.ts.map +1 -0
- package/dist/session-watcher.js +175 -0
- package/dist/session-watcher.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/start.ts +47 -36
- package/src/session-discovery.ts +328 -0
- package/src/session-watcher.ts +182 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cursor CLI Session Watcher
|
|
4
|
+
*
|
|
5
|
+
* Polls cursor-agent JSONL transcript files for new messages and emits events.
|
|
6
|
+
* Used with the SDK's onWatchSession / onUnwatchSession hooks.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.CursorSessionWatcher = void 0;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const session_discovery_1 = require("./session-discovery");
|
|
45
|
+
const POLL_INTERVAL_MS = 500;
|
|
46
|
+
const COMPLETION_DELAY_MS = 5000;
|
|
47
|
+
class CursorSessionWatcher {
|
|
48
|
+
watchedSessions = new Map();
|
|
49
|
+
completionTimers = new Map();
|
|
50
|
+
pollTimer = null;
|
|
51
|
+
onEvent;
|
|
52
|
+
onCompletion;
|
|
53
|
+
constructor(onEvent, onCompletion) {
|
|
54
|
+
this.onEvent = onEvent;
|
|
55
|
+
this.onCompletion = onCompletion || null;
|
|
56
|
+
}
|
|
57
|
+
watchSession(sessionId, filePath) {
|
|
58
|
+
if (this.watchedSessions.has(sessionId))
|
|
59
|
+
return;
|
|
60
|
+
if (!fs.existsSync(filePath)) {
|
|
61
|
+
console.warn(`[CursorWatcher] File not found: ${filePath}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.statSync(filePath);
|
|
66
|
+
const messages = (0, session_discovery_1.parseTranscriptFile)(filePath);
|
|
67
|
+
const lastAgent = [...messages].reverse().find(m => m.role === 'agent');
|
|
68
|
+
this.watchedSessions.set(sessionId, {
|
|
69
|
+
sessionId,
|
|
70
|
+
filePath,
|
|
71
|
+
lastSize: stat.size,
|
|
72
|
+
processedCount: messages.length,
|
|
73
|
+
messageCount: messages.length,
|
|
74
|
+
lastMessage: lastAgent?.content.slice(0, 200) || '',
|
|
75
|
+
});
|
|
76
|
+
console.log(`[CursorWatcher] Started watching session ${sessionId} (${messages.length} existing messages)`);
|
|
77
|
+
if (!this.pollTimer)
|
|
78
|
+
this.startPolling();
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error(`[CursorWatcher] Failed to watch ${filePath}:`, err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
unwatchSession(sessionId) {
|
|
85
|
+
this.cancelCompletionTimer(sessionId);
|
|
86
|
+
if (this.watchedSessions.delete(sessionId)) {
|
|
87
|
+
console.log(`[CursorWatcher] Stopped watching session ${sessionId}`);
|
|
88
|
+
}
|
|
89
|
+
if (this.watchedSessions.size === 0 && this.pollTimer) {
|
|
90
|
+
clearInterval(this.pollTimer);
|
|
91
|
+
this.pollTimer = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
unwatchAll() {
|
|
95
|
+
for (const timer of this.completionTimers.values())
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
this.completionTimers.clear();
|
|
98
|
+
this.watchedSessions.clear();
|
|
99
|
+
if (this.pollTimer) {
|
|
100
|
+
clearInterval(this.pollTimer);
|
|
101
|
+
this.pollTimer = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
get watchCount() {
|
|
105
|
+
return this.watchedSessions.size;
|
|
106
|
+
}
|
|
107
|
+
startPolling() {
|
|
108
|
+
this.pollTimer = setInterval(() => {
|
|
109
|
+
for (const session of this.watchedSessions.values()) {
|
|
110
|
+
this.checkSession(session);
|
|
111
|
+
}
|
|
112
|
+
}, POLL_INTERVAL_MS);
|
|
113
|
+
}
|
|
114
|
+
checkSession(session) {
|
|
115
|
+
try {
|
|
116
|
+
if (!fs.existsSync(session.filePath)) {
|
|
117
|
+
this.unwatchSession(session.sessionId);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const stat = fs.statSync(session.filePath);
|
|
121
|
+
if (stat.size === session.lastSize)
|
|
122
|
+
return;
|
|
123
|
+
session.lastSize = stat.size;
|
|
124
|
+
const allMessages = (0, session_discovery_1.parseTranscriptFile)(session.filePath);
|
|
125
|
+
const newMessages = allMessages.slice(session.processedCount);
|
|
126
|
+
if (newMessages.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
let sawAgent = false;
|
|
129
|
+
for (const msg of newMessages) {
|
|
130
|
+
const uuid = (0, session_discovery_1.stableUuid)(session.sessionId + ':' + msg.id);
|
|
131
|
+
this.onEvent({
|
|
132
|
+
type: msg.role === 'user' ? 'USER_MESSAGE' : 'AGENT_RESPONSE',
|
|
133
|
+
sessionId: session.sessionId,
|
|
134
|
+
uuid,
|
|
135
|
+
content: msg.content,
|
|
136
|
+
});
|
|
137
|
+
if (msg.role === 'agent') {
|
|
138
|
+
sawAgent = true;
|
|
139
|
+
session.lastMessage = msg.content.slice(0, 200);
|
|
140
|
+
}
|
|
141
|
+
session.messageCount++;
|
|
142
|
+
}
|
|
143
|
+
session.processedCount = allMessages.length;
|
|
144
|
+
if (sawAgent)
|
|
145
|
+
this.startCompletionTimer(session);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(`[CursorWatcher] Error checking session ${session.sessionId}:`, err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
startCompletionTimer(session) {
|
|
152
|
+
this.cancelCompletionTimer(session.sessionId);
|
|
153
|
+
if (!this.onCompletion)
|
|
154
|
+
return;
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
this.completionTimers.delete(session.sessionId);
|
|
157
|
+
this.onCompletion?.({
|
|
158
|
+
sessionId: session.sessionId,
|
|
159
|
+
filePath: session.filePath,
|
|
160
|
+
lastMessage: session.lastMessage,
|
|
161
|
+
messageCount: session.messageCount,
|
|
162
|
+
});
|
|
163
|
+
}, COMPLETION_DELAY_MS);
|
|
164
|
+
this.completionTimers.set(session.sessionId, timer);
|
|
165
|
+
}
|
|
166
|
+
cancelCompletionTimer(sessionId) {
|
|
167
|
+
const timer = this.completionTimers.get(sessionId);
|
|
168
|
+
if (timer) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
this.completionTimers.delete(sessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.CursorSessionWatcher = CursorSessionWatcher;
|
|
175
|
+
//# sourceMappingURL=session-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-watcher.js","sourceRoot":"","sources":["../src/session-watcher.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,uCAAyB;AACzB,2DAAsE;AAEtE,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,mBAAmB,GAAG,IAAI,CAAC;AA4BjC,MAAa,oBAAoB;IACvB,eAAe,GAAgC,IAAI,GAAG,EAAE,CAAC;IACzD,gBAAgB,GAAgC,IAAI,GAAG,EAAE,CAAC;IAC1D,SAAS,GAA0B,IAAI,CAAC;IACxC,OAAO,CAAgB;IACvB,YAAY,CAA4B;IAEhD,YAAY,OAAsB,EAAE,YAAiC;QACnE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,YAAY,IAAI,IAAI,CAAC;IAC3C,CAAC;IAED,YAAY,CAAC,SAAiB,EAAE,QAAgB;QAC9C,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO;QAEhD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,OAAO,CAAC,IAAI,CAAC,mCAAmC,QAAQ,EAAE,CAAC,CAAC;YAC5D,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACnC,MAAM,QAAQ,GAAG,IAAA,uCAAmB,EAAC,QAAQ,CAAC,CAAC;YAC/C,MAAM,SAAS,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;YAExE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE;gBAClC,SAAS;gBACT,QAAQ;gBACR,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,cAAc,EAAE,QAAQ,CAAC,MAAM;gBAC/B,YAAY,EAAE,QAAQ,CAAC,MAAM;gBAC7B,WAAW,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE;aACpD,CAAC,CAAC;YAEH,OAAO,CAAC,GAAG,CAAC,4CAA4C,SAAS,KAAK,QAAQ,CAAC,MAAM,qBAAqB,CAAC,CAAC;YAE5G,IAAI,CAAC,IAAI,CAAC,SAAS;gBAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,mCAAmC,QAAQ,GAAG,EAAE,GAAG,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,cAAc,CAAC,SAAiB;QAC9B,IAAI,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,4CAA4C,SAAS,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,IAAI,CAAC,eAAe,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtD,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC;IAED,UAAU;QACR,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACxE,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC;IACnC,CAAC;IAEO,YAAY;QAClB,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;YAChC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC;gBACpD,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACvB,CAAC;IAEO,YAAY,CAAC,OAAuB;QAC1C,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBACvC,OAAO;YACT,CAAC;YAED,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC3C,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,QAAQ;gBAAE,OAAO;YAE3C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;YAE7B,MAAM,WAAW,GAAG,IAAA,uCAAmB,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC1D,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;YAC9D,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAErC,IAAI,QAAQ,GAAG,KAAK,CAAC;YAErB,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;gBAC9B,MAAM,IAAI,GAAG,IAAA,8BAAU,EAAC,OAAO,CAAC,SAAS,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC1D,IAAI,CAAC,OAAO,CAAC;oBACX,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,gBAAgB;oBAC7D,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,IAAI;oBACJ,OAAO,EAAE,GAAG,CAAC,OAAO;iBACrB,CAAC,CAAC;gBAEH,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;oBACzB,QAAQ,GAAG,IAAI,CAAC;oBAChB,OAAO,CAAC,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;gBAClD,CAAC;gBAED,OAAO,CAAC,YAAY,EAAE,CAAC;YACzB,CAAC;YAED,OAAO,CAAC,cAAc,GAAG,WAAW,CAAC,MAAM,CAAC;YAE5C,IAAI,QAAQ;gBAAE,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,0CAA0C,OAAO,CAAC,SAAS,GAAG,EAAE,GAAG,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IAEO,oBAAoB,CAAC,OAAuB;QAClD,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,YAAY;YAAE,OAAO;QAE/B,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAChD,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,YAAY,EAAE,OAAO,CAAC,YAAY;aACnC,CAAC,CAAC;QACL,CAAC,EAAE,mBAAmB,CAAC,CAAC;QAExB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACtD,CAAC;IAEO,qBAAqB,CAAC,SAAiB;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACnD,IAAI,KAAK,EAAE,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;CACF;AA9ID,oDA8IC"}
|
package/package.json
CHANGED
package/src/commands/start.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { DaemonClient, ConfigManager } from '@cmdctrl/daemon-sdk';
|
|
4
4
|
import { CursorAdapter } from '../adapter/cursor-cli';
|
|
5
|
-
import {
|
|
5
|
+
import { discoverSessions, readSessionMessages } from '../session-discovery';
|
|
6
|
+
import { CursorSessionWatcher } from '../session-watcher';
|
|
6
7
|
|
|
7
8
|
const configManager = new ConfigManager('cursor-cli');
|
|
8
9
|
|
|
@@ -39,9 +40,33 @@ export async function start(): Promise<void> {
|
|
|
39
40
|
|
|
40
41
|
configManager.writePidFile(process.pid);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
43
|
+
// Managed session IDs (started via task_start) – excluded from native discovery
|
|
44
|
+
const managedSessionIds = new Set<string>();
|
|
45
|
+
|
|
46
|
+
const sessionWatcher = new CursorSessionWatcher(
|
|
47
|
+
(event) => {
|
|
48
|
+
// Only send activity for user messages – agent responses are the final
|
|
49
|
+
// answer already shown via the transcript; sending them here duplicates.
|
|
50
|
+
if (event.type !== 'USER_MESSAGE') return;
|
|
51
|
+
client.sendSessionActivity(
|
|
52
|
+
event.sessionId,
|
|
53
|
+
'',
|
|
54
|
+
event.content,
|
|
55
|
+
1,
|
|
56
|
+
false,
|
|
57
|
+
new Date().toISOString()
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
(completion) => {
|
|
61
|
+
client.sendSessionActivity(
|
|
62
|
+
completion.sessionId,
|
|
63
|
+
completion.filePath,
|
|
64
|
+
completion.lastMessage,
|
|
65
|
+
completion.messageCount,
|
|
66
|
+
true
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
45
70
|
|
|
46
71
|
// Event callback wired into the DaemonClient below
|
|
47
72
|
let sendEvent: (taskId: string, eventType: string, data: Record<string, unknown>) => void;
|
|
@@ -49,21 +74,8 @@ export async function start(): Promise<void> {
|
|
|
49
74
|
const adapter = new CursorAdapter((taskId, eventType, data) => {
|
|
50
75
|
const sessionId = data.session_id as string | undefined;
|
|
51
76
|
|
|
52
|
-
// Store initial user message when session starts
|
|
53
77
|
if (eventType === 'SESSION_STARTED' && sessionId) {
|
|
54
|
-
|
|
55
|
-
if (instruction) {
|
|
56
|
-
messageStore.storeMessage(sessionId, 'USER', instruction);
|
|
57
|
-
pendingInstructions.delete(taskId);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Store agent response on completion
|
|
62
|
-
if (eventType === 'TASK_COMPLETE' && data.result) {
|
|
63
|
-
const sid = (data.session_id as string) || taskSessionMap.get(taskId);
|
|
64
|
-
if (sid) {
|
|
65
|
-
messageStore.storeMessage(sid, 'AGENT', data.result as string);
|
|
66
|
-
}
|
|
78
|
+
managedSessionIds.add(sessionId);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
sendEvent(taskId, eventType, data);
|
|
@@ -77,10 +89,13 @@ export async function start(): Promise<void> {
|
|
|
77
89
|
version: daemonVersion,
|
|
78
90
|
});
|
|
79
91
|
|
|
80
|
-
|
|
92
|
+
client.setSessionsProvider(() => discoverSessions(managedSessionIds));
|
|
93
|
+
|
|
81
94
|
sendEvent = (taskId, eventType, data) => {
|
|
82
|
-
//
|
|
83
|
-
//
|
|
95
|
+
// cursor-agent writes all content to transcript files – suppress OUTPUT events
|
|
96
|
+
// and strip result from TASK_COMPLETE to avoid duplicating transcript content.
|
|
97
|
+
if (eventType === 'OUTPUT') return;
|
|
98
|
+
if (eventType === 'TASK_COMPLETE') data = { ...data, result: '' };
|
|
84
99
|
(client as any).send({
|
|
85
100
|
type: 'event',
|
|
86
101
|
task_id: taskId,
|
|
@@ -89,8 +104,15 @@ export async function start(): Promise<void> {
|
|
|
89
104
|
});
|
|
90
105
|
};
|
|
91
106
|
|
|
107
|
+
client.onWatchSession((sessionId, filePath) => {
|
|
108
|
+
sessionWatcher.watchSession(sessionId, filePath);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
client.onUnwatchSession((sessionId) => {
|
|
112
|
+
sessionWatcher.unwatchSession(sessionId);
|
|
113
|
+
});
|
|
114
|
+
|
|
92
115
|
client.onTaskStart(async (task) => {
|
|
93
|
-
pendingInstructions.set(task.taskId, task.instruction);
|
|
94
116
|
try {
|
|
95
117
|
await adapter.startTask(task.taskId, task.instruction, task.projectPath);
|
|
96
118
|
} catch (err: unknown) {
|
|
@@ -99,8 +121,6 @@ export async function start(): Promise<void> {
|
|
|
99
121
|
});
|
|
100
122
|
|
|
101
123
|
client.onTaskResume(async (task) => {
|
|
102
|
-
messageStore.storeMessage(task.sessionId, 'USER', task.message);
|
|
103
|
-
taskSessionMap.set(task.taskId, task.sessionId);
|
|
104
124
|
try {
|
|
105
125
|
await adapter.resumeTask(task.taskId, task.sessionId, task.message, task.projectPath);
|
|
106
126
|
} catch (err: unknown) {
|
|
@@ -112,19 +132,9 @@ export async function start(): Promise<void> {
|
|
|
112
132
|
await adapter.cancelTask(taskId);
|
|
113
133
|
});
|
|
114
134
|
|
|
135
|
+
// cursor-agent always writes to transcript files – use them as the single source of truth
|
|
115
136
|
client.onGetMessages((req) => {
|
|
116
|
-
|
|
117
|
-
req.sessionId,
|
|
118
|
-
req.limit,
|
|
119
|
-
req.beforeUuid,
|
|
120
|
-
req.afterUuid
|
|
121
|
-
);
|
|
122
|
-
return {
|
|
123
|
-
messages: result.messages,
|
|
124
|
-
hasMore: result.hasMore,
|
|
125
|
-
oldestUuid: result.oldestUuid,
|
|
126
|
-
newestUuid: result.newestUuid,
|
|
127
|
-
};
|
|
137
|
+
return readSessionMessages(req.sessionId, req.limit, req.beforeUuid, req.afterUuid);
|
|
128
138
|
});
|
|
129
139
|
|
|
130
140
|
client.onVersionStatus((msg) => {
|
|
@@ -135,6 +145,7 @@ export async function start(): Promise<void> {
|
|
|
135
145
|
|
|
136
146
|
const shutdown = async () => {
|
|
137
147
|
console.log('\nShutting down...');
|
|
148
|
+
sessionWatcher.unwatchAll();
|
|
138
149
|
await adapter.stopAll();
|
|
139
150
|
await client.disconnect();
|
|
140
151
|
configManager.deletePidFile();
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor CLI Session Discovery
|
|
3
|
+
*
|
|
4
|
+
* Scans ~/.cursor/projects/<encoded-path>/agent-transcripts/<session-id>.jsonl
|
|
5
|
+
* to discover existing cursor-agent sessions.
|
|
6
|
+
*
|
|
7
|
+
* File format – each line is a JSON object:
|
|
8
|
+
* { role: "user" | "assistant", message: { content: [{ type: "text", text: "..." }] } }
|
|
9
|
+
*
|
|
10
|
+
* User messages have text wrapped in <user_query>...</user_query> tags.
|
|
11
|
+
* Session ID = filename (UUID, without .jsonl).
|
|
12
|
+
* Project path = decoded from the project directory name (hyphens → slashes).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as crypto from 'crypto';
|
|
19
|
+
|
|
20
|
+
const ACTIVE_THRESHOLD_MS = 30 * 1000;
|
|
21
|
+
|
|
22
|
+
export interface ExternalSession {
|
|
23
|
+
session_id: string;
|
|
24
|
+
slug: string;
|
|
25
|
+
title: string;
|
|
26
|
+
project: string;
|
|
27
|
+
project_name: string;
|
|
28
|
+
file_path: string;
|
|
29
|
+
last_message: string;
|
|
30
|
+
last_activity: string;
|
|
31
|
+
is_active: boolean;
|
|
32
|
+
message_count: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedMessage {
|
|
36
|
+
id: string;
|
|
37
|
+
role: 'user' | 'agent';
|
|
38
|
+
content: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Cache: file path → { session, fileMtime }
|
|
42
|
+
const sessionCache = new Map<string, { session: ExternalSession; fileMtime: number }>();
|
|
43
|
+
|
|
44
|
+
// Cache: file path → { messages, fileMtime }
|
|
45
|
+
const messageCache = new Map<string, { messages: ParsedMessage[]; fileMtime: number }>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Strip <user_query>...</user_query> wrapper added by cursor-agent.
|
|
49
|
+
*/
|
|
50
|
+
function stripUserQueryTags(text: string): string {
|
|
51
|
+
return text.replace(/^\s*<user_query>\s*/i, '').replace(/\s*<\/user_query>\s*$/i, '').trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract plain text from a cursor-agent message content array.
|
|
56
|
+
*/
|
|
57
|
+
function extractText(content: Array<{ type: string; text?: string }>): string {
|
|
58
|
+
return content
|
|
59
|
+
.filter(b => b.type === 'text' && b.text)
|
|
60
|
+
.map(b => b.text!)
|
|
61
|
+
.join('')
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse all messages from a cursor-agent transcript JSONL file.
|
|
67
|
+
*/
|
|
68
|
+
export function parseTranscriptFile(filePath: string): ParsedMessage[] {
|
|
69
|
+
try {
|
|
70
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
71
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
72
|
+
const messages: ParsedMessage[] = [];
|
|
73
|
+
let idx = 0;
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
try {
|
|
77
|
+
const obj = JSON.parse(line) as {
|
|
78
|
+
role: string;
|
|
79
|
+
message: { content: Array<{ type: string; text?: string }> };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (!obj.role || !obj.message?.content) continue;
|
|
83
|
+
|
|
84
|
+
let text = extractText(obj.message.content);
|
|
85
|
+
if (!text) continue;
|
|
86
|
+
|
|
87
|
+
if (obj.role === 'user') {
|
|
88
|
+
text = stripUserQueryTags(text);
|
|
89
|
+
if (!text) continue;
|
|
90
|
+
messages.push({ id: `user-${idx++}`, role: 'user', content: text });
|
|
91
|
+
} else if (obj.role === 'assistant') {
|
|
92
|
+
// cursor-agent appends thinking after the first blank line – keep only the answer
|
|
93
|
+
const answerEnd = text.indexOf('\n\n');
|
|
94
|
+
if (answerEnd !== -1) text = text.slice(0, answerEnd).trim();
|
|
95
|
+
if (!text) continue;
|
|
96
|
+
messages.push({ id: `agent-${idx++}`, role: 'agent', content: text });
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// skip invalid lines
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return messages;
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Attempt to decode a cursor project directory name back to a filesystem path.
|
|
111
|
+
* The encoding replaces '/' with '-' and drops the leading '/'.
|
|
112
|
+
* e.g. "Users-mrwoof-src-testing" → "/Users/mrwoof/src/testing"
|
|
113
|
+
*
|
|
114
|
+
* We try all possible slash placements and return the first existing path.
|
|
115
|
+
* Falls back to returning the encoded name if nothing exists.
|
|
116
|
+
*/
|
|
117
|
+
function decodeProjectPath(encoded: string): string {
|
|
118
|
+
// Simple heuristic: replace all hyphens with slashes and prepend /
|
|
119
|
+
const candidate = '/' + encoded.replace(/-/g, '/');
|
|
120
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
121
|
+
|
|
122
|
+
// Walk all subdirs of ~/ looking for the encoded name match
|
|
123
|
+
// (handles paths with hyphens in component names by trying common prefixes)
|
|
124
|
+
const home = os.homedir();
|
|
125
|
+
const homeEncoded = home.replace(/^\//, '').replace(/\//g, '-');
|
|
126
|
+
if (encoded.startsWith(homeEncoded + '-')) {
|
|
127
|
+
const rest = encoded.slice(homeEncoded.length + 1);
|
|
128
|
+
const restPath = rest.replace(/-/g, '/');
|
|
129
|
+
const tryPath = path.join(home, restPath);
|
|
130
|
+
if (fs.existsSync(tryPath)) return tryPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return candidate;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate title from first user message.
|
|
138
|
+
*/
|
|
139
|
+
function generateTitle(text: string): string {
|
|
140
|
+
const firstLine = text.split('\n')[0].trim();
|
|
141
|
+
if (firstLine.length <= 50) return firstLine;
|
|
142
|
+
const truncated = firstLine.slice(0, 50);
|
|
143
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
144
|
+
if (lastSpace > 30) return truncated.slice(0, lastSpace) + '...';
|
|
145
|
+
return truncated + '...';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Discover all cursor-agent sessions on this device.
|
|
150
|
+
* Scans ~/.cursor/projects/<project>/agent-transcripts/<session>.jsonl
|
|
151
|
+
*/
|
|
152
|
+
export function discoverSessions(excludeSessionIDs: Set<string> = new Set()): ExternalSession[] {
|
|
153
|
+
const projectsDir = path.join(os.homedir(), '.cursor', 'projects');
|
|
154
|
+
const sessions: ExternalSession[] = [];
|
|
155
|
+
|
|
156
|
+
if (!fs.existsSync(projectsDir)) return sessions;
|
|
157
|
+
|
|
158
|
+
let projectDirs: string[];
|
|
159
|
+
try {
|
|
160
|
+
projectDirs = fs.readdirSync(projectsDir);
|
|
161
|
+
} catch {
|
|
162
|
+
return sessions;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const projectDir of projectDirs) {
|
|
166
|
+
const transcriptsDir = path.join(projectsDir, projectDir, 'agent-transcripts');
|
|
167
|
+
if (!fs.existsSync(transcriptsDir)) continue;
|
|
168
|
+
|
|
169
|
+
const projectPath = decodeProjectPath(projectDir);
|
|
170
|
+
const projectName = path.basename(projectPath);
|
|
171
|
+
|
|
172
|
+
let transcriptFiles: string[];
|
|
173
|
+
try {
|
|
174
|
+
transcriptFiles = fs.readdirSync(transcriptsDir).filter(f => f.endsWith('.jsonl'));
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const file of transcriptFiles) {
|
|
180
|
+
const sessionId = file.replace('.jsonl', '');
|
|
181
|
+
if (excludeSessionIDs.has(sessionId)) continue;
|
|
182
|
+
|
|
183
|
+
const filePath = path.join(transcriptsDir, file);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const stat = fs.statSync(filePath);
|
|
187
|
+
const fileMtime = stat.mtimeMs;
|
|
188
|
+
|
|
189
|
+
const cached = sessionCache.get(filePath);
|
|
190
|
+
if (cached && cached.fileMtime === fileMtime) {
|
|
191
|
+
const session = { ...cached.session };
|
|
192
|
+
session.is_active = Date.now() - new Date(session.last_activity).getTime() < ACTIVE_THRESHOLD_MS;
|
|
193
|
+
sessions.push(session);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const messages = parseTranscriptFile(filePath);
|
|
198
|
+
if (messages.length === 0) continue;
|
|
199
|
+
|
|
200
|
+
const firstUser = messages.find(m => m.role === 'user');
|
|
201
|
+
const lastUser = [...messages].reverse().find(m => m.role === 'user');
|
|
202
|
+
const title = generateTitle(firstUser?.content || '') || sessionId.slice(0, 8);
|
|
203
|
+
const lastMessage = lastUser?.content.slice(0, 100) || '';
|
|
204
|
+
const lastActivity = new Date(stat.mtimeMs).toISOString();
|
|
205
|
+
const isActive = Date.now() - stat.mtimeMs < ACTIVE_THRESHOLD_MS;
|
|
206
|
+
|
|
207
|
+
const session: ExternalSession = {
|
|
208
|
+
session_id: sessionId,
|
|
209
|
+
slug: '',
|
|
210
|
+
title,
|
|
211
|
+
project: projectPath,
|
|
212
|
+
project_name: projectName,
|
|
213
|
+
file_path: filePath,
|
|
214
|
+
last_message: lastMessage,
|
|
215
|
+
last_activity: lastActivity,
|
|
216
|
+
is_active: isActive,
|
|
217
|
+
message_count: messages.length,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
sessionCache.set(filePath, { session, fileMtime });
|
|
221
|
+
sessions.push(session);
|
|
222
|
+
} catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
sessions.sort((a, b) =>
|
|
229
|
+
new Date(b.last_activity).getTime() - new Date(a.last_activity).getTime()
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
return sessions;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Find the file path for a given session ID.
|
|
237
|
+
*/
|
|
238
|
+
export function findSessionFile(sessionId: string): string | null {
|
|
239
|
+
for (const [filePath, cached] of sessionCache.entries()) {
|
|
240
|
+
if (cached.session.session_id === sessionId) return filePath;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const projectsDir = path.join(os.homedir(), '.cursor', 'projects');
|
|
244
|
+
if (!fs.existsSync(projectsDir)) return null;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
for (const projectDir of fs.readdirSync(projectsDir)) {
|
|
248
|
+
const candidate = path.join(projectsDir, projectDir, 'agent-transcripts', `${sessionId}.jsonl`);
|
|
249
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
// ignore
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Read messages from a cursor-agent session for the CmdCtrl get_messages protocol.
|
|
260
|
+
*/
|
|
261
|
+
export function readSessionMessages(
|
|
262
|
+
sessionId: string,
|
|
263
|
+
limit: number,
|
|
264
|
+
beforeUuid?: string,
|
|
265
|
+
afterUuid?: string
|
|
266
|
+
): { messages: Array<{ uuid: string; role: 'USER' | 'AGENT'; content: string; timestamp: string }>; hasMore: boolean; oldestUuid?: string; newestUuid?: string } {
|
|
267
|
+
const filePath = findSessionFile(sessionId);
|
|
268
|
+
if (!filePath) return { messages: [], hasMore: false };
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const stat = fs.statSync(filePath);
|
|
272
|
+
const fileMtime = stat.mtimeMs;
|
|
273
|
+
|
|
274
|
+
let parsed: ParsedMessage[];
|
|
275
|
+
const cached = messageCache.get(filePath);
|
|
276
|
+
if (cached && cached.fileMtime === fileMtime) {
|
|
277
|
+
parsed = cached.messages;
|
|
278
|
+
} else {
|
|
279
|
+
parsed = parseTranscriptFile(filePath);
|
|
280
|
+
messageCache.set(filePath, { messages: parsed, fileMtime });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Assign sequential timestamps 1s apart, ending at file mtime, to preserve order
|
|
284
|
+
const total = parsed.filter(m => m.content.length > 0).length;
|
|
285
|
+
let seq = 0;
|
|
286
|
+
let messages = parsed.map(msg => ({
|
|
287
|
+
uuid: stableUuid(sessionId + ':' + msg.id),
|
|
288
|
+
role: (msg.role === 'user' ? 'USER' : 'AGENT') as 'USER' | 'AGENT',
|
|
289
|
+
content: msg.content,
|
|
290
|
+
timestamp: new Date(stat.mtimeMs - (total - seq++) * 1000).toISOString(),
|
|
291
|
+
})).filter(m => m.content.length > 0);
|
|
292
|
+
|
|
293
|
+
if (beforeUuid) {
|
|
294
|
+
const idx = messages.findIndex(m => m.uuid === beforeUuid);
|
|
295
|
+
if (idx > 0) messages = messages.slice(0, idx);
|
|
296
|
+
}
|
|
297
|
+
if (afterUuid) {
|
|
298
|
+
const idx = messages.findIndex(m => m.uuid === afterUuid);
|
|
299
|
+
if (idx >= 0) messages = messages.slice(idx + 1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const hasMore = messages.length > limit;
|
|
303
|
+
const limited = messages.slice(-limit);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
messages: limited,
|
|
307
|
+
hasMore,
|
|
308
|
+
oldestUuid: limited[0]?.uuid,
|
|
309
|
+
newestUuid: limited[limited.length - 1]?.uuid,
|
|
310
|
+
};
|
|
311
|
+
} catch {
|
|
312
|
+
return { messages: [], hasMore: false };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Generate a stable UUID from an input string.
|
|
318
|
+
*/
|
|
319
|
+
export function stableUuid(input: string): string {
|
|
320
|
+
const hash = crypto.createHash('sha256').update(input).digest('hex');
|
|
321
|
+
return [
|
|
322
|
+
hash.slice(0, 8),
|
|
323
|
+
hash.slice(8, 12),
|
|
324
|
+
'4' + hash.slice(13, 16),
|
|
325
|
+
'8' + hash.slice(17, 20),
|
|
326
|
+
hash.slice(20, 32),
|
|
327
|
+
].join('-');
|
|
328
|
+
}
|