cursorconnect 0.1.2 → 0.1.4

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 (36) hide show
  1. package/README.md +5 -4
  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/bridge-dir.js +5 -0
  25. package/dist/cli-version.js +13 -0
  26. package/dist/diagnose.js +224 -0
  27. package/dist/index.js +56 -55
  28. package/dist/launch.js +45 -13
  29. package/dist/pairing-code.js +18 -0
  30. package/dist/pairing-identity.js +3 -6
  31. package/dist/print-pairing.js +9 -7
  32. package/dist/relay-config.js +49 -0
  33. package/dist/semver.js +21 -0
  34. package/dist/version-check.js +31 -0
  35. package/package.json +6 -2
  36. package/version-policy.json +8 -0
@@ -4,10 +4,14 @@ import { randomBytes, timingSafeEqual } from 'crypto';
4
4
  import { basename } from 'path';
5
5
  import { readAllowedMediaFile, resolveMediaPathParam } from './media-path.js';
6
6
  import { Server as SocketServer } from 'socket.io';
7
+ import { ChatDisplayStore } from './chat-display-store.js';
8
+ import { filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
9
+ import { isChatSyncedWithCursor } from './chat-sync.js';
7
10
  import { mergeSidebarWithJsonl } from './sidebar-merge.js';
8
11
  import { transcribeAudioBuffer } from './openai-transcribe.js';
9
12
  import { extensionForMime, saveUploadedImage } from './image-upload-store.js';
10
13
  import { RelayUpstream } from './relay-upstream.js';
14
+ import { bridgePipelineLog, bridgePipelineReportLines, bridgePipelineSnapshot, } from './history-pipeline-log.js';
11
15
  export class Relay {
12
16
  stateManager;
13
17
  commandExecutor;
@@ -16,12 +20,17 @@ export class Relay {
16
20
  messageDebugStore;
17
21
  domExtractor;
18
22
  lastJsonlIndex = { repos: [], updatedAt: 0 };
23
+ indexEmitTimer = null;
24
+ lastIndexBroadcastAt = 0;
25
+ lastSidebarIndexKey = '';
26
+ lastJsonlIndexKey = '';
19
27
  config;
20
28
  app = express();
21
29
  httpServer = createServer(this.app);
22
30
  io;
23
31
  tokens = new Set();
24
32
  upstream = null;
33
+ chatDisplay = new ChatDisplayStore();
25
34
  constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
26
35
  this.stateManager = stateManager;
27
36
  this.commandExecutor = commandExecutor;
@@ -32,6 +41,7 @@ export class Relay {
32
41
  this.config = config;
33
42
  this.io = new SocketServer(this.httpServer, {
34
43
  cors: { origin: true, credentials: true },
44
+ maxHttpBufferSize: 20e6,
35
45
  });
36
46
  this.setupHttp();
37
47
  this.setupSocket();
@@ -82,6 +92,91 @@ export class Relay {
82
92
  lastError: state.lastError,
83
93
  });
84
94
  });
95
+ this.app.get('/api/agents/index', async (req, res) => {
96
+ if (!this.checkMediaAuth(req)) {
97
+ res.status(401).json({ error: 'Unauthorized' });
98
+ return;
99
+ }
100
+ try {
101
+ const stale = Date.now() - (this.lastJsonlIndex.updatedAt ?? 0) > 60_000;
102
+ const index = this.lastJsonlIndex.repos.length && !stale
103
+ ? this.lastJsonlIndex
104
+ : await this.jsonlIndex.rebuild({ broadcast: false });
105
+ this.lastJsonlIndex = index;
106
+ const merged = mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle);
107
+ const bytes = JSON.stringify(merged).length;
108
+ bridgePipelineLog({
109
+ dir: 'out',
110
+ event: 'http:agents:index',
111
+ bytes,
112
+ detail: `repos=${merged.repos?.length ?? 0}`,
113
+ });
114
+ res.json(merged);
115
+ }
116
+ catch (err) {
117
+ res.status(500).json({ error: err.message });
118
+ }
119
+ });
120
+ this.app.get('/api/agents/history', async (req, res) => {
121
+ if (!this.checkMediaAuth(req)) {
122
+ res.status(401).json({ error: 'Unauthorized' });
123
+ return;
124
+ }
125
+ const agentId = String(req.query.agentId ?? '').trim();
126
+ const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
127
+ const limitRaw = Number(req.query.limit ?? 0);
128
+ const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined;
129
+ const requestId = typeof req.query.requestId === 'string' ? req.query.requestId.trim() : undefined;
130
+ if (!agentId) {
131
+ res.status(400).json({ error: 'agentId required' });
132
+ return;
133
+ }
134
+ bridgePipelineLog({
135
+ dir: 'in',
136
+ event: 'http:agents:history',
137
+ requestId,
138
+ agentId,
139
+ detail: `limit=${limit ?? 'all'} title=${title?.slice(0, 32) ?? '-'}`,
140
+ });
141
+ try {
142
+ const history = await this.jsonlIndex.loadHistory(agentId, {
143
+ title,
144
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
145
+ limit,
146
+ });
147
+ const messages = this.chatDisplay.applyHistory(agentId, history.messages);
148
+ const body = { ...history, messages, requestId, updatedAt: Date.now() };
149
+ const bytes = JSON.stringify(body).length;
150
+ bridgePipelineLog({
151
+ dir: 'out',
152
+ event: 'http:agents:history',
153
+ requestId,
154
+ agentId,
155
+ bytes,
156
+ msgs: history.messages?.length ?? 0,
157
+ detail: `total=${history.totalMessages ?? '?'}`,
158
+ });
159
+ res.json(body);
160
+ }
161
+ catch (err) {
162
+ bridgePipelineLog({
163
+ dir: 'internal',
164
+ event: 'http:agents:history:ERR',
165
+ requestId,
166
+ agentId,
167
+ detail: err.message,
168
+ });
169
+ res.status(500).json({ error: err.message });
170
+ }
171
+ });
172
+ this.app.get('/api/debug/history-pipeline', (_req, res) => {
173
+ res.json({
174
+ ok: true,
175
+ at: Date.now(),
176
+ lines: bridgePipelineReportLines(),
177
+ entries: bridgePipelineSnapshot(),
178
+ });
179
+ });
85
180
  this.app.get('/media/file', (req, res) => {
86
181
  if (!this.checkMediaAuth(req)) {
87
182
  res.status(401).json({ error: 'Unauthorized' });
@@ -212,37 +307,108 @@ export class Relay {
212
307
  this.io.on('connection', (socket) => this.onConnect(socket));
213
308
  }
214
309
  broadcast(event, payload) {
310
+ if (event === 'agents:history' || event === 'agent:history') {
311
+ const h = payload;
312
+ const bytes = JSON.stringify(payload ?? {}).length;
313
+ bridgePipelineLog({
314
+ dir: 'out',
315
+ event: `broadcast:${event}`,
316
+ requestId: h?.requestId,
317
+ agentId: h?.agentId,
318
+ bytes,
319
+ msgs: h?.messages?.length ?? 0,
320
+ detail: `upstream=${this.upstream?.connected ?? false}`,
321
+ });
322
+ }
215
323
  this.io.emit(event, payload);
216
324
  this.upstream?.emit(event, payload);
217
325
  }
326
+ prepareStateMessages(raw, patch) {
327
+ const state = { ...this.stateManager.getState(), ...patch };
328
+ for (const [agentId, meta] of this.jsonlIndex.getSubscribedAgents()) {
329
+ if (isChatSyncedWithCursor(agentId, meta.title, state)) {
330
+ return this.chatDisplay.mergeLiveForAgent(agentId, raw);
331
+ }
332
+ }
333
+ return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
334
+ }
335
+ withDisplayState(payload) {
336
+ if (!payload.messages?.length)
337
+ return payload;
338
+ return {
339
+ ...payload,
340
+ messages: this.prepareStateMessages(payload.messages, payload),
341
+ };
342
+ }
218
343
  wireEvents() {
219
344
  this.stateManager.on('state:full', (state) => {
220
- this.broadcast('state:full', state);
345
+ this.broadcast('state:full', this.withDisplayState(state));
221
346
  this.emitAgentsIndex();
222
347
  });
223
348
  this.stateManager.on('state:patch', (patch) => {
224
- this.broadcast('state:patch', patch);
225
- if (patch.sidebarRepos || patch.composerIdByTitle)
226
- this.emitAgentsIndex();
349
+ this.broadcast('state:patch', this.withDisplayState(patch));
350
+ if (patch.sidebarRepos || patch.composerIdByTitle) {
351
+ const key = JSON.stringify([patch.sidebarRepos, patch.composerIdByTitle]);
352
+ if (key !== this.lastSidebarIndexKey) {
353
+ this.lastSidebarIndexKey = key;
354
+ this.emitAgentsIndex();
355
+ }
356
+ }
227
357
  });
228
358
  this.jsonlIndex.on('agents:index', (index) => {
359
+ const key = JSON.stringify(index.repos?.map((r) => [r.id, r.agents.length]));
360
+ if (key === this.lastJsonlIndexKey)
361
+ return;
362
+ this.lastJsonlIndexKey = key;
229
363
  this.lastJsonlIndex = index;
230
364
  this.emitAgentsIndex();
231
365
  });
232
- this.jsonlIndex.on('agent:history', (history) => this.broadcast('agent:history', history));
366
+ this.jsonlIndex.on('agent:history', (history) => {
367
+ const messages = this.chatDisplay.applyHistory(history.agentId, history.messages, {
368
+ mergeWithCache: true,
369
+ });
370
+ this.broadcast('agent:history', { ...history, messages });
371
+ });
233
372
  }
234
- emitAgentsIndex() {
235
- const sidebar = this.stateManager.getState().sidebarRepos;
236
- const merged = mergeSidebarWithJsonl(sidebar, this.lastJsonlIndex, this.stateManager.getState().composerIdByTitle);
237
- this.broadcast('agents:index', merged);
373
+ emitAgentsIndex(force = false) {
374
+ const send = () => {
375
+ const merged = mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, this.lastJsonlIndex, this.stateManager.getState().composerIdByTitle);
376
+ this.broadcast('agents:index', merged);
377
+ this.lastIndexBroadcastAt = Date.now();
378
+ };
379
+ if (force) {
380
+ if (this.indexEmitTimer)
381
+ clearTimeout(this.indexEmitTimer);
382
+ this.indexEmitTimer = null;
383
+ send();
384
+ return;
385
+ }
386
+ const elapsed = Date.now() - this.lastIndexBroadcastAt;
387
+ if (elapsed >= 60_000) {
388
+ send();
389
+ return;
390
+ }
391
+ if (this.indexEmitTimer)
392
+ return;
393
+ this.indexEmitTimer = setTimeout(() => {
394
+ this.indexEmitTimer = null;
395
+ send();
396
+ }, 60_000 - elapsed);
238
397
  }
239
- pushFullStateToRemote() {
240
- this.broadcast('state:full', this.stateManager.getState());
241
- void this.jsonlIndex.rebuild().then((index) => {
398
+ refreshAgentsIndex(force = false) {
399
+ void this.jsonlIndex.rebuild({ broadcast: false }).then((index) => {
242
400
  this.lastJsonlIndex = index;
243
- this.broadcast('agents:index', mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle));
401
+ this.emitAgentsIndex(force);
244
402
  });
245
403
  }
404
+ pushFullStateToRemote() {
405
+ this.broadcast('state:full', this.withDisplayState(this.stateManager.getState()));
406
+ if (this.lastJsonlIndex.updatedAt > 0) {
407
+ this.emitAgentsIndex(true);
408
+ return;
409
+ }
410
+ this.refreshAgentsIndex(true);
411
+ }
246
412
  async trySwitchWindowForAgent(agentId) {
247
413
  const projectDir = this.jsonlIndex.findProjectDirForAgent(agentId);
248
414
  if (!projectDir)
@@ -273,11 +439,19 @@ export class Relay {
273
439
  this.dispatchClientEvent(event, args, reply);
274
440
  }
275
441
  onConnect(socket) {
276
- socket.emit('state:full', this.stateManager.getState());
277
- void this.jsonlIndex.rebuild().then((index) => {
278
- this.lastJsonlIndex = index;
442
+ socket.emit('state:full', this.withDisplayState(this.stateManager.getState()));
443
+ const sendIndex = (index) => {
279
444
  socket.emit('agents:index', mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle));
280
- });
445
+ };
446
+ if (this.lastJsonlIndex.updatedAt > 0) {
447
+ sendIndex(this.lastJsonlIndex);
448
+ }
449
+ else {
450
+ void this.jsonlIndex.rebuild({ broadcast: false }).then((index) => {
451
+ this.lastJsonlIndex = index;
452
+ sendIndex(index);
453
+ });
454
+ }
281
455
  const reply = (ev, payload) => {
282
456
  socket.emit(ev, payload);
283
457
  };
@@ -297,7 +471,7 @@ export class Relay {
297
471
  await this.runAgentsFocus(data, reply);
298
472
  });
299
473
  socket.on('agents:refresh', () => {
300
- void this.jsonlIndex.rebuild();
474
+ this.refreshAgentsIndex(true);
301
475
  });
302
476
  socket.on('disconnect', () => {
303
477
  /* subscriptions stay global — one phone session typical */
@@ -325,7 +499,7 @@ export class Relay {
325
499
  return;
326
500
  }
327
501
  if (event === 'agents:refresh') {
328
- void this.jsonlIndex.rebuild();
502
+ this.refreshAgentsIndex(true);
329
503
  }
330
504
  }
331
505
  async runCommand(payload, reply) {
@@ -353,12 +527,65 @@ export class Relay {
353
527
  this.domExtractor.pollNow();
354
528
  }
355
529
  }
356
- async runAgentsHistory({ agentId, title }, reply) {
357
- const history = await this.jsonlIndex.loadHistory(agentId, {
358
- title,
359
- composerIdByTitle: this.stateManager.getState().composerIdByTitle,
530
+ async runAgentsHistory({ agentId, title, requestId, limit, }, reply) {
531
+ const t0 = Date.now();
532
+ const viaUpstream = this.upstream?.connected ?? false;
533
+ const socketLimit = limit && limit > 0 ? limit : 15;
534
+ console.log(`[relay] agents:history req agentId=${agentId} title=${title?.slice(0, 48) ?? '-'} limit=${socketLimit} rid=${requestId ?? '-'} upstream=${viaUpstream}`);
535
+ this.jsonlIndex.historyReplyInFlight.add(agentId);
536
+ bridgePipelineLog({
537
+ dir: 'in',
538
+ event: 'agents:history:req',
539
+ requestId,
540
+ agentId,
541
+ detail: `limit=${socketLimit} title=${title?.slice(0, 32) ?? '-'} upstream=${viaUpstream}`,
360
542
  });
361
- reply('agents:history', history);
543
+ try {
544
+ const history = await this.jsonlIndex.loadHistory(agentId, {
545
+ title,
546
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
547
+ limit: socketLimit,
548
+ });
549
+ const messages = this.chatDisplay.applyHistory(agentId, history.messages);
550
+ const ms = Date.now() - t0;
551
+ const bytes = JSON.stringify(messages).length;
552
+ console.log(`[relay] agents:history ok agentId=${history.agentId} msgs=${history.messages?.length ?? 0}/${history.totalMessages ?? '?'} bytes=${bytes} ms=${ms} rid=${requestId ?? '-'}`);
553
+ bridgePipelineLog({
554
+ dir: 'out',
555
+ event: 'agents:history:reply',
556
+ requestId,
557
+ agentId: history.agentId,
558
+ bytes,
559
+ msgs: history.messages?.length ?? 0,
560
+ detail: `total=${history.totalMessages ?? '?'} ms=${ms}`,
561
+ });
562
+ reply('agents:history', {
563
+ ...history,
564
+ messages,
565
+ requestId,
566
+ updatedAt: Date.now(),
567
+ });
568
+ }
569
+ catch (err) {
570
+ console.error(`[relay] agents:history fail agentId=${agentId} ms=${Date.now() - t0} rid=${requestId ?? '-'}:`, err.message);
571
+ bridgePipelineLog({
572
+ dir: 'internal',
573
+ event: 'agents:history:fail',
574
+ requestId,
575
+ agentId,
576
+ detail: err.message,
577
+ });
578
+ reply('agents:history', {
579
+ agentId,
580
+ messages: [],
581
+ totalMessages: 0,
582
+ requestId,
583
+ updatedAt: Date.now(),
584
+ });
585
+ }
586
+ finally {
587
+ this.jsonlIndex.historyReplyInFlight.delete(agentId);
588
+ }
362
589
  }
363
590
  async runAgentsSubscribe({ agentId, title, focus, }) {
364
591
  if (!agentId)
@@ -379,8 +606,10 @@ export class Relay {
379
606
  }
380
607
  }
381
608
  runAgentsUnsubscribe({ agentId }) {
382
- if (agentId)
609
+ if (agentId) {
383
610
  this.jsonlIndex.unsubscribe(agentId);
611
+ this.chatDisplay.clearAgent(agentId);
612
+ }
384
613
  }
385
614
  async runAgentsFocus({ agentId, title }, reply) {
386
615
  if (!agentId)
@@ -3,7 +3,7 @@ export { normalizeAgentTitle } from './agent-title-match.js';
3
3
  /** Prefer Cursor DOM sidebar order/names; attach JSONL ids when titles match. */
4
4
  export function mergeSidebarWithJsonl(sidebarRepos, jsonl, composerIdByTitle) {
5
5
  if (!sidebarRepos?.length)
6
- return jsonl;
6
+ return { ...jsonl, listSource: 'jsonl' };
7
7
  const byTitle = new Map();
8
8
  for (const repo of jsonl.repos) {
9
9
  for (const agent of repo.agents) {
@@ -43,5 +43,5 @@ export function mergeSidebarWithJsonl(sidebarRepos, jsonl, composerIdByTitle) {
43
43
  };
44
44
  }),
45
45
  }));
46
- return { repos, updatedAt: Date.now() };
46
+ return { repos, updatedAt: Date.now(), listSource: 'sidebar' };
47
47
  }
@@ -229,15 +229,23 @@ export interface RepoGroup {
229
229
  export interface AgentsIndex {
230
230
  repos: RepoGroup[];
231
231
  updatedAt: number;
232
+ /** `sidebar` = порядок/названия из CDP; `jsonl` = архив на диске (не как в Cursor). */
233
+ listSource?: 'sidebar' | 'jsonl';
232
234
  }
233
235
  export interface HistoryMessage {
234
236
  role: 'user' | 'assistant' | 'system';
235
237
  text: string;
238
+ html?: string;
239
+ /** Absolute paths from JSONL `<image_files>` (served via GET /media/file). */
240
+ images?: string[];
236
241
  ts?: number;
237
242
  }
238
243
  export interface AgentHistory {
239
244
  agentId: string;
240
- messages: HistoryMessage[];
245
+ /** Display-ready chat rows (normalized, deduped, filtered on bridge). */
246
+ messages: ChatMessage[];
241
247
  updatedAt?: number;
248
+ requestId?: string;
249
+ totalMessages?: number;
242
250
  }
243
251
  export type ExtractedPageState = Omit<CursorState, 'connected' | 'windows' | 'activeWindowId' | 'windowSnapshots' | 'updatedAt'>;
@@ -0,0 +1,3 @@
1
+ # CursorConnect — встроенный relay (переопределение: ~/.cursorconnect/config.env)
2
+ RELAY_URL=https://cc.fanpay.online
3
+ RELAY_TOKEN=cursorconnect-test-relay-2026
@@ -65,6 +65,11 @@ export function ensureUserConfig() {
65
65
  copyFileSync(devEnv, USER_CONFIG_ENV);
66
66
  return;
67
67
  }
68
+ const bundled = join(resolvePackageRoot(), 'config.env.defaults');
69
+ if (existsSync(bundled)) {
70
+ copyFileSync(bundled, USER_CONFIG_ENV);
71
+ return;
72
+ }
68
73
  const template = join(resolvePackageRoot(), 'bridge-runtime', '.env.example');
69
74
  if (existsSync(template)) {
70
75
  copyFileSync(template, USER_CONFIG_ENV);
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
5
+ export const CLI_VERSION = (() => {
6
+ try {
7
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
8
+ return pkg.version?.trim() || '0.0.0';
9
+ }
10
+ catch {
11
+ return '0.0.0';
12
+ }
13
+ })();
@@ -0,0 +1,224 @@
1
+ import { io } from 'socket.io-client';
2
+ import { resolveRelayConfig, DEFAULT_RELAY_URL } from './relay-config.js';
3
+ import { loadPairingIdentity } from './pairing-identity.js';
4
+ function loadRelayUrl() {
5
+ return resolveRelayConfig().relayUrl || DEFAULT_RELAY_URL;
6
+ }
7
+ function socketProbe(relayUrl, auth, transports, expectIndex) {
8
+ return new Promise((resolve) => {
9
+ const socket = io(relayUrl, {
10
+ transports,
11
+ auth,
12
+ reconnection: false,
13
+ timeout: 12_000,
14
+ });
15
+ let indexRepos = null;
16
+ const transportLabel = transports.join('+');
17
+ const done = (ok, detail) => {
18
+ socket.disconnect();
19
+ resolve({ ok, detail });
20
+ };
21
+ socket.on('connect', () => {
22
+ if (!expectIndex) {
23
+ done(true, `connect id=${socket.id} transport=${socket.io.engine.transport.name}`);
24
+ return;
25
+ }
26
+ socket.emit('agents:refresh');
27
+ });
28
+ socket.on('agents:index', (idx) => {
29
+ indexRepos = idx?.repos?.length ?? 0;
30
+ done(true, `connect transport=${socket.io.engine.transport.name} agents:index repos=${indexRepos}`);
31
+ });
32
+ socket.on('connect_error', (err) => {
33
+ done(false, `${transportLabel}: ${err.message}`);
34
+ });
35
+ setTimeout(() => {
36
+ if (socket.connected && !expectIndex)
37
+ return;
38
+ if (socket.connected && expectIndex) {
39
+ done(false, `${transportLabel}: connect ok, agents:index timeout (wrong room?)`);
40
+ return;
41
+ }
42
+ done(false, `${transportLabel}: timeout`);
43
+ }, expectIndex ? 10_000 : 8_000);
44
+ });
45
+ }
46
+ function historyProbe(relayUrl, auth, agentId, title) {
47
+ return new Promise((resolve) => {
48
+ const socket = io(relayUrl, {
49
+ transports: ['websocket', 'polling'],
50
+ auth,
51
+ reconnection: false,
52
+ timeout: 15_000,
53
+ });
54
+ const t0 = Date.now();
55
+ const done = (ok, detail) => {
56
+ socket.disconnect();
57
+ resolve({ ok, detail });
58
+ };
59
+ socket.on('connect', () => {
60
+ socket.emit('agents:history', { agentId, title });
61
+ });
62
+ socket.on('agents:history', (h) => {
63
+ done(true, `${h.messages?.length ?? 0} msgs for ${h.agentId ?? agentId} in ${Date.now() - t0}ms`);
64
+ });
65
+ socket.on('connect_error', (err) => done(false, err.message));
66
+ setTimeout(() => done(false, 'agents:history timeout (20s)'), 20_000);
67
+ });
68
+ }
69
+ export async function runDiagnose() {
70
+ const identity = loadPairingIdentity();
71
+ const relayUrl = (loadRelayUrl() || 'https://cc.fanpay.online').replace(/\/$/, '');
72
+ const checks = [];
73
+ if (!identity) {
74
+ return {
75
+ at: new Date().toISOString(),
76
+ relayUrl,
77
+ roomId: '',
78
+ checks: [
79
+ {
80
+ id: 'identity',
81
+ ok: false,
82
+ detail: 'Нет ~/.cursorconnect/identity.json',
83
+ hint: 'cursorconnect start',
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ const roomId = identity.roomId;
89
+ const token = identity.clientToken;
90
+ // 1 Relay HTTP
91
+ try {
92
+ const res = await fetch(`${relayUrl}/health`);
93
+ const j = (await res.json());
94
+ checks.push({
95
+ id: 'relay.health',
96
+ ok: Boolean(j.ok),
97
+ detail: `ok=${j.ok} connector=${j.connector} peers=${JSON.stringify(j.peers)}`,
98
+ hint: j.connector ? undefined : 'cursorconnect start на Mac',
99
+ });
100
+ }
101
+ catch (e) {
102
+ checks.push({
103
+ id: 'relay.health',
104
+ ok: false,
105
+ detail: e.message,
106
+ hint: 'Проверьте RELAY_URL и интернет',
107
+ });
108
+ }
109
+ // 2 Room diagnostics API
110
+ try {
111
+ const q = new URLSearchParams({ roomId, token });
112
+ const res = await fetch(`${relayUrl}/api/diagnostics?${q}`);
113
+ const j = (await res.json());
114
+ const ok = Boolean(j.ok);
115
+ checks.push({
116
+ id: 'relay.room',
117
+ ok: ok && Boolean(j.connectorInRoom) && Boolean(j.tokenRegistered),
118
+ detail: JSON.stringify(j),
119
+ hint: !j.connectorInRoom
120
+ ? 'Bridge не в этой комнате на relay'
121
+ : !j.tokenRegistered
122
+ ? 'Токен не зарегистрирован — перезапустите cursorconnect start'
123
+ : !j.tokenAccepted
124
+ ? 'Токен не в whitelist комнаты'
125
+ : undefined,
126
+ });
127
+ }
128
+ catch (e) {
129
+ checks.push({
130
+ id: 'relay.room',
131
+ ok: false,
132
+ detail: e.message,
133
+ hint: 'Обновите relay: npm run deploy:relay',
134
+ });
135
+ }
136
+ // 3 Local bridge
137
+ try {
138
+ const res = await fetch('http://127.0.0.1:3847/health');
139
+ const j = (await res.json());
140
+ checks.push({
141
+ id: 'bridge.local',
142
+ ok: Boolean(j.ok),
143
+ detail: `ok=${j.ok} cdp=${j.cdp}`,
144
+ hint: j.cdp ? undefined : 'Cursor с --remote-debugging-port=9222',
145
+ });
146
+ }
147
+ catch (e) {
148
+ checks.push({
149
+ id: 'bridge.local',
150
+ ok: false,
151
+ detail: e.message,
152
+ hint: 'cursorconnect start',
153
+ });
154
+ }
155
+ // 4 Pairing code (не вызываем POST /api/pair — код одноразовый)
156
+ const code = identity.pairingCode?.replace(/[^A-Z0-9]/gi, '').toUpperCase() ?? '';
157
+ const codeValid = code.length === 6 && identity.pairingCodeExpiresAt > Date.now();
158
+ if (codeValid) {
159
+ const secLeft = Math.max(0, Math.floor((identity.pairingCodeExpiresAt - Date.now()) / 1000));
160
+ checks.push({
161
+ id: 'pair.code',
162
+ ok: true,
163
+ detail: `active ${code} (~${secLeft}s)`,
164
+ hint: 'Введите в app до истечения; повторный start — новый код',
165
+ });
166
+ }
167
+ else {
168
+ checks.push({
169
+ id: 'pair.code',
170
+ ok: false,
171
+ detail: 'Код истёк или отсутствует',
172
+ hint: 'cursorconnect start',
173
+ });
174
+ }
175
+ const auth = { token, roomId };
176
+ const ws = await socketProbe(relayUrl, auth, ['websocket'], true);
177
+ checks.push({
178
+ id: 'socket.ws+room',
179
+ ok: ws.ok,
180
+ detail: ws.detail,
181
+ hint: ws.ok ? undefined : 'Телефон: нужен polling+websocket в app',
182
+ });
183
+ const poll = await socketProbe(relayUrl, auth, ['polling'], true);
184
+ checks.push({
185
+ id: 'socket.poll+room',
186
+ ok: poll.ok,
187
+ detail: poll.detail,
188
+ hint: poll.ok ? undefined : 'Relay/nginx блокирует long-polling?',
189
+ });
190
+ const wrongRoom = await socketProbe(relayUrl, { token, roomId: 'default' }, ['websocket', 'polling'], true);
191
+ checks.push({
192
+ id: 'socket.wrong-room',
193
+ ok: !wrongRoom.ok,
194
+ detail: wrongRoom.detail,
195
+ hint: wrongRoom.ok
196
+ ? 'App подключается к room=default вместо вашего roomId'
197
+ : undefined,
198
+ });
199
+ const history = await historyProbe(relayUrl, auth, 'sidebar-0', 'Git repository creation and project deployment');
200
+ checks.push({
201
+ id: 'socket.history',
202
+ ok: history.ok,
203
+ detail: history.detail,
204
+ hint: history.ok ? undefined : 'connectorInRoom false или bridge не отвечает — cursorconnect start',
205
+ });
206
+ return { at: new Date().toISOString(), relayUrl, roomId, checks };
207
+ }
208
+ export function formatDiagnoseReport(report) {
209
+ const lines = [
210
+ `CursorConnect diagnose @ ${report.at}`,
211
+ `relay=${report.relayUrl}`,
212
+ `room=${report.roomId}`,
213
+ '',
214
+ ];
215
+ for (const c of report.checks) {
216
+ lines.push(`${c.ok ? 'PASS' : 'FAIL'} ${c.id}`);
217
+ lines.push(` ${c.detail}`);
218
+ if (c.hint)
219
+ lines.push(` → ${c.hint}`);
220
+ }
221
+ const failed = report.checks.filter((c) => !c.ok).length;
222
+ lines.push('', failed === 0 ? 'Итог: все проверки пройдены' : `Итог: ${failed} проблем(а)`);
223
+ return lines.join('\n');
224
+ }