cursorconnect 0.1.2 → 0.1.5

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 (39) hide show
  1. package/README.md +6 -5
  2. package/bridge-runtime/dist/agent-title-match.js +16 -0
  3. package/bridge-runtime/dist/chat-display-store.d.ts +13 -0
  4. package/bridge-runtime/dist/chat-display-store.js +29 -0
  5. package/bridge-runtime/dist/chat-display.d.ts +11 -0
  6. package/bridge-runtime/dist/chat-display.js +290 -0
  7. package/bridge-runtime/dist/chat-sync.d.ts +6 -0
  8. package/bridge-runtime/dist/chat-sync.js +88 -0
  9. package/bridge-runtime/dist/extract-page.js +99 -3
  10. package/bridge-runtime/dist/history-pipeline-log.d.ts +16 -0
  11. package/bridge-runtime/dist/history-pipeline-log.js +29 -0
  12. package/bridge-runtime/dist/jsonl-index.d.ts +15 -3
  13. package/bridge-runtime/dist/jsonl-index.js +48 -12
  14. package/bridge-runtime/dist/message-filter.d.ts +10 -0
  15. package/bridge-runtime/dist/message-filter.js +65 -5
  16. package/bridge-runtime/dist/pairing-code.d.ts +3 -0
  17. package/bridge-runtime/dist/pairing-code.js +17 -0
  18. package/bridge-runtime/dist/pairing-identity.js +4 -7
  19. package/bridge-runtime/dist/relay.d.ts +8 -0
  20. package/bridge-runtime/dist/relay.js +254 -25
  21. package/bridge-runtime/dist/sidebar-merge.js +2 -2
  22. package/bridge-runtime/dist/types.d.ts +9 -1
  23. package/config.env.defaults +3 -0
  24. package/dist/big-code.js +36 -5
  25. package/dist/bridge-dir.js +6 -1
  26. package/dist/cli-version.js +13 -0
  27. package/dist/diagnose.js +224 -0
  28. package/dist/index.js +56 -92
  29. package/dist/launch.js +52 -14
  30. package/dist/pairing-code.js +18 -0
  31. package/dist/pairing-identity.js +6 -8
  32. package/dist/pairing-ttl.js +3 -0
  33. package/dist/print-pairing.js +18 -25
  34. package/dist/relay-config.js +49 -0
  35. package/dist/repo-root.js +2 -2
  36. package/dist/semver.js +21 -0
  37. package/dist/version-check.js +31 -0
  38. package/package.json +7 -3
  39. package/version-policy.json +8 -0
@@ -0,0 +1,29 @@
1
+ const RING = [];
2
+ const MAX = 150;
3
+ export function bridgePipelineLog(entry) {
4
+ const row = {
5
+ layer: 'bridge',
6
+ at: entry.at ?? Date.now(),
7
+ dir: entry.dir,
8
+ event: entry.event,
9
+ requestId: entry.requestId,
10
+ agentId: entry.agentId,
11
+ bytes: entry.bytes,
12
+ msgs: entry.msgs,
13
+ detail: entry.detail,
14
+ };
15
+ RING.push(row);
16
+ while (RING.length > MAX)
17
+ RING.shift();
18
+ const rid = row.requestId ? ` rid=${row.requestId}` : '';
19
+ console.log(`[history-pipe] bridge ${row.dir} ${row.event}${rid} agent=${row.agentId ?? '-'} bytes=${row.bytes ?? '-'} msgs=${row.msgs ?? '-'} ${row.detail ?? ''}`.trim());
20
+ }
21
+ export function bridgePipelineSnapshot() {
22
+ return [...RING];
23
+ }
24
+ export function bridgePipelineReportLines() {
25
+ return RING.map((r) => {
26
+ const t = new Date(r.at).toISOString().slice(11, 23);
27
+ return `${t} bridge ${r.dir} ${r.event} rid=${r.requestId ?? '-'} agent=${r.agentId ?? '-'} bytes=${r.bytes ?? '-'} msgs=${r.msgs ?? '-'} ${r.detail ?? ''}`.trim();
28
+ });
29
+ }
@@ -1,24 +1,36 @@
1
1
  import { EventEmitter } from 'events';
2
- import type { AgentHistory, AgentsIndex } from './types.js';
2
+ import type { AgentsIndex, HistoryMessage } from './types.js';
3
3
  export declare class JsonlIndex extends EventEmitter {
4
4
  private projectsDir;
5
5
  private debounceTimer;
6
6
  private subscribed;
7
7
  private mtimeByAgent;
8
8
  private pollTimer;
9
+ /** Skip poll `agent:history` while serving explicit `agents:history` (avoids relay race). */
10
+ readonly historyReplyInFlight: Set<string>;
9
11
  constructor(projectsDir: string);
10
12
  start(): void;
11
13
  stop(): void;
12
14
  subscribe(agentId: string, title?: string): void;
13
15
  unsubscribe(agentId: string): void;
16
+ getSubscribedAgents(): ReadonlyMap<string, {
17
+ title?: string;
18
+ }>;
14
19
  private pollSubscribed;
15
20
  private emitHistoryForAgent;
16
21
  private emitHistoryForFile;
17
22
  private scheduleRebuild;
18
- rebuild(): Promise<AgentsIndex>;
23
+ rebuild(opts?: {
24
+ broadcast?: boolean;
25
+ }): Promise<AgentsIndex>;
19
26
  findProjectDirForAgent(agentId: string): string | null;
20
27
  loadHistory(agentId: string, opts?: {
21
28
  title?: string;
22
29
  composerIdByTitle?: Record<string, string>;
23
- }): Promise<AgentHistory>;
30
+ limit?: number;
31
+ }): Promise<{
32
+ agentId: string;
33
+ messages: HistoryMessage[];
34
+ totalMessages: number;
35
+ }>;
24
36
  }
@@ -4,13 +4,16 @@ import { basename, join } from 'path';
4
4
  import { createInterface } from 'readline';
5
5
  import chokidar from 'chokidar';
6
6
  import { resolveJsonlFilePath } from './agent-title-match.js';
7
- import { cleanUserText, filterAssistantJsonlParts, isNoiseChatText, } from './message-filter.js';
7
+ import { cleanUserText, parseUserImagePaths, filterAssistantJsonlParts, isNoiseChatText, stripJsonlRedactionArtifacts, } from './message-filter.js';
8
+ import { bridgePipelineLog } from './history-pipeline-log.js';
8
9
  export class JsonlIndex extends EventEmitter {
9
10
  projectsDir;
10
11
  debounceTimer = null;
11
12
  subscribed = new Map();
12
13
  mtimeByAgent = new Map();
13
14
  pollTimer = null;
15
+ /** Skip poll `agent:history` while serving explicit `agents:history` (avoids relay race). */
16
+ historyReplyInFlight = new Set();
14
17
  constructor(projectsDir) {
15
18
  super();
16
19
  this.projectsDir = projectsDir;
@@ -44,6 +47,9 @@ export class JsonlIndex extends EventEmitter {
44
47
  this.subscribed.delete(agentId);
45
48
  this.mtimeByAgent.delete(agentId);
46
49
  }
50
+ getSubscribedAgents() {
51
+ return this.subscribed;
52
+ }
47
53
  async pollSubscribed() {
48
54
  for (const [agentId, meta] of this.subscribed) {
49
55
  const filePath = resolveJsonlFilePath(this.projectsDir, agentId, {
@@ -57,7 +63,7 @@ export class JsonlIndex extends EventEmitter {
57
63
  if (prev === mtime)
58
64
  continue;
59
65
  this.mtimeByAgent.set(agentId, mtime);
60
- await this.emitHistoryForFile(filePath);
66
+ await this.emitHistoryForFile(filePath, agentId);
61
67
  }
62
68
  catch {
63
69
  /* skip */
@@ -83,6 +89,15 @@ export class JsonlIndex extends EventEmitter {
83
89
  if (!fileAgentId || fileAgentId.includes('/'))
84
90
  return;
85
91
  const agentId = replyAgentId ?? fileAgentId;
92
+ if (this.historyReplyInFlight.has(agentId) || this.historyReplyInFlight.has(fileAgentId)) {
93
+ bridgePipelineLog({
94
+ dir: 'internal',
95
+ event: 'poll:agent:history:SKIP',
96
+ agentId,
97
+ detail: `inFlight file=${fileAgentId.slice(0, 8)}`,
98
+ });
99
+ return;
100
+ }
86
101
  try {
87
102
  const messages = await parseJsonlFile(filePath);
88
103
  this.mtimeByAgent.set(agentId, statSync(filePath).mtimeMs);
@@ -97,11 +112,13 @@ export class JsonlIndex extends EventEmitter {
97
112
  clearTimeout(this.debounceTimer);
98
113
  this.debounceTimer = setTimeout(() => void this.rebuild(), 500);
99
114
  }
100
- async rebuild() {
115
+ async rebuild(opts) {
116
+ const broadcast = opts?.broadcast !== false;
101
117
  const repos = new Map();
102
118
  if (!existsSync(this.projectsDir)) {
103
119
  const empty = { repos: [], updatedAt: Date.now() };
104
- this.emit('agents:index', empty);
120
+ if (broadcast)
121
+ this.emit('agents:index', empty);
105
122
  return empty;
106
123
  }
107
124
  for (const entry of readdirSync(this.projectsDir, { withFileTypes: true })) {
@@ -126,8 +143,10 @@ export class JsonlIndex extends EventEmitter {
126
143
  const index = {
127
144
  repos: [...repos.values()].sort((a, b) => a.name.localeCompare(b.name)),
128
145
  updatedAt: Date.now(),
146
+ listSource: 'jsonl',
129
147
  };
130
- this.emit('agents:index', index);
148
+ if (broadcast)
149
+ this.emit('agents:index', index);
131
150
  return index;
132
151
  }
133
152
  findProjectDirForAgent(agentId) {
@@ -142,10 +161,15 @@ export class JsonlIndex extends EventEmitter {
142
161
  async loadHistory(agentId, opts) {
143
162
  const filePath = resolveJsonlFilePath(this.projectsDir, agentId, opts);
144
163
  if (!filePath) {
145
- return { agentId, messages: [] };
164
+ return { agentId, messages: [], totalMessages: 0 };
146
165
  }
147
166
  const messages = await parseJsonlFile(filePath);
148
- return { agentId, messages };
167
+ const totalMessages = messages.length;
168
+ const limit = opts?.limit;
169
+ const trimmed = limit && limit > 0 && messages.length > limit
170
+ ? messages.slice(-limit)
171
+ : messages;
172
+ return { agentId, messages: trimmed, totalMessages };
149
173
  }
150
174
  }
151
175
  function shouldIgnoreWatchPath(p) {
@@ -207,7 +231,7 @@ function peekFirstUserMessage(filePath) {
207
231
  if (row.role !== 'user')
208
232
  continue;
209
233
  const text = row.message?.content?.find((c) => c.type === 'text')?.text ?? '';
210
- const cleaned = text.replace(/<user_query>\s*/gi, '').replace(/<\/user_query>/gi, '').trim();
234
+ const cleaned = cleanUserText(text);
211
235
  if (cleaned)
212
236
  return cleaned.slice(0, 200);
213
237
  }
@@ -272,8 +296,10 @@ async function parseJsonlFile(filePath) {
272
296
  try {
273
297
  const row = JSON.parse(line);
274
298
  const parts = [];
299
+ const rawParts = [];
275
300
  for (const c of row.message?.content ?? []) {
276
301
  if (c.type === 'text' && c.text) {
302
+ rawParts.push(c.text);
277
303
  const t = cleanUserText(c.text);
278
304
  if (t)
279
305
  parts.push(t);
@@ -281,10 +307,20 @@ async function parseJsonlFile(filePath) {
281
307
  // tool_use, tool_result — ignore
282
308
  }
283
309
  if (row.role === 'user') {
284
- const text = parts.join('\n').trim();
285
- if (text && !isNoiseChatText(text)) {
286
- messages.push({ role: 'user', text, ts: lineNo });
287
- }
310
+ const raw = rawParts.join('\n');
311
+ const images = parseUserImagePaths(raw);
312
+ const text = stripJsonlRedactionArtifacts(cleanUserText(parts.join('\n').trim()));
313
+ const hasImages = images.length > 0;
314
+ if ((!text || isNoiseChatText(text)) && !hasImages)
315
+ continue;
316
+ if (text && isNoiseChatText(text) && !hasImages)
317
+ continue;
318
+ messages.push({
319
+ role: 'user',
320
+ text: text || (hasImages ? 'Изображение' : ''),
321
+ images: hasImages ? images : undefined,
322
+ ts: lineNo,
323
+ });
288
324
  continue;
289
325
  }
290
326
  if (row.role === 'assistant') {
@@ -9,11 +9,21 @@ export declare const WORKING_STATUS_LINE: RegExp;
9
9
  /** Short DOM status line only — not a sentence inside a chat message. */
10
10
  export declare function matchBackgroundWorkStatusText(text: string): string | undefined;
11
11
  export declare function isBackgroundWorkStatusText(text: string): boolean;
12
+ /** Chat line that is only passive-work status (not prose mentioning it). */
13
+ export declare function isPassiveStatusChatLine(text: string): boolean;
12
14
  /** DOM/system lines that mean the agent is still busy */
13
15
  export declare function textImpliesAgentWorking(text: string): boolean;
16
+ /** Cursor JSONL redacts tool/thinking tails as `[REDACTED]` — not shown in live DOM. */
17
+ export declare function stripJsonlRedactionArtifacts(text: string): string;
14
18
  export declare function isNoiseChatText(text: string): boolean;
19
+ /** Chain-of-thought / tool trace rows — not user-facing answers (DOM + JSONL). */
20
+ export declare function isAssistantReflectionText(text: string): boolean;
15
21
  /** Short agent status worth showing (not tool log). */
16
22
  export declare function isMeaningfulAssistantText(text: string): boolean;
23
+ /** Paths from Cursor `<image_files>` blocks (JSONL + DOM text). */
24
+ export declare function parseUserImagePaths(text: string): string[];
25
+ /** Cursor injects image metadata into user bubbles — hide it in the app. */
26
+ export declare function stripUserImageMetadata(text: string): string;
17
27
  /** Keep in sync with `cleanUserText` in `extract-page.ts` (CDP runs in-page). */
18
28
  export declare function cleanUserText(text: string): string;
19
29
  export declare function filterAssistantJsonlParts(parts: string[]): string[];
@@ -22,12 +22,28 @@ export function matchBackgroundWorkStatusText(text) {
22
22
  export function isBackgroundWorkStatusText(text) {
23
23
  return matchBackgroundWorkStatusText(text) !== undefined;
24
24
  }
25
+ /** Chat line that is only passive-work status (not prose mentioning it). */
26
+ export function isPassiveStatusChatLine(text) {
27
+ const t = text.trim().replace(/\s+/g, ' ');
28
+ if (!t || t.length > 80 || t.includes('?'))
29
+ return false;
30
+ if (BACKGROUND_WORK_STATUS.test(t) || WORKING_STATUS_LINE.test(t))
31
+ return true;
32
+ return false;
33
+ }
25
34
  /** DOM/system lines that mean the agent is still busy */
26
35
  export function textImpliesAgentWorking(text) {
27
36
  return isBackgroundWorkStatusText(text);
28
37
  }
38
+ /** Cursor JSONL redacts tool/thinking tails as `[REDACTED]` — not shown in live DOM. */
39
+ export function stripJsonlRedactionArtifacts(text) {
40
+ return text
41
+ .replace(/(?:\n|\r\n)*\[REDACTED\]\s*$/g, '')
42
+ .replace(/^\[REDACTED\]\s*$/g, '')
43
+ .trim();
44
+ }
29
45
  export function isNoiseChatText(text) {
30
- const t = text.trim().replace(/\s+/g, ' ');
46
+ const t = stripJsonlRedactionArtifacts(text).trim().replace(/\s+/g, ' ');
31
47
  if (!t || t.length < 2)
32
48
  return true;
33
49
  if (NOISE_LINE.test(t))
@@ -56,10 +72,29 @@ export function isNoiseChatText(text) {
56
72
  return true;
57
73
  return false;
58
74
  }
75
+ /** Chain-of-thought / tool trace rows — not user-facing answers (DOM + JSONL). */
76
+ export function isAssistantReflectionText(text) {
77
+ const t = stripJsonlRedactionArtifacts(text).trim().replace(/\s+/g, ' ');
78
+ if (!t || isNoiseChatText(t))
79
+ return true;
80
+ if (/^Thought\s*for\s*\d/i.test(t) && t.length < 160)
81
+ return true;
82
+ if (/^(Exploring|Grepped|Searched|Listed|Read |Ran |Edited |Loading|Planning|Using image)/i.test(t)) {
83
+ return true;
84
+ }
85
+ const toolHits = t.match(/\b(Explored|Grepped|Searched|Listed|Read )\b/gi);
86
+ if (toolHits && toolHits.length >= 2)
87
+ return true;
88
+ if (t.length > 160 &&
89
+ /\b(state\.messages|flat-index|mapKeyedChildren|humanEl|extract-page|userMessagesEquivalent)\b/i.test(t)) {
90
+ return true;
91
+ }
92
+ return false;
93
+ }
59
94
  /** Short agent status worth showing (not tool log). */
60
95
  export function isMeaningfulAssistantText(text) {
61
96
  const t = text.trim();
62
- if (!t || isNoiseChatText(t))
97
+ if (!t || isNoiseChatText(t) || isAssistantReflectionText(t))
63
98
  return false;
64
99
  // Markdown prose / explanation
65
100
  if (t.length >= 20 && /\s/.test(t))
@@ -69,15 +104,40 @@ export function isMeaningfulAssistantText(text) {
69
104
  return true;
70
105
  return t.length >= 50;
71
106
  }
107
+ /** Paths from Cursor `<image_files>` blocks (JSONL + DOM text). */
108
+ export function parseUserImagePaths(text) {
109
+ const paths = [];
110
+ const scope = text.match(/<image_files>[\s\S]*?<\/image_files>/i)?.[0] ?? text;
111
+ const re = /\d+\.\s+(\/?[^\s\n]+?\.(?:png|jpe?g|gif|webp))/gi;
112
+ let m;
113
+ while ((m = re.exec(scope))) {
114
+ const p = m[1]?.trim();
115
+ if (p && !paths.includes(p))
116
+ paths.push(p);
117
+ }
118
+ return paths;
119
+ }
120
+ /** Cursor injects image metadata into user bubbles — hide it in the app. */
121
+ export function stripUserImageMetadata(text) {
122
+ return text
123
+ .replace(/^\[Image\]\s*$/gim, '')
124
+ .replace(/<image_files>[\s\S]*?<\/image_files>/gi, '')
125
+ .replace(/(?:^|\n)\s*The following images? (?:were |has been )?provid(?:ed|ied)[^\n]*(?:\n\s*\d+\.\s*[^\n]+)*/gi, '\n')
126
+ .replace(/(?:^|\n)\s*These images can be copied for use in other locations\.?\s*/gi, '\n')
127
+ .replace(/\n{3,}/g, '\n\n')
128
+ .trim();
129
+ }
72
130
  /** Keep in sync with `cleanUserText` in `extract-page.ts` (CDP runs in-page). */
73
131
  export function cleanUserText(text) {
74
- return text
132
+ return stripUserImageMetadata(text
75
133
  .replace(/<user_query>\s*/gi, '')
76
134
  .replace(/<\/user_query>/gi, '')
77
135
  .replace(/([.!?…])(\d+\.\s*)/g, '$1\n$2')
78
136
  .replace(/([^\n\d\s])(\d+\.\s+)/g, '$1\n$2')
79
- .trim();
137
+ .trim());
80
138
  }
81
139
  export function filterAssistantJsonlParts(parts) {
82
- return parts.filter((p) => isMeaningfulAssistantText(p));
140
+ return parts
141
+ .map((p) => stripJsonlRedactionArtifacts(p))
142
+ .filter((p) => isMeaningfulAssistantText(p));
83
143
  }
@@ -0,0 +1,3 @@
1
+ export declare const PAIRING_CODE_LENGTH = 6;
2
+ export declare function generatePairingCode(): string;
3
+ export declare function normalizePairingCode(raw: string): string | null;
@@ -0,0 +1,17 @@
1
+ import { randomBytes } from 'crypto';
2
+ export const PAIRING_CODE_LENGTH = 6;
3
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
4
+ export function generatePairingCode() {
5
+ const bytes = randomBytes(PAIRING_CODE_LENGTH);
6
+ let out = '';
7
+ for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
8
+ out += ALPHABET[bytes[i] % ALPHABET.length];
9
+ }
10
+ return out;
11
+ }
12
+ export function normalizePairingCode(raw) {
13
+ const code = raw.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
14
+ if (code.length !== PAIRING_CODE_LENGTH)
15
+ return null;
16
+ return code;
17
+ }
@@ -1,13 +1,10 @@
1
1
  import { randomBytes, randomUUID } from 'crypto';
2
+ import { generatePairingCode } from './pairing-code.js';
2
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
4
  import { homedir, hostname } from 'os';
4
5
  import { join } from 'path';
5
6
  const DIR = join(homedir(), '.cursorconnect');
6
7
  const FILE = join(DIR, 'identity.json');
7
- function formatPairingCode() {
8
- const n = randomBytes(3).readUIntBE(0, 3) % 1_000_000;
9
- return String(n).padStart(6, '0');
10
- }
11
8
  export function pairingIdentityPath() {
12
9
  return FILE;
13
10
  }
@@ -35,7 +32,7 @@ export function ensurePairingIdentity(machineLabel) {
35
32
  roomId: randomUUID(),
36
33
  clientToken: randomBytes(32).toString('hex'),
37
34
  machineLabel: machineLabel?.trim() || defaultMachineLabel(),
38
- pairingCode: formatPairingCode(),
35
+ pairingCode: generatePairingCode(),
39
36
  pairingCodeExpiresAt: Date.now() + 10 * 60_000,
40
37
  createdAt: now,
41
38
  updatedAt: now,
@@ -47,7 +44,7 @@ export function refreshPairingCode(identity, machineLabel) {
47
44
  const next = {
48
45
  ...identity,
49
46
  machineLabel: machineLabel?.trim() || identity.machineLabel,
50
- pairingCode: formatPairingCode(),
47
+ pairingCode: generatePairingCode(),
51
48
  pairingCodeExpiresAt: Date.now() + 10 * 60_000,
52
49
  updatedAt: new Date().toISOString(),
53
50
  };
@@ -58,7 +55,7 @@ export function rotateClientToken(identity) {
58
55
  const next = {
59
56
  ...identity,
60
57
  clientToken: randomBytes(32).toString('hex'),
61
- pairingCode: formatPairingCode(),
58
+ pairingCode: generatePairingCode(),
62
59
  pairingCodeExpiresAt: Date.now() + 10 * 60_000,
63
60
  updatedAt: new Date().toISOString(),
64
61
  };
@@ -13,12 +13,17 @@ export declare class Relay {
13
13
  private messageDebugStore;
14
14
  private domExtractor;
15
15
  private lastJsonlIndex;
16
+ private indexEmitTimer;
17
+ private lastIndexBroadcastAt;
18
+ private lastSidebarIndexKey;
19
+ private lastJsonlIndexKey;
16
20
  private config;
17
21
  private app;
18
22
  private httpServer;
19
23
  private io;
20
24
  private tokens;
21
25
  private upstream;
26
+ private readonly chatDisplay;
22
27
  constructor(config: ServerConfig, stateManager: StateManager, commandExecutor: CommandExecutor, cdpBridge: CDPBridge, jsonlIndex: JsonlIndex, messageDebugStore: MessageDebugStore, domExtractor: DOMExtractor);
23
28
  listen(): Promise<void>;
24
29
  private get authEnabled();
@@ -26,8 +31,11 @@ export declare class Relay {
26
31
  private setupHttp;
27
32
  private setupSocket;
28
33
  private broadcast;
34
+ private prepareStateMessages;
35
+ private withDisplayState;
29
36
  private wireEvents;
30
37
  private emitAgentsIndex;
38
+ private refreshAgentsIndex;
31
39
  private pushFullStateToRemote;
32
40
  private trySwitchWindowForAgent;
33
41
  private handleRemoteClientEvent;