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,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
+ ? 'Cursor Connect не в этой комнате на 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: 'cursor-connect.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: 'cursor-connect.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 или Cursor Connect не отвечает — 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
+ }
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync } from 'fs';
3
2
  import { join, resolve } from 'path';
4
3
  import { ensurePairingIdentity, loadPairingIdentity, refreshPairingCode, } from './pairing-identity.js';
5
4
  import { ensureCursorCdp, isBridgeRunning, startBridge, stopBridge, waitBridgeHealth, waitRelayConnector, } from './launch.js';
@@ -7,40 +6,24 @@ import { printPairingToTerminal } from './print-pairing.js';
7
6
  import { BRIDGE_LOG_FILE } from './paths.js';
8
7
  import { ensureUserConfig, isValidBridgeDir, resolveBridgeDir, USER_CONFIG_ENV, } from './bridge-dir.js';
9
8
  import { configFilePath, saveInstallConfig } from './repo-root.js';
10
- function loadEnvFile(path) {
11
- if (!existsSync(path))
12
- return {};
13
- const out = {};
14
- for (const line of readFileSync(path, 'utf-8').split('\n')) {
15
- const t = line.trim();
16
- if (!t || t.startsWith('#'))
17
- continue;
18
- const i = t.indexOf('=');
19
- if (i < 1)
20
- continue;
21
- out[t.slice(0, i).trim()] = t.slice(i + 1).trim();
22
- }
23
- return out;
24
- }
9
+ import { formatDiagnoseReport, runDiagnose } from './diagnose.js';
10
+ import { loadEnvFile, resolveRelayConfig } from './relay-config.js';
11
+ import { ensureCliVersionForRelay } from './version-check.js';
12
+ import { CLI_VERSION } from './cli-version.js';
25
13
  function bridgeEnv(identity) {
26
14
  ensureUserConfig();
27
- const paths = [
28
- USER_CONFIG_ENV,
29
- join(resolveBridgeDir(), '.env'),
30
- ];
31
- let file = {};
32
- for (const p of paths) {
33
- if (existsSync(p)) {
34
- file = { ...file, ...loadEnvFile(p) };
35
- }
36
- }
37
- const relayUrl = file.RELAY_URL ?? process.env.RELAY_URL ?? '';
38
- const relayToken = file.RELAY_TOKEN ?? process.env.RELAY_TOKEN ?? '';
39
- if (!relayUrl || !relayToken) {
40
- console.error(`Нужны RELAY_URL и RELAY_TOKEN в ${USER_CONFIG_ENV}\n` +
41
- '(создаётся при установке; отредактируйте и снова: cursorconnect start)');
15
+ const bridgeDotEnv = join(resolveBridgeDir(), '.env');
16
+ const { relayUrl, relayToken } = resolveRelayConfig([bridgeDotEnv]);
17
+ if (!relayToken) {
18
+ console.error('[cursorconnect] Не найден RELAY_TOKEN (встроенный конфиг пакета повреждён).\n' +
19
+ ` Переустановите: npm install -g cursorconnect\n` +
20
+ ` Или укажите RELAY_TOKEN в ${USER_CONFIG_ENV} (свой relay).`);
42
21
  process.exit(1);
43
22
  }
23
+ const file = {
24
+ ...loadEnvFile(bridgeDotEnv),
25
+ ...loadEnvFile(USER_CONFIG_ENV),
26
+ };
44
27
  return {
45
28
  RELAY_URL: relayUrl,
46
29
  RELAY_TOKEN: relayToken,
@@ -48,45 +31,49 @@ function bridgeEnv(identity) {
48
31
  SERVER_HOST: '127.0.0.1',
49
32
  SERVER_PORT: '3847',
50
33
  CDP_URL: file.CDP_URL ?? 'http://127.0.0.1:9222',
34
+ CURSORCONNECT_CLI_VERSION: CLI_VERSION,
51
35
  };
52
36
  }
53
37
  function startFlags(argv) {
54
38
  return {
55
39
  restartCursor: argv.includes('--restart-cursor') || argv.includes('-r'),
40
+ noRestartCursor: argv.includes('--no-restart-cursor'),
56
41
  };
57
42
  }
58
43
  async function cmdStart(argv) {
59
- const { restartCursor } = startFlags(argv);
44
+ const { restartCursor, noRestartCursor } = startFlags(argv);
60
45
  const identity = ensurePairingIdentity();
61
46
  const refreshed = refreshPairingCode(identity);
62
47
  const env = bridgeEnv(refreshed);
63
48
  const relayUrl = env.RELAY_URL;
64
- console.log('CursorConnect');
65
- if (restartCursor) {
66
- console.log('Перезапуск Cursor с CDP…');
49
+ await ensureCliVersionForRelay(relayUrl);
50
+ // 1) Сначала CDP (порт 9222), иначе bridge стартует без сайдбара Cursor.
51
+ const cdpPortOk = await ensureCursorCdp({
52
+ cdpUrl: env.CDP_URL,
53
+ restartCursor,
54
+ noRestartCursor,
55
+ });
56
+ if (!cdpPortOk) {
57
+ console.warn('[cursorconnect] CDP порт недоступен — Cursor Connect запустится, список чатов будет из архива JSONL.');
67
58
  }
68
- console.log(`Компьютер: ${refreshed.machineLabel}`);
69
59
  if (isBridgeRunning()) {
70
- console.log('Перезапуск bridge (новый код pairing)…');
60
+ console.error('[cursorconnect] Перезапуск Cursor Connect…');
71
61
  stopBridge();
72
62
  }
73
63
  startBridge(env);
74
- console.log(`Bridge в фоне ${BRIDGE_LOG_FILE}`);
75
- const cdpOk = await ensureCursorCdp({
76
- cdpUrl: env.CDP_URL,
77
- restartCursor,
78
- });
79
- const health = await waitBridgeHealth(25_000);
80
- const connectorOk = await waitRelayConnector(relayUrl, 25_000);
81
- printPairingToTerminal(refreshed, {
82
- bridge: health.ok,
83
- cdp: cdpOk && health.cdp,
84
- connector: connectorOk,
85
- });
64
+ let health = await waitBridgeHealth(12_000, { requireCdp: false });
65
+ if (health.ok && cdpPortOk && !health.cdp) {
66
+ health = await waitBridgeHealth(15_000, { requireCdp: true });
67
+ }
68
+ await waitRelayConnector(relayUrl, 12_000);
69
+ if (cdpPortOk && health.ok && !health.cdp) {
70
+ console.error(`[cursorconnect] CDP не подключён (cdp:false) · лог: ${BRIDGE_LOG_FILE}`);
71
+ }
72
+ printPairingToTerminal(refreshed);
86
73
  }
87
74
  async function cmdStop() {
88
75
  stopBridge();
89
- console.log('Bridge остановлен');
76
+ console.log('Cursor Connect остановлен');
90
77
  }
91
78
  async function cmdInit(pathArg) {
92
79
  const target = pathArg?.trim() || process.cwd();
@@ -102,49 +89,21 @@ async function cmdInit(pathArg) {
102
89
  console.log(`repoRoot: ${abs}`);
103
90
  console.log('\nДальше: cursorconnect start');
104
91
  }
92
+ async function cmdDiagnose() {
93
+ const report = await runDiagnose();
94
+ console.log(formatDiagnoseReport(report));
95
+ const failed = report.checks.some((c) => !c.ok);
96
+ process.exit(failed ? 1 : 0);
97
+ }
105
98
  async function cmdStatus() {
106
99
  const identity = loadPairingIdentity();
107
100
  ensureUserConfig();
108
- const env = loadEnvFile(USER_CONFIG_ENV);
109
- const relayUrl = env.RELAY_URL ?? process.env.RELAY_URL ?? '';
110
- console.log('Bridge running:', isBridgeRunning());
111
101
  if (!identity) {
112
- console.log('Identity не найден — запустите: cursorconnect start');
113
- return;
114
- }
115
- let connector = false;
116
- let cdp = false;
117
- let bridgeOk = false;
118
- try {
119
- const h = await fetch('http://127.0.0.1:3847/health');
120
- const j = (await h.json());
121
- bridgeOk = Boolean(j.ok);
122
- cdp = Boolean(j.cdp);
123
- }
124
- catch {
125
- /* offline */
126
- }
127
- if (relayUrl) {
128
- try {
129
- const res = await fetch(`${relayUrl.replace(/\/$/, '')}/health`);
130
- const j = (await res.json());
131
- connector = Boolean(j.connector);
132
- }
133
- catch {
134
- /* ignore */
135
- }
136
- }
137
- if (relayUrl) {
138
- printPairingToTerminal(identity, {
139
- bridge: bridgeOk,
140
- cdp,
141
- connector,
142
- });
143
- }
144
- else {
145
- console.log('Room:', identity.roomId);
146
- console.log('Код:', identity.pairingCode);
102
+ console.error('Identity не найден — запустите: cursorconnect start');
103
+ process.exit(1);
147
104
  }
105
+ console.error(`[cursorconnect] Cursor Connect работает: ${isBridgeRunning()}`);
106
+ printPairingToTerminal(identity);
148
107
  }
149
108
  async function main() {
150
109
  const argv = process.argv.slice(2);
@@ -160,21 +119,26 @@ async function main() {
160
119
  case 'status':
161
120
  await cmdStatus();
162
121
  break;
122
+ case 'diagnose':
123
+ await cmdDiagnose();
124
+ break;
163
125
  case 'init':
164
126
  await cmdInit(rest[0]);
165
127
  break;
166
128
  default:
167
129
  console.log(`Usage: cursorconnect <command>
168
130
 
169
- start [--restart-cursor|-r] bridge в фоне + код для приложения
170
- stop остановить bridge
131
+ start CDP (авто-перезапуск Cursor) + Cursor Connect + код
132
+ start -r то же (явный перезапуск Cursor)
133
+ start --no-restart-cursor — не трогать Cursor (спросит y/n в TTY)
134
+ stop — остановить Cursor Connect
171
135
  status — код и статус
136
+ diagnose — проверка relay / Cursor Connect / socket (без гаданий)
172
137
  init [path] — только для разработки (клон репо)
173
138
 
174
139
  Установка (из любой папки):
175
140
  npm install -g cursorconnect
176
- # один раз: ~/.cursorconnect/config.env RELAY_URL и RELAY_TOKEN
177
- cursorconnect start`);
141
+ cursorconnect start # код pairing, relay уже в пакете`);
178
142
  process.exit(cmd === 'help' ? 0 : 1);
179
143
  }
180
144
  }
package/dist/launch.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execSync, spawn, spawnSync } from 'child_process';
2
2
  import { askYesNo } from './ask.js';
3
- import { existsSync, openSync, readFileSync, writeFileSync } from 'fs';
3
+ import { closeSync, existsSync, openSync, readFileSync, writeFileSync } from 'fs';
4
4
  import { BRIDGE_LOG_FILE, BRIDGE_PID_FILE } from './paths.js';
5
5
  import { resolveBridgeDir } from './bridge-dir.js';
6
6
  export function isBridgeRunning() {
@@ -34,8 +34,22 @@ export function stopBridge() {
34
34
  /* ignore */
35
35
  }
36
36
  }
37
+ function killProcessesOnBridgePort() {
38
+ try {
39
+ execSync('lsof -t -i:3847 2>/dev/null | xargs kill -TERM 2>/dev/null', {
40
+ stdio: 'ignore',
41
+ shell: '/bin/bash',
42
+ });
43
+ /* give old bridge time to exit */
44
+ execSync('sleep 0.5', { stdio: 'ignore', shell: '/bin/bash' });
45
+ }
46
+ catch {
47
+ /* port free */
48
+ }
49
+ }
37
50
  export function startBridge(env) {
38
51
  stopBridge();
52
+ killProcessesOnBridgePort();
39
53
  const bridgeDir = resolveBridgeDir();
40
54
  const logFd = openLogAppend();
41
55
  const child = spawn('node', ['dist/index.js'], {
@@ -45,25 +59,47 @@ export function startBridge(env) {
45
59
  stdio: ['ignore', logFd, logFd],
46
60
  });
47
61
  child.unref();
62
+ try {
63
+ closeSync(logFd);
64
+ }
65
+ catch {
66
+ /* ignore */
67
+ }
48
68
  writeFileSync(BRIDGE_PID_FILE, String(child.pid));
49
69
  return child;
50
70
  }
51
71
  function openLogAppend() {
52
72
  return openSync(BRIDGE_LOG_FILE, 'a');
53
73
  }
54
- export async function waitBridgeHealth(maxMs = 30_000) {
74
+ export async function waitBridgeHealth(maxMs = 30_000, opts) {
75
+ const requireCdp = opts?.requireCdp === true;
55
76
  const deadline = Date.now() + maxMs;
56
77
  while (Date.now() < deadline) {
57
78
  try {
58
79
  const res = await fetch('http://127.0.0.1:3847/health');
59
- if (res.ok)
60
- return (await res.json());
80
+ if (res.ok) {
81
+ const j = (await res.json());
82
+ const ok = Boolean(j.ok);
83
+ const cdp = Boolean(j.cdp);
84
+ if (ok && (!requireCdp || cdp))
85
+ return { ok, cdp };
86
+ }
61
87
  }
62
88
  catch {
63
89
  /* retry */
64
90
  }
65
91
  await sleep(500);
66
92
  }
93
+ try {
94
+ const res = await fetch('http://127.0.0.1:3847/health');
95
+ if (res.ok) {
96
+ const j = (await res.json());
97
+ return { ok: Boolean(j.ok), cdp: Boolean(j.cdp) };
98
+ }
99
+ }
100
+ catch {
101
+ /* ignore */
102
+ }
67
103
  return { ok: false, cdp: false };
68
104
  }
69
105
  export async function waitRelayConnector(relayUrl, maxMs = 45_000) {
@@ -146,29 +182,31 @@ export async function ensureCursorCdp(opts = {}) {
146
182
  if (!running) {
147
183
  console.log('[cursorconnect] Cursor не запущен — старт с --remote-debugging-port=9222…');
148
184
  launchCursorWithCdp();
149
- return waitCdp(cdpUrl, 50);
185
+ if (await waitCdp(cdpUrl, 50))
186
+ return true;
187
+ printManualCdpInstructions();
188
+ return false;
150
189
  }
151
- // Cursor запущен, CDP недоступен
152
- let shouldRestart = Boolean(opts.restartCursor);
190
+ // Cursor запущен, порт 9222 не отвечает — обычный запуск без --remote-debugging-port
191
+ let shouldRestart = opts.noRestartCursor !== true;
153
192
  if (!shouldRestart) {
154
- if (!process.stdin.isTTY) {
155
- console.warn('[cursorconnect] Cursor без CDP (нет интерактивного терминала).');
156
- printManualCdpInstructions();
157
- return false;
193
+ shouldRestart = Boolean(opts.restartCursor);
194
+ if (!shouldRestart && process.stdin.isTTY) {
195
+ shouldRestart = await askYesNo('Cursor без CDP. Перезапустить с --remote-debugging-port=9222? (y/n): ');
158
196
  }
159
- shouldRestart = await askYesNo('Cursor запущен без CDP. Перезапустить с --remote-debugging-port=9222? (y/n): ');
160
197
  }
161
198
  if (!shouldRestart) {
199
+ console.warn('[cursorconnect] Cursor без CDP — список чатов будет только из JSONL.');
162
200
  printManualCdpInstructions();
163
201
  return false;
164
202
  }
203
+ console.log('[cursorconnect] Cursor без CDP — перезапуск с --remote-debugging-port=9222…');
165
204
  const quitOk = await quitCursor();
166
205
  if (!quitOk) {
167
- console.warn('[cursorconnect] Не удалось закрыть Cursor.');
206
+ console.warn('[cursorconnect] Не удалось закрыть Cursor (Cmd+Q вручную).');
168
207
  printManualCdpInstructions();
169
208
  return false;
170
209
  }
171
- console.log('[cursorconnect] Запуск Cursor с --remote-debugging-port=9222…');
172
210
  launchCursorWithCdp();
173
211
  if (await waitCdp(cdpUrl, 50))
174
212
  return true;
@@ -0,0 +1,18 @@
1
+ import { randomBytes } from 'crypto';
2
+ export const PAIRING_CODE_LENGTH = 6;
3
+ /** Latin letters + digits (ASCII), uppercase in storage. */
4
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
5
+ export function generatePairingCode() {
6
+ const bytes = randomBytes(PAIRING_CODE_LENGTH);
7
+ let out = '';
8
+ for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
9
+ out += ALPHABET[bytes[i] % ALPHABET.length];
10
+ }
11
+ return out;
12
+ }
13
+ export function normalizePairingCode(raw) {
14
+ const code = raw.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
15
+ if (code.length !== PAIRING_CODE_LENGTH)
16
+ return null;
17
+ return code;
18
+ }
@@ -1,13 +1,11 @@
1
1
  import { randomBytes, randomUUID } from 'crypto';
2
+ import { generatePairingCode } from './pairing-code.js';
3
+ import { PAIRING_CODE_TTL_MS } from './pairing-ttl.js';
2
4
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
5
  import { homedir, hostname } from 'os';
4
6
  import { join } from 'path';
5
7
  const DIR = join(homedir(), '.cursorconnect');
6
8
  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
9
  export function pairingIdentityPath() {
12
10
  return FILE;
13
11
  }
@@ -35,8 +33,8 @@ export function ensurePairingIdentity(machineLabel) {
35
33
  roomId: randomUUID(),
36
34
  clientToken: randomBytes(32).toString('hex'),
37
35
  machineLabel: machineLabel?.trim() || defaultMachineLabel(),
38
- pairingCode: formatPairingCode(),
39
- pairingCodeExpiresAt: Date.now() + 10 * 60_000,
36
+ pairingCode: generatePairingCode(),
37
+ pairingCodeExpiresAt: Date.now() + PAIRING_CODE_TTL_MS,
40
38
  createdAt: now,
41
39
  updatedAt: now,
42
40
  };
@@ -47,8 +45,8 @@ export function refreshPairingCode(identity, machineLabel) {
47
45
  const next = {
48
46
  ...identity,
49
47
  machineLabel: machineLabel?.trim() || identity.machineLabel,
50
- pairingCode: formatPairingCode(),
51
- pairingCodeExpiresAt: Date.now() + 10 * 60_000,
48
+ pairingCode: generatePairingCode(),
49
+ pairingCodeExpiresAt: Date.now() + PAIRING_CODE_TTL_MS,
52
50
  updatedAt: new Date().toISOString(),
53
51
  };
54
52
  savePairingIdentity(next);
@@ -0,0 +1,3 @@
1
+ /** Срок жизни pairing-кода (Mac ↔ relay). */
2
+ export const PAIRING_CODE_TTL_MS = 10 * 60_000;
3
+ export const PAIRING_CODE_TTL_MINUTES = 10;