botmux 2.60.0 → 2.61.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/README.en.md +3 -3
- package/README.md +4 -4
- package/dist/bot-registry.d.ts +9 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +16 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/core/command-discovery.d.ts +24 -0
- package/dist/core/command-discovery.d.ts.map +1 -0
- package/dist/core/command-discovery.js +201 -0
- package/dist/core/command-discovery.js.map +1 -0
- package/dist/core/command-handler.d.ts +9 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +50 -3
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +8 -1
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.js +3 -3
- package/dist/daemon.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +12 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +12 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +17 -0
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +84 -0
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/services/codex-bridge-queue.d.ts +4 -0
- package/dist/services/codex-bridge-queue.d.ts.map +1 -1
- package/dist/services/codex-bridge-queue.js +117 -84
- package/dist/services/codex-bridge-queue.js.map +1 -1
- package/dist/services/cursor-transcript.d.ts +47 -0
- package/dist/services/cursor-transcript.d.ts.map +1 -0
- package/dist/services/cursor-transcript.js +324 -0
- package/dist/services/cursor-transcript.js.map +1 -0
- package/dist/worker.js +146 -5
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for Cursor Agent's per-chat transcript JSONL.
|
|
3
|
+
*
|
|
4
|
+
* `cursor-agent` keeps each chat's authoritative store in a SQLite file
|
|
5
|
+
* ~/.cursor/chats/<projectHash>/<chatId>/store.db
|
|
6
|
+
* (held open via fd for the whole session) and, in parallel, mirrors the
|
|
7
|
+
* conversation into an append-only JSONL transcript at
|
|
8
|
+
* ~/.cursor/projects/<projectSlug>/agent-transcripts/<chatId>/<chatId>.jsonl
|
|
9
|
+
*
|
|
10
|
+
* The bridge reads the JSONL (not the SQLite store) because it's append-only
|
|
11
|
+
* plain text — the same integration surface the Codex/CoCo bridges use. Each
|
|
12
|
+
* line is `{ role: 'user' | 'assistant', message: { content: [...] } }` where
|
|
13
|
+
* a content block is either `{ type: 'text', text }` or `{ type: 'tool_use',
|
|
14
|
+
* name, input }`. Tool *results* are not recorded.
|
|
15
|
+
*
|
|
16
|
+
* Where Cursor sits between the two existing bridge transcript shapes:
|
|
17
|
+
* - Claude is a STREAMING event stream — one role:user event, then a run of
|
|
18
|
+
* role:assistant events whose text grows incrementally; a turn has no
|
|
19
|
+
* explicit terminator, so the bridge queue tracks the in-flight turn with
|
|
20
|
+
* a `collecting` pointer.
|
|
21
|
+
* - Codex is DISCRETE complete events — exactly one user_message and one
|
|
22
|
+
* assistant_final per turn, each carrying the full text, with a definite
|
|
23
|
+
* terminator (phase=final_answer).
|
|
24
|
+
* Cursor is a hybrid: each JSONL line is a DISCRETE, complete event (verified
|
|
25
|
+
* empirically — assistant lines are never growing prefixes of one another, so
|
|
26
|
+
* there is no Claude-style snapshot replay risk), but a turn is composed of
|
|
27
|
+
* MANY assistant lines (one per step). Crucially it still has a definite
|
|
28
|
+
* terminator: every intermediate step pairs its narration with a `tool_use`
|
|
29
|
+
* block, and the agent loop only stops when the model returns a message with
|
|
30
|
+
* NO tool_use. So a `text`-only assistant line is the end-of-turn final reply:
|
|
31
|
+
* - role=user → the user's prompt
|
|
32
|
+
* - role=assistant, text & no tool_use → the model's final reply
|
|
33
|
+
* Every line carrying a tool_use block is an intermediate step and is dropped.
|
|
34
|
+
* This lets the reader distill Cursor's multi-event turn down to Codex's
|
|
35
|
+
* two-event (user, assistant_final) shape, so it can reuse the proven
|
|
36
|
+
* CodexBridgeQueue attribution as-is rather than a Claude-style streaming
|
|
37
|
+
* accumulator.
|
|
38
|
+
*
|
|
39
|
+
* Consequences of that distillation (intentional):
|
|
40
|
+
* - Only the final wrap-up text is forwarded; the short per-step narrations
|
|
41
|
+
* ("Let me read…", "Now I'll check…") are deliberately not relayed to Lark.
|
|
42
|
+
* - An interrupted turn (process killed / Esc mid-tool, leaving no text-only
|
|
43
|
+
* line) emits NOTHING rather than a half-answer — the safe failure mode.
|
|
44
|
+
*
|
|
45
|
+
* Cursor's JSONL carries no per-event timestamp, so the worker baselines this
|
|
46
|
+
* transcript by byte offset at adopt time (history is behind the offset and
|
|
47
|
+
* never re-ingested) and stamps live events with the drain wall-clock. That's
|
|
48
|
+
* why every emitted event uses `Date.now()` for `timestampMs` — enough for the
|
|
49
|
+
* shared CodexBridgeQueue's freshness gates given the offset baseline.
|
|
50
|
+
*
|
|
51
|
+
* Pure I/O. Attribution belongs in CodexBridgeQueue.
|
|
52
|
+
*/
|
|
53
|
+
import { existsSync, statSync, openSync, readSync, closeSync, readdirSync, readlinkSync } from 'node:fs';
|
|
54
|
+
import { execSync } from 'node:child_process';
|
|
55
|
+
import { homedir, platform } from 'node:os';
|
|
56
|
+
import { join } from 'node:path';
|
|
57
|
+
const IS_LINUX = platform() === 'linux';
|
|
58
|
+
const CHAT_ID_RE = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
59
|
+
/** Default `~/.cursor/projects` root. Overridable by callers (tests) so the
|
|
60
|
+
* scan doesn't depend on a real home directory. */
|
|
61
|
+
export function cursorProjectsRoot() {
|
|
62
|
+
return join(homedir(), '.cursor', 'projects');
|
|
63
|
+
}
|
|
64
|
+
/** Extract the chatId encoded in a Cursor store.db path of the shape
|
|
65
|
+
* `.../.cursor/chats/<projectHash>/<chatId>/store.db` (also matches the
|
|
66
|
+
* `-wal` / `-shm` sidecar files SQLite keeps open). The chatId is the same
|
|
67
|
+
* UUID used to name the agent-transcript JSONL, so it's the bridge between
|
|
68
|
+
* the open fd and the transcript file. Returns undefined for non-matching
|
|
69
|
+
* paths. */
|
|
70
|
+
export function cursorChatIdFromStoreDbPath(path) {
|
|
71
|
+
const re = new RegExp(`/\\.cursor/chats/[^/]+/(${CHAT_ID_RE})/store\\.db(?:-wal|-shm)?$`, 'i');
|
|
72
|
+
const m = re.exec(path);
|
|
73
|
+
return m ? m[1] : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Find the chatId of an externally-running cursor-agent process by reading
|
|
76
|
+
* the store.db file it keeps open. cursor-agent holds an fd on its current
|
|
77
|
+
* chat's SQLite store for the whole session lifetime, which makes this the
|
|
78
|
+
* authoritative pid→chatId binding — far more reliable than scanning chat
|
|
79
|
+
* dirs by mtime (which would race with sibling cursor-agent panes).
|
|
80
|
+
*
|
|
81
|
+
* Linux: `/proc/<pid>/fd/*` fast path. macOS / BSD: `lsof -p <pid> -Fn`
|
|
82
|
+
* fallback (same shape as codex-transcript.findCodexRolloutByPid). */
|
|
83
|
+
export function findCursorChatIdByPid(pid) {
|
|
84
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (IS_LINUX) {
|
|
87
|
+
const fdDir = `/proc/${pid}/fd`;
|
|
88
|
+
if (existsSync(fdDir)) {
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = readdirSync(fdDir);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
for (const fd of entries) {
|
|
97
|
+
let target;
|
|
98
|
+
try {
|
|
99
|
+
target = readlinkSync(join(fdDir, fd));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const chatId = cursorChatIdFromStoreDbPath(target);
|
|
105
|
+
if (chatId)
|
|
106
|
+
return chatId;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
// /proc unreadable — fall through to lsof.
|
|
111
|
+
}
|
|
112
|
+
let out;
|
|
113
|
+
try {
|
|
114
|
+
out = execSync(`lsof -p ${pid} -Fn`, {
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
for (const line of out.split('\n')) {
|
|
123
|
+
if (!line.startsWith('n/'))
|
|
124
|
+
continue;
|
|
125
|
+
const chatId = cursorChatIdFromStoreDbPath(line.slice(1));
|
|
126
|
+
if (chatId)
|
|
127
|
+
return chatId;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/** Locate the agent-transcript JSONL for a given chatId. The chatId is a
|
|
132
|
+
* globally-unique UUID, so a one-shot scan of the (small) projects root for
|
|
133
|
+
* `<slug>/agent-transcripts/<chatId>/<chatId>.jsonl` is unambiguous and
|
|
134
|
+
* avoids having to reproduce Cursor's opaque cwd→slug hashing. */
|
|
135
|
+
export function findCursorTranscriptByChatId(chatId, projectsRoot = cursorProjectsRoot()) {
|
|
136
|
+
if (!chatId || !existsSync(projectsRoot))
|
|
137
|
+
return undefined;
|
|
138
|
+
let slugs;
|
|
139
|
+
try {
|
|
140
|
+
slugs = readdirSync(projectsRoot);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
for (const slug of slugs) {
|
|
146
|
+
const candidate = join(projectsRoot, slug, 'agent-transcripts', chatId, `${chatId}.jsonl`);
|
|
147
|
+
if (existsSync(candidate))
|
|
148
|
+
return candidate;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
/** Resolve the transcript path for an externally-running cursor-agent pid:
|
|
153
|
+
* pid → open store.db → chatId → agent-transcript JSONL. Returns both the
|
|
154
|
+
* path and the chatId so the caller can remember the chatId for a later
|
|
155
|
+
* retry if the JSONL isn't on disk yet. */
|
|
156
|
+
export function findCursorTranscriptByPid(pid, projectsRoot = cursorProjectsRoot()) {
|
|
157
|
+
const chatId = findCursorChatIdByPid(pid);
|
|
158
|
+
if (!chatId)
|
|
159
|
+
return undefined;
|
|
160
|
+
const path = findCursorTranscriptByChatId(chatId, projectsRoot);
|
|
161
|
+
return path ? { path, chatId } : undefined;
|
|
162
|
+
}
|
|
163
|
+
/** Concatenate the text of all `type:'text'` blocks. Cursor uses the same
|
|
164
|
+
* `{type:'text', text}` shape for both user prompts and assistant replies;
|
|
165
|
+
* `tool_use` (and any other) blocks are ignored — the bridge only forwards
|
|
166
|
+
* text. Tolerates a bare-string content for defensiveness. */
|
|
167
|
+
function joinTextBlocks(content) {
|
|
168
|
+
if (typeof content === 'string')
|
|
169
|
+
return content;
|
|
170
|
+
if (!Array.isArray(content))
|
|
171
|
+
return '';
|
|
172
|
+
const parts = [];
|
|
173
|
+
for (const block of content) {
|
|
174
|
+
if (block && typeof block === 'object' && block.type === 'text') {
|
|
175
|
+
const text = block.text;
|
|
176
|
+
if (typeof text === 'string')
|
|
177
|
+
parts.push(text);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return parts.join('\n');
|
|
181
|
+
}
|
|
182
|
+
/** True when an assistant content array contains at least one tool_use block,
|
|
183
|
+
* i.e. this is a mid-turn step rather than the final reply. */
|
|
184
|
+
function hasToolUse(content) {
|
|
185
|
+
if (!Array.isArray(content))
|
|
186
|
+
return false;
|
|
187
|
+
return content.some(b => b && typeof b === 'object' && b.type === 'tool_use');
|
|
188
|
+
}
|
|
189
|
+
const CURSOR_REASONING_LEAK_HEADING_RE = new RegExp([
|
|
190
|
+
'\\n{2,}\\*\\*(?:',
|
|
191
|
+
[
|
|
192
|
+
'Considering',
|
|
193
|
+
'Thinking',
|
|
194
|
+
'Planning',
|
|
195
|
+
'Inspecting',
|
|
196
|
+
'Exploring',
|
|
197
|
+
'Reviewing',
|
|
198
|
+
'Troubleshooting',
|
|
199
|
+
'Diagnosing',
|
|
200
|
+
'Evaluating',
|
|
201
|
+
'Running',
|
|
202
|
+
'Checking',
|
|
203
|
+
'Reading',
|
|
204
|
+
'Understanding',
|
|
205
|
+
'Analyzing',
|
|
206
|
+
'Debugging',
|
|
207
|
+
'Responding',
|
|
208
|
+
].join('|'),
|
|
209
|
+
')\\b[^*\\n]{0,80}\\*\\*\\n{2,}',
|
|
210
|
+
].join(''));
|
|
211
|
+
function stripCursorReasoningLeak(text) {
|
|
212
|
+
// Cursor's mirror can append the model's internal planning/debug summary to
|
|
213
|
+
// the same text-only assistant line that otherwise represents the final user
|
|
214
|
+
// reply. The leak starts with a bold English activity heading after a blank
|
|
215
|
+
// paragraph, e.g. "**Considering user response**".
|
|
216
|
+
const marker = CURSOR_REASONING_LEAK_HEADING_RE.exec(text);
|
|
217
|
+
if (!marker || marker.index <= 0)
|
|
218
|
+
return text;
|
|
219
|
+
return text.slice(0, marker.index).trimEnd();
|
|
220
|
+
}
|
|
221
|
+
function eventFromLine(path, lineStart, obj, timestampMs) {
|
|
222
|
+
const role = obj?.role ?? obj?.message?.role;
|
|
223
|
+
const content = obj?.message?.content;
|
|
224
|
+
if (role === 'user') {
|
|
225
|
+
const t = joinTextBlocks(content);
|
|
226
|
+
if (!t)
|
|
227
|
+
return undefined;
|
|
228
|
+
return { uuid: `${path}:${lineStart}`, timestampMs, kind: 'user', text: t };
|
|
229
|
+
}
|
|
230
|
+
if (role === 'assistant') {
|
|
231
|
+
// A turn ends with a text-only assistant line; any line carrying a
|
|
232
|
+
// tool_use block is an intermediate step and must not be forwarded.
|
|
233
|
+
if (hasToolUse(content))
|
|
234
|
+
return undefined;
|
|
235
|
+
const t = stripCursorReasoningLeak(joinTextBlocks(content));
|
|
236
|
+
if (!t)
|
|
237
|
+
return undefined;
|
|
238
|
+
return { uuid: `${path}:${lineStart}`, timestampMs, kind: 'assistant_final', text: t };
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
/** Increment-read the transcript from `fromOffset`. Mirrors the byte-offset
|
|
243
|
+
* contract of codex-transcript.drainCodexRollout so the worker can reuse the
|
|
244
|
+
* same fs.watch / poll wakeup machinery and the shared CodexBridgeQueue. */
|
|
245
|
+
export function drainCursorTranscript(path, fromOffset) {
|
|
246
|
+
if (!existsSync(path))
|
|
247
|
+
return { events: [], newOffset: fromOffset, pendingTail: '' };
|
|
248
|
+
let size;
|
|
249
|
+
try {
|
|
250
|
+
size = statSync(path).size;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return { events: [], newOffset: fromOffset, pendingTail: '' };
|
|
254
|
+
}
|
|
255
|
+
let start = fromOffset;
|
|
256
|
+
// Cursor's mirror can briefly disappear / shrink while it rewrites. Do not
|
|
257
|
+
// reset to 0 here: replaying the full history pollutes attribution state and
|
|
258
|
+
// can wedge a live turn behind old events. Wait for the mirror to grow past
|
|
259
|
+
// the last consumed byte instead.
|
|
260
|
+
if (size < start)
|
|
261
|
+
return { events: [], newOffset: fromOffset, pendingTail: '' };
|
|
262
|
+
if (size === start)
|
|
263
|
+
return { events: [], newOffset: start, pendingTail: '' };
|
|
264
|
+
const len = size - start;
|
|
265
|
+
const buf = Buffer.alloc(len);
|
|
266
|
+
const fd = openSync(path, 'r');
|
|
267
|
+
try {
|
|
268
|
+
readSync(fd, buf, 0, len, start);
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
closeSync(fd);
|
|
272
|
+
}
|
|
273
|
+
const text = buf.toString('utf8');
|
|
274
|
+
const lastNl = text.lastIndexOf('\n');
|
|
275
|
+
const completeText = lastNl >= 0 ? text.slice(0, lastNl + 1) : '';
|
|
276
|
+
let pendingTail = lastNl >= 0 ? text.slice(lastNl + 1) : text;
|
|
277
|
+
let newOffset = start + Buffer.byteLength(completeText, 'utf8');
|
|
278
|
+
const events = [];
|
|
279
|
+
// Track byte offset within the file so synthetic uuids are stable across
|
|
280
|
+
// re-drains (the transcript is append-only).
|
|
281
|
+
let cursor = start;
|
|
282
|
+
for (const line of completeText.split('\n')) {
|
|
283
|
+
if (line.length === 0) {
|
|
284
|
+
cursor += 1; // the \n after an empty line
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const lineByteLen = Buffer.byteLength(line, 'utf8') + 1; // include \n
|
|
288
|
+
const lineStart = cursor;
|
|
289
|
+
cursor += lineByteLen;
|
|
290
|
+
let obj;
|
|
291
|
+
try {
|
|
292
|
+
obj = JSON.parse(line);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// No per-event timestamp in Cursor's JSONL — stamp with the drain
|
|
298
|
+
// wall-clock. Combined with byte-offset baselining at attach, this keeps
|
|
299
|
+
// the CodexBridgeQueue freshness gates happy without a real timestamp.
|
|
300
|
+
const timestampMs = Date.now();
|
|
301
|
+
const ev = eventFromLine(path, lineStart, obj, timestampMs);
|
|
302
|
+
if (ev)
|
|
303
|
+
events.push(ev);
|
|
304
|
+
}
|
|
305
|
+
// Cursor frequently leaves the final JSON object at EOF without a trailing
|
|
306
|
+
// newline until the next turn mutates the mirror. If the tail is already a
|
|
307
|
+
// complete JSON object, consume it now; otherwise keep it pending.
|
|
308
|
+
if (pendingTail.length > 0) {
|
|
309
|
+
try {
|
|
310
|
+
const lineStart = newOffset;
|
|
311
|
+
const obj = JSON.parse(pendingTail);
|
|
312
|
+
const ev = eventFromLine(path, lineStart, obj, Date.now());
|
|
313
|
+
if (ev)
|
|
314
|
+
events.push(ev);
|
|
315
|
+
newOffset = size;
|
|
316
|
+
pendingTail = '';
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// Still being written.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { events, newOffset, pendingTail };
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=cursor-transcript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-transcript.js","sourceRoot":"","sources":["../../src/services/cursor-transcript.ts"],"names":[],"mappings":"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"}
|
package/dist/worker.js
CHANGED
|
@@ -26,6 +26,7 @@ import { findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/t
|
|
|
26
26
|
import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
|
|
27
27
|
import { currentHermesStateOffset, drainHermesStateDb } from './services/hermes-transcript.js';
|
|
28
28
|
import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
|
|
29
|
+
import { drainCursorTranscript, findCursorTranscriptByChatId, findCursorTranscriptByPid } from './services/cursor-transcript.js';
|
|
29
30
|
import { baselineJsonlCursor } from './services/jsonl-cursor.js';
|
|
30
31
|
import { dirname } from 'node:path';
|
|
31
32
|
import { createServer as createHttpServer } from 'node:http';
|
|
@@ -1433,7 +1434,16 @@ function drainPathInto(path, fromOffset) {
|
|
|
1433
1434
|
function codexBridgeFallbackActive() {
|
|
1434
1435
|
// True for transcript-backed CLIs whose final output can be harvested
|
|
1435
1436
|
// when the model forgets to call `botmux send`.
|
|
1436
|
-
|
|
1437
|
+
const id = lastInitConfig?.cliId;
|
|
1438
|
+
if (id === 'codex' || id === 'traex' || id === 'coco' || id === 'hermes' || id === 'mtr')
|
|
1439
|
+
return true;
|
|
1440
|
+
// Cursor only harvests its transcript in adopt mode: a botmux-spawned
|
|
1441
|
+
// cursor session carries the botmux skill and replies via `botmux send`,
|
|
1442
|
+
// and we never resolve a transcript path for it — so leave that flow
|
|
1443
|
+
// (screen capture + botmux send) untouched and scope the bridge to adopt.
|
|
1444
|
+
if (id === 'cursor')
|
|
1445
|
+
return lastInitConfig?.adoptMode === true;
|
|
1446
|
+
return false;
|
|
1437
1447
|
}
|
|
1438
1448
|
// Both Codex and TRAE share the same rollout JSONL layout (response_item
|
|
1439
1449
|
// messages), so drainCodexRollout works for both.
|
|
@@ -1446,9 +1456,14 @@ function structuredBridgeIsHermes() {
|
|
|
1446
1456
|
function structuredBridgeIsMtr() {
|
|
1447
1457
|
return lastInitConfig?.cliId === 'mtr';
|
|
1448
1458
|
}
|
|
1459
|
+
function codexBridgeIsCursor() {
|
|
1460
|
+
return lastInitConfig?.cliId === 'cursor';
|
|
1461
|
+
}
|
|
1449
1462
|
function structuredBridgeIngestPath(path, offset) {
|
|
1450
1463
|
if (structuredBridgeIsCodex())
|
|
1451
1464
|
return drainCodexRollout(path, offset);
|
|
1465
|
+
if (codexBridgeIsCursor())
|
|
1466
|
+
return drainCursorTranscript(path, offset);
|
|
1452
1467
|
if (structuredBridgeIsHermes()) {
|
|
1453
1468
|
const result = drainHermesStateDb(offset);
|
|
1454
1469
|
return { events: result.events, newOffset: result.newOffset, pendingTail: '' };
|
|
@@ -1496,6 +1511,30 @@ function codexBridgeStartTimer() {
|
|
|
1496
1511
|
emitReadyCodexTurns();
|
|
1497
1512
|
return;
|
|
1498
1513
|
}
|
|
1514
|
+
if (codexBridgeIsCursor()) {
|
|
1515
|
+
// Late-attach: the transcript usually exists at adopt time (the
|
|
1516
|
+
// session is already running), so cursorBridgeAttach in setup wins.
|
|
1517
|
+
// This covers the rare race where pid→chatId resolved but the JSONL
|
|
1518
|
+
// hadn't been created yet. Resolution order: chatId (cliSessionId) →
|
|
1519
|
+
// path; then adopt pid → store.db fd → chatId → path.
|
|
1520
|
+
if (!codexBridgeRolloutPath) {
|
|
1521
|
+
let path = codexBridgePendingSessionId
|
|
1522
|
+
? findCursorTranscriptByChatId(codexBridgePendingSessionId)
|
|
1523
|
+
: undefined;
|
|
1524
|
+
if (!path && codexAdoptPendingPid) {
|
|
1525
|
+
path = findCursorTranscriptByPid(codexAdoptPendingPid)?.path;
|
|
1526
|
+
}
|
|
1527
|
+
if (path) {
|
|
1528
|
+
codexBridgePendingSessionId = undefined;
|
|
1529
|
+
codexAdoptPendingPid = undefined;
|
|
1530
|
+
cursorBridgeAttach(path, cursorLateAttachMode(path));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
codexBridgeIngest();
|
|
1534
|
+
if (isPromptReady)
|
|
1535
|
+
emitReadyCodexTurns();
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1499
1538
|
if (!codexBridgeRolloutPath) {
|
|
1500
1539
|
// Two discovery paths, in order: cliSessionId (known via writeInput
|
|
1501
1540
|
// result for non-adopt or daemon-side probe for adopt) → exact
|
|
@@ -1661,6 +1700,17 @@ function codexBridgeAttach(rolloutPath, mode) {
|
|
|
1661
1700
|
codexBridgeBaselineDone = true;
|
|
1662
1701
|
log(`Codex bridge split-live degraded to fresh (file missing): ${rolloutPath}`);
|
|
1663
1702
|
}
|
|
1703
|
+
else if (mode === 'baseline-existing-skip-tail' && existsSync(rolloutPath)) {
|
|
1704
|
+
let size = 0;
|
|
1705
|
+
try {
|
|
1706
|
+
size = statSync(rolloutPath).size;
|
|
1707
|
+
}
|
|
1708
|
+
catch { /* degrade below */ }
|
|
1709
|
+
codexBridgeOffset = size;
|
|
1710
|
+
codexBridgePendingTail = '';
|
|
1711
|
+
codexBridgeBaselineDone = true;
|
|
1712
|
+
log(`Codex bridge baselined: ${rolloutPath} (offset=${codexBridgeOffset}, skipTail=true)`);
|
|
1713
|
+
}
|
|
1664
1714
|
else if (existsSync(rolloutPath)) {
|
|
1665
1715
|
const cursor = baselineJsonlCursor(rolloutPath);
|
|
1666
1716
|
codexBridgeOffset = cursor.newOffset;
|
|
@@ -1696,6 +1746,39 @@ function codexBridgeAttach(rolloutPath, mode) {
|
|
|
1696
1746
|
// (codexBridgeIngest 在 offset 未推进时是 no-op)。
|
|
1697
1747
|
codexBridgeStartTimer();
|
|
1698
1748
|
}
|
|
1749
|
+
function cursorLateAttachMode(path) {
|
|
1750
|
+
const start = codexAdoptStartMs;
|
|
1751
|
+
if (start !== undefined) {
|
|
1752
|
+
try {
|
|
1753
|
+
const birthtimeMs = statSync(path).birthtimeMs;
|
|
1754
|
+
// Cursor often creates the agent-transcript file lazily on the first
|
|
1755
|
+
// post-adopt submit. In that case the first user line is live and must
|
|
1756
|
+
// be ingested from byte 0 rather than swallowed as history.
|
|
1757
|
+
if (Number.isFinite(birthtimeMs) && birthtimeMs >= start - 5_000)
|
|
1758
|
+
return 'fresh-empty';
|
|
1759
|
+
}
|
|
1760
|
+
catch { /* fall back to history-safe baseline */ }
|
|
1761
|
+
}
|
|
1762
|
+
return 'baseline-existing';
|
|
1763
|
+
}
|
|
1764
|
+
/** Attach the Cursor adopt bridge. Cursor's JSONL has no per-event
|
|
1765
|
+
* timestamp, so existing transcripts are baselined by byte offset. Cursor
|
|
1766
|
+
* restore intentionally skips any partial tail present at attach time: it is
|
|
1767
|
+
* old in-flight output and must not be attributed to the next Lark turn. If
|
|
1768
|
+
* the transcript is created after /adopt, attach fresh so the first
|
|
1769
|
+
* post-adopt Lark/user turn can still be attributed. */
|
|
1770
|
+
function cursorBridgeAttach(path, mode = 'baseline-existing') {
|
|
1771
|
+
if (mode === 'baseline-existing' && existsSync(path)) {
|
|
1772
|
+
try {
|
|
1773
|
+
const full = drainCursorTranscript(path, 0);
|
|
1774
|
+
maybeEmitCodexAdoptPreamble(full.events);
|
|
1775
|
+
}
|
|
1776
|
+
catch (err) {
|
|
1777
|
+
log(`Cursor bridge preamble drain failed: ${err.message}`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
codexBridgeAttach(path, mode === 'baseline-existing' ? 'baseline-existing-skip-tail' : mode);
|
|
1781
|
+
}
|
|
1699
1782
|
/** Called from flushPending after writeInput first returns a cliSessionId.
|
|
1700
1783
|
* Tries to locate the rollout file immediately; if it's not on disk yet,
|
|
1701
1784
|
* remembers the sid so the 1s poller can keep retrying. */
|
|
@@ -1714,6 +1797,20 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
1714
1797
|
}
|
|
1715
1798
|
return;
|
|
1716
1799
|
}
|
|
1800
|
+
if (codexBridgeIsCursor()) {
|
|
1801
|
+
// Cursor's cliSessionId is the chatId — the same UUID naming the
|
|
1802
|
+
// agent-transcript JSONL, so it resolves the path directly.
|
|
1803
|
+
const cursorPath = findCursorTranscriptByChatId(cliSessionId);
|
|
1804
|
+
if (cursorPath) {
|
|
1805
|
+
codexBridgePendingSessionId = undefined;
|
|
1806
|
+
cursorBridgeAttach(cursorPath, cursorLateAttachMode(cursorPath));
|
|
1807
|
+
}
|
|
1808
|
+
else {
|
|
1809
|
+
codexBridgePendingSessionId = cliSessionId;
|
|
1810
|
+
codexBridgeStartTimer();
|
|
1811
|
+
}
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1717
1814
|
const path = lastInitConfig?.cliId === 'traex'
|
|
1718
1815
|
? findTraexRolloutBySessionId(cliSessionId)
|
|
1719
1816
|
: findCodexRolloutBySessionId(cliSessionId);
|
|
@@ -2986,6 +3083,37 @@ function setupAdoptTranscriptBridges(cfg) {
|
|
|
2986
3083
|
codexBridgeStartTimer();
|
|
2987
3084
|
}
|
|
2988
3085
|
}
|
|
3086
|
+
else if (cfg.cliId === 'cursor') {
|
|
3087
|
+
const adoptStartMs = Date.now();
|
|
3088
|
+
codexAdoptStartMs = adoptStartMs;
|
|
3089
|
+
// Cursor JSONL lacks per-event timestamps, but adopt still needs parity
|
|
3090
|
+
// with other transcript bridges: direct terminal input should be surfaced
|
|
3091
|
+
// as a local-turn card in Lark. Baseline/offset handling above keeps
|
|
3092
|
+
// pre-adopt history out of the queue; worst-case mirror replay is a
|
|
3093
|
+
// duplicate local-turn message rather than lost local input.
|
|
3094
|
+
codexBridgeQueue.setLocalTurns(true, adoptStartMs);
|
|
3095
|
+
// Resolve the transcript: cliSessionId (= Cursor chatId) when discovery
|
|
3096
|
+
// captured it, else the adopt pid via its open store.db fd. Cursor lacks
|
|
3097
|
+
// per-event timestamps, so cursorBridgeAttach baselines by byte offset
|
|
3098
|
+
// rather than the timestamp-cutoff split-live the other CLIs use.
|
|
3099
|
+
let path;
|
|
3100
|
+
if (cfg.cliSessionId)
|
|
3101
|
+
path = findCursorTranscriptByChatId(cfg.cliSessionId);
|
|
3102
|
+
if (!path && cfg.adoptCliPid) {
|
|
3103
|
+
const probed = findCursorTranscriptByPid(cfg.adoptCliPid);
|
|
3104
|
+
if (probed)
|
|
3105
|
+
path = probed.path;
|
|
3106
|
+
}
|
|
3107
|
+
if (path) {
|
|
3108
|
+
cursorBridgeAttach(path);
|
|
3109
|
+
}
|
|
3110
|
+
else {
|
|
3111
|
+
if (cfg.cliSessionId)
|
|
3112
|
+
codexBridgePendingSessionId = cfg.cliSessionId;
|
|
3113
|
+
codexAdoptPendingPid = cfg.adoptCliPid;
|
|
3114
|
+
codexBridgeStartTimer();
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
2989
3117
|
}
|
|
2990
3118
|
function adoptIdleAdapter(cfg) {
|
|
2991
3119
|
return cfg.bridgeJsonlPath
|
|
@@ -4120,11 +4248,24 @@ process.on('message', async (raw) => {
|
|
|
4120
4248
|
// in-flight events from a local-typed prior turn close before
|
|
4121
4249
|
// this Lark turn's fingerprint window opens. Mark works even
|
|
4122
4250
|
// pre-attach (queue is path-agnostic).
|
|
4123
|
-
|
|
4124
|
-
|
|
4251
|
+
if (codexBridgeIsCursor()) {
|
|
4252
|
+
// Cursor may append the current Lark/user line to its transcript
|
|
4253
|
+
// before this IPC message is handled. Mark first so that preexisting
|
|
4254
|
+
// current-line can still fingerprint-match instead of being marked
|
|
4255
|
+
// seen as an unmatched event.
|
|
4256
|
+
codexBridgeMarkPendingTurn(content);
|
|
4257
|
+
try {
|
|
4258
|
+
codexBridgeIngest();
|
|
4259
|
+
}
|
|
4260
|
+
catch { /* best effort */ }
|
|
4261
|
+
}
|
|
4262
|
+
else {
|
|
4263
|
+
try {
|
|
4264
|
+
codexBridgeIngest();
|
|
4265
|
+
}
|
|
4266
|
+
catch { /* best effort */ }
|
|
4267
|
+
codexBridgeMarkPendingTurn(content);
|
|
4125
4268
|
}
|
|
4126
|
-
catch { /* best effort */ }
|
|
4127
|
-
codexBridgeMarkPendingTurn(content);
|
|
4128
4269
|
}
|
|
4129
4270
|
// Adopt mode write:
|
|
4130
4271
|
// - codex routes through cliAdapter.writeInput so the adapter's
|