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.
- package/README.md +6 -5
- package/bridge-runtime/dist/agent-title-match.js +16 -0
- package/bridge-runtime/dist/chat-display-store.d.ts +13 -0
- package/bridge-runtime/dist/chat-display-store.js +29 -0
- package/bridge-runtime/dist/chat-display.d.ts +11 -0
- package/bridge-runtime/dist/chat-display.js +290 -0
- package/bridge-runtime/dist/chat-sync.d.ts +6 -0
- package/bridge-runtime/dist/chat-sync.js +88 -0
- package/bridge-runtime/dist/extract-page.js +99 -3
- package/bridge-runtime/dist/history-pipeline-log.d.ts +16 -0
- package/bridge-runtime/dist/history-pipeline-log.js +29 -0
- package/bridge-runtime/dist/jsonl-index.d.ts +15 -3
- package/bridge-runtime/dist/jsonl-index.js +48 -12
- package/bridge-runtime/dist/message-filter.d.ts +10 -0
- package/bridge-runtime/dist/message-filter.js +65 -5
- package/bridge-runtime/dist/pairing-code.d.ts +3 -0
- package/bridge-runtime/dist/pairing-code.js +17 -0
- package/bridge-runtime/dist/pairing-identity.js +4 -7
- package/bridge-runtime/dist/relay.d.ts +8 -0
- package/bridge-runtime/dist/relay.js +254 -25
- package/bridge-runtime/dist/sidebar-merge.js +2 -2
- package/bridge-runtime/dist/types.d.ts +9 -1
- package/config.env.defaults +3 -0
- package/dist/big-code.js +36 -5
- package/dist/bridge-dir.js +6 -1
- package/dist/cli-version.js +13 -0
- package/dist/diagnose.js +224 -0
- package/dist/index.js +56 -92
- package/dist/launch.js +52 -14
- package/dist/pairing-code.js +18 -0
- package/dist/pairing-identity.js +6 -8
- package/dist/pairing-ttl.js +3 -0
- package/dist/print-pairing.js +18 -25
- package/dist/relay-config.js +49 -0
- package/dist/repo-root.js +2 -2
- package/dist/semver.js +21 -0
- package/dist/version-check.js +31 -0
- package/package.json +7 -3
- package/version-policy.json +8 -0
package/dist/diagnose.js
ADDED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
60
|
+
console.error('[cursorconnect] Перезапуск Cursor Connect…');
|
|
71
61
|
stopBridge();
|
|
72
62
|
}
|
|
73
63
|
startBridge(env);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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('
|
|
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.
|
|
113
|
-
|
|
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
|
|
170
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
if (await waitCdp(cdpUrl, 50))
|
|
186
|
+
return true;
|
|
187
|
+
printManualCdpInstructions();
|
|
188
|
+
return false;
|
|
150
189
|
}
|
|
151
|
-
// Cursor запущен,
|
|
152
|
-
let shouldRestart =
|
|
190
|
+
// Cursor запущен, порт 9222 не отвечает — обычный запуск без --remote-debugging-port
|
|
191
|
+
let shouldRestart = opts.noRestartCursor !== true;
|
|
153
192
|
if (!shouldRestart) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
package/dist/pairing-identity.js
CHANGED
|
@@ -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:
|
|
39
|
-
pairingCodeExpiresAt: Date.now() +
|
|
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:
|
|
51
|
-
pairingCodeExpiresAt: Date.now() +
|
|
48
|
+
pairingCode: generatePairingCode(),
|
|
49
|
+
pairingCodeExpiresAt: Date.now() + PAIRING_CODE_TTL_MS,
|
|
52
50
|
updatedAt: new Date().toISOString(),
|
|
53
51
|
};
|
|
54
52
|
savePairingIdentity(next);
|