cursorconnect 0.1.6 → 0.1.7
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/bridge-runtime/.env.example +10 -2
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +42 -0
- package/bridge-runtime/dist/agent-completion-push.js +220 -0
- package/bridge-runtime/dist/agent-title-match.d.ts +8 -7
- package/bridge-runtime/dist/agent-title-match.js +11 -1
- package/bridge-runtime/dist/chat-display-store.d.ts +21 -9
- package/bridge-runtime/dist/chat-display-store.js +94 -23
- package/bridge-runtime/dist/chat-display.d.ts +2 -0
- package/bridge-runtime/dist/chat-display.js +197 -33
- package/bridge-runtime/dist/chat-history-mode.d.ts +5 -0
- package/bridge-runtime/dist/chat-history-mode.js +7 -0
- package/bridge-runtime/dist/command-executor.d.ts +2 -0
- package/bridge-runtime/dist/command-executor.js +44 -0
- package/bridge-runtime/dist/composer-title-index.d.ts +1 -0
- package/bridge-runtime/dist/composer-title-index.js +7 -7
- package/bridge-runtime/dist/debug-chats-page.d.ts +2 -0
- package/bridge-runtime/dist/debug-chats-page.js +491 -0
- package/bridge-runtime/dist/dom-transcript-store.d.ts +17 -0
- package/bridge-runtime/dist/dom-transcript-store.js +76 -0
- package/bridge-runtime/dist/extract-page.js +56 -85
- package/bridge-runtime/dist/history-limit.d.ts +2 -0
- package/bridge-runtime/dist/history-limit.js +2 -0
- package/bridge-runtime/dist/history-request.d.ts +8 -0
- package/bridge-runtime/dist/history-request.js +7 -0
- package/bridge-runtime/dist/index.js +1 -0
- package/bridge-runtime/dist/jsonl-index.d.ts +21 -3
- package/bridge-runtime/dist/jsonl-index.js +237 -73
- package/bridge-runtime/dist/jsonl-live-debug.d.ts +24 -0
- package/bridge-runtime/dist/jsonl-live-debug.js +175 -0
- package/bridge-runtime/dist/media-path.d.ts +2 -0
- package/bridge-runtime/dist/media-path.js +17 -0
- package/bridge-runtime/dist/message-filter.d.ts +2 -0
- package/bridge-runtime/dist/message-filter.js +21 -5
- package/bridge-runtime/dist/pairing-code.d.ts +2 -0
- package/bridge-runtime/dist/pairing-code.js +9 -2
- package/bridge-runtime/dist/relay-upstream.d.ts +2 -1
- package/bridge-runtime/dist/relay-upstream.js +4 -1
- package/bridge-runtime/dist/relay.d.ts +21 -0
- package/bridge-runtime/dist/relay.js +332 -28
- package/bridge-runtime/dist/types.d.ts +21 -0
- package/bridge-runtime/selectors.json +4 -5
- package/dist/index.js +79 -20
- package/dist/launch.js +23 -5
- package/dist/macos-autostart.js +87 -0
- package/dist/pairing-code.js +12 -3
- package/dist/print-pairing.js +2 -0
- package/dist/run-service.js +31 -0
- package/dist/startup-check.js +165 -0
- package/package.json +1 -1
- package/version-policy.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { join, resolve } from 'path';
|
|
3
3
|
import { ensurePairingIdentity, loadPairingIdentity, refreshPairingCode, } from './pairing-identity.js';
|
|
4
|
-
import { ensureCursorCdp, isBridgeRunning, startBridge, stopBridge,
|
|
4
|
+
import { ensureCursorCdp, isBridgeRunning, startBridge, stopBridge, } from './launch.js';
|
|
5
5
|
import { printPairingToTerminal } from './print-pairing.js';
|
|
6
6
|
import { BRIDGE_LOG_FILE } from './paths.js';
|
|
7
7
|
import { ensureUserConfig, isValidBridgeDir, resolveBridgeDir, USER_CONFIG_ENV, } from './bridge-dir.js';
|
|
8
8
|
import { configFilePath, saveInstallConfig } from './repo-root.js';
|
|
9
9
|
import { formatDiagnoseReport, runDiagnose } from './diagnose.js';
|
|
10
|
-
import {
|
|
10
|
+
import { awaitStartupReadiness, formatStartupChecklist, printStartupFailure, snapshotStartupReadiness, } from './startup-check.js';
|
|
11
|
+
import { DEFAULT_RELAY_URL, loadEnvFile, resolveRelayConfig } from './relay-config.js';
|
|
11
12
|
import { ensureCliVersionForRelay } from './version-check.js';
|
|
12
13
|
import { assertBundledBridgeSupportsRelay, bundledBridgeHasClientVersion, } from './bundled-bridge-check.js';
|
|
13
14
|
import { CLI_VERSION } from './cli-version.js';
|
|
15
|
+
import { askYesNo } from './ask.js';
|
|
16
|
+
import { installCursorCdpLauncher, installMacAutostart, isAutostartInstalled, uninstallMacAutostart, } from './macos-autostart.js';
|
|
17
|
+
import { runServiceLoop } from './run-service.js';
|
|
14
18
|
function bridgeEnv(identity) {
|
|
15
19
|
ensureUserConfig();
|
|
16
20
|
const bridgeDotEnv = join(resolveBridgeDir(), '.env');
|
|
@@ -63,27 +67,57 @@ async function cmdStart(argv) {
|
|
|
63
67
|
stopBridge();
|
|
64
68
|
}
|
|
65
69
|
startBridge(env);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.error('[cursorconnect] Код не зарегистрирован на relay — приложение его не примет.');
|
|
73
|
-
if (!pairingReady.connectorInRoom) {
|
|
74
|
-
console.error(' Mac не подключён к серверу. Обновите CLI: npm install -g cursorconnect@latest');
|
|
75
|
-
console.error(' Затем снова: cursorconnect start');
|
|
76
|
-
console.error(` Лог bridge: ${BRIDGE_LOG_FILE}`);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
console.error(' Connector в комнате, но код не на сервере — подождите и повторите start.');
|
|
80
|
-
}
|
|
70
|
+
const readiness = await awaitStartupReadiness(relayUrl, refreshed, {
|
|
71
|
+
waitMs: 45_000,
|
|
72
|
+
requireCdp: cdpPortOk,
|
|
73
|
+
});
|
|
74
|
+
if (!readiness.ready) {
|
|
75
|
+
printStartupFailure(readiness, BRIDGE_LOG_FILE);
|
|
81
76
|
process.exit(1);
|
|
82
77
|
}
|
|
83
|
-
if (cdpPortOk &&
|
|
84
|
-
console.
|
|
78
|
+
if (cdpPortOk && !readiness.localCdp) {
|
|
79
|
+
console.warn(`[cursorconnect] CDP не подключён (cdp:false) · лог: ${BRIDGE_LOG_FILE}`);
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(' Проверки перед pairing:');
|
|
83
|
+
for (const line of formatStartupChecklist(readiness)) {
|
|
84
|
+
console.log(` ${line}`);
|
|
85
85
|
}
|
|
86
|
+
console.log('');
|
|
86
87
|
printPairingToTerminal(refreshed);
|
|
88
|
+
if (process.stdin.isTTY && !isAutostartInstalled()) {
|
|
89
|
+
const yes = await askYesNo('Включить автозапуск Cursor Connect при входе в macOS? (y/n): ');
|
|
90
|
+
if (yes) {
|
|
91
|
+
installMacAutostart();
|
|
92
|
+
const cdp = installCursorCdpLauncher();
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(' Автозапуск включён (launchd).');
|
|
95
|
+
console.log(' Cursor с CDP: откройте из Dock скрипт');
|
|
96
|
+
console.log(` ${cdp}`);
|
|
97
|
+
console.log(' (перетащите в Dock или «Программы при входе» в настройках macOS).');
|
|
98
|
+
console.log(' Код pairing: cursorconnect status');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function cmdRunService() {
|
|
103
|
+
assertBundledBridgeSupportsRelay();
|
|
104
|
+
const identity = ensurePairingIdentity();
|
|
105
|
+
refreshPairingCode(identity);
|
|
106
|
+
const env = bridgeEnv(identity);
|
|
107
|
+
await ensureCliVersionForRelay(env.RELAY_URL);
|
|
108
|
+
console.log('[cursorconnect] run-service (фон, для launchd)…');
|
|
109
|
+
await runServiceLoop(env);
|
|
110
|
+
}
|
|
111
|
+
async function cmdInstallAutostart() {
|
|
112
|
+
installMacAutostart();
|
|
113
|
+
const cdp = installCursorCdpLauncher();
|
|
114
|
+
console.log('Автозапуск Cursor Connect включён.');
|
|
115
|
+
console.log(`Cursor с CDP (9222): ${cdp}`);
|
|
116
|
+
console.log('Код на телефон: cursorconnect status');
|
|
117
|
+
}
|
|
118
|
+
async function cmdUninstallAutostart() {
|
|
119
|
+
uninstallMacAutostart();
|
|
120
|
+
console.log('Автозапуск Cursor Connect отключён.');
|
|
87
121
|
}
|
|
88
122
|
async function cmdStop() {
|
|
89
123
|
stopBridge();
|
|
@@ -121,6 +155,19 @@ async function cmdStatus() {
|
|
|
121
155
|
? 'Bridge bundle: OK (clientVersion в relay-upstream)'
|
|
122
156
|
: 'Bridge bundle: STALE — обновите npm install -g cursorconnect@latest');
|
|
123
157
|
console.log(`Bridge process: ${isBridgeRunning() ? 'running' : 'stopped'}`);
|
|
158
|
+
const { relayUrl: relayUrlRaw } = resolveRelayConfig([USER_CONFIG_ENV]);
|
|
159
|
+
const relayUrl = (relayUrlRaw || DEFAULT_RELAY_URL).replace(/\/$/, '');
|
|
160
|
+
const readiness = await snapshotStartupReadiness(relayUrl, identity);
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(' Состояние:');
|
|
163
|
+
for (const line of formatStartupChecklist(readiness)) {
|
|
164
|
+
console.log(` ${line}`);
|
|
165
|
+
}
|
|
166
|
+
if (!readiness.ready) {
|
|
167
|
+
console.log('');
|
|
168
|
+
console.warn(' Код в терминале может не принять приложение — выполните: cursorconnect start');
|
|
169
|
+
}
|
|
170
|
+
console.log('');
|
|
124
171
|
printPairingToTerminal(identity);
|
|
125
172
|
}
|
|
126
173
|
async function main() {
|
|
@@ -143,6 +190,15 @@ async function main() {
|
|
|
143
190
|
case 'init':
|
|
144
191
|
await cmdInit(rest[0]);
|
|
145
192
|
break;
|
|
193
|
+
case 'run-service':
|
|
194
|
+
await cmdRunService();
|
|
195
|
+
break;
|
|
196
|
+
case 'install-autostart':
|
|
197
|
+
await cmdInstallAutostart();
|
|
198
|
+
break;
|
|
199
|
+
case 'uninstall-autostart':
|
|
200
|
+
await cmdUninstallAutostart();
|
|
201
|
+
break;
|
|
146
202
|
default:
|
|
147
203
|
console.log(`Usage: cursorconnect <command>
|
|
148
204
|
|
|
@@ -151,12 +207,15 @@ async function main() {
|
|
|
151
207
|
start --no-restart-cursor — не трогать Cursor (спросит y/n в TTY)
|
|
152
208
|
stop — остановить Cursor Connect
|
|
153
209
|
status — код и статус
|
|
210
|
+
install-autostart — Mac: Cursor Connect при входе (launchd)
|
|
211
|
+
uninstall-autostart — отключить автозапуск
|
|
154
212
|
diagnose — проверка relay / Cursor Connect / socket (без гаданий)
|
|
155
213
|
init [path] — только для разработки (клон репо)
|
|
156
214
|
|
|
157
215
|
Установка (из любой папки):
|
|
158
216
|
npm install -g cursorconnect
|
|
159
|
-
cursorconnect start
|
|
217
|
+
cursorconnect start # первый раз: код + опционально автозапуск
|
|
218
|
+
cursorconnect install-autostart # без консоли каждый день`);
|
|
160
219
|
process.exit(cmd === 'help' ? 0 : 1);
|
|
161
220
|
}
|
|
162
221
|
}
|
package/dist/launch.js
CHANGED
|
@@ -121,21 +121,37 @@ export async function waitRelayConnector(relayUrl, maxMs = 45_000) {
|
|
|
121
121
|
}
|
|
122
122
|
return false;
|
|
123
123
|
}
|
|
124
|
-
/** Mac в relay
|
|
125
|
-
export async function waitRelayPairingReady(relayUrl, roomId, maxMs = 45_000) {
|
|
124
|
+
/** Mac в relay-комнате, код на сервере, токен зарегистрирован (иначе app получит 404). */
|
|
125
|
+
export async function waitRelayPairingReady(relayUrl, roomId, maxMs = 45_000, clientToken) {
|
|
126
126
|
const base = relayUrl.replace(/\/$/, '');
|
|
127
127
|
const deadline = Date.now() + maxMs;
|
|
128
128
|
let lastConnector = false;
|
|
129
129
|
let lastCodes = 0;
|
|
130
|
+
let lastTokens = 0;
|
|
131
|
+
let lastTokenRegistered = false;
|
|
130
132
|
while (Date.now() < deadline) {
|
|
131
133
|
try {
|
|
132
|
-
const
|
|
134
|
+
const q = new URLSearchParams({ roomId });
|
|
135
|
+
if (clientToken)
|
|
136
|
+
q.set('token', clientToken);
|
|
137
|
+
const res = await fetch(`${base}/api/diagnostics?${q}`, {
|
|
138
|
+
signal: AbortSignal.timeout(8_000),
|
|
139
|
+
});
|
|
133
140
|
if (res.ok) {
|
|
134
141
|
const d = (await res.json());
|
|
135
142
|
lastConnector = Boolean(d.connectorInRoom);
|
|
136
143
|
lastCodes = d.pairingCodesActive ?? 0;
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
lastTokens = d.tokensRegistered ?? 0;
|
|
145
|
+
lastTokenRegistered = d.tokenRegistered === true;
|
|
146
|
+
const tokenOk = clientToken ? lastTokenRegistered : lastTokens > 0;
|
|
147
|
+
if (lastConnector && lastCodes > 0 && tokenOk) {
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
connectorInRoom: true,
|
|
151
|
+
pairingCodesActive: lastCodes,
|
|
152
|
+
tokensRegistered: lastTokens,
|
|
153
|
+
tokenRegistered: lastTokenRegistered,
|
|
154
|
+
};
|
|
139
155
|
}
|
|
140
156
|
}
|
|
141
157
|
}
|
|
@@ -148,6 +164,8 @@ export async function waitRelayPairingReady(relayUrl, roomId, maxMs = 45_000) {
|
|
|
148
164
|
ok: false,
|
|
149
165
|
connectorInRoom: lastConnector,
|
|
150
166
|
pairingCodesActive: lastCodes,
|
|
167
|
+
tokensRegistered: lastTokens,
|
|
168
|
+
tokenRegistered: lastTokenRegistered,
|
|
151
169
|
};
|
|
152
170
|
}
|
|
153
171
|
export function isCursorRunning() {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { CURSORCONNECT_DIR } from './paths.js';
|
|
6
|
+
const LABEL = 'com.cursorconnect.service';
|
|
7
|
+
const LAUNCH_AGENT = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
8
|
+
const CURSOR_CDP_SCRIPT = join(CURSORCONNECT_DIR, 'bin', 'open-cursor-cdp.sh');
|
|
9
|
+
export function resolveCursorconnectBin() {
|
|
10
|
+
try {
|
|
11
|
+
return execSync('command -v cursorconnect', { encoding: 'utf-8' }).trim();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 'cursorconnect';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function launchctlLoad() {
|
|
18
|
+
execSync(`launchctl bootout gui/$(id -u) "${LAUNCH_AGENT}" 2>/dev/null || true`, {
|
|
19
|
+
stdio: 'ignore',
|
|
20
|
+
shell: '/bin/bash',
|
|
21
|
+
});
|
|
22
|
+
execSync(`launchctl bootstrap gui/$(id -u) "${LAUNCH_AGENT}"`, {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
shell: '/bin/bash',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function isAutostartInstalled() {
|
|
28
|
+
return existsSync(LAUNCH_AGENT);
|
|
29
|
+
}
|
|
30
|
+
export function installMacAutostart() {
|
|
31
|
+
mkdirSync(CURSORCONNECT_DIR, { recursive: true, mode: 0o700 });
|
|
32
|
+
const bin = resolveCursorconnectBin();
|
|
33
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
34
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
35
|
+
<plist version="1.0">
|
|
36
|
+
<dict>
|
|
37
|
+
<key>Label</key>
|
|
38
|
+
<string>${LABEL}</string>
|
|
39
|
+
<key>ProgramArguments</key>
|
|
40
|
+
<array>
|
|
41
|
+
<string>${bin}</string>
|
|
42
|
+
<string>run-service</string>
|
|
43
|
+
</array>
|
|
44
|
+
<key>RunAtLoad</key>
|
|
45
|
+
<true/>
|
|
46
|
+
<key>KeepAlive</key>
|
|
47
|
+
<true/>
|
|
48
|
+
<key>StandardOutPath</key>
|
|
49
|
+
<string>${join(CURSORCONNECT_DIR, 'launchd.out.log')}</string>
|
|
50
|
+
<key>StandardErrorPath</key>
|
|
51
|
+
<string>${join(CURSORCONNECT_DIR, 'launchd.err.log')}</string>
|
|
52
|
+
<key>EnvironmentVariables</key>
|
|
53
|
+
<dict>
|
|
54
|
+
<key>PATH</key>
|
|
55
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
56
|
+
</dict>
|
|
57
|
+
</dict>
|
|
58
|
+
</plist>
|
|
59
|
+
`;
|
|
60
|
+
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
61
|
+
writeFileSync(LAUNCH_AGENT, plist, { mode: 0o644 });
|
|
62
|
+
launchctlLoad();
|
|
63
|
+
}
|
|
64
|
+
export function uninstallMacAutostart() {
|
|
65
|
+
if (existsSync(LAUNCH_AGENT)) {
|
|
66
|
+
try {
|
|
67
|
+
execSync(`launchctl bootout gui/$(id -u) "${LAUNCH_AGENT}" 2>/dev/null || true`, {
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
shell: '/bin/bash',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
unlinkSync(LAUNCH_AGENT);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Скрипт: Cursor с CDP (порт 9222) — положите в Dock вместо обычного Cursor. */
|
|
79
|
+
export function installCursorCdpLauncher() {
|
|
80
|
+
mkdirSync(join(CURSORCONNECT_DIR, 'bin'), { recursive: true, mode: 0o700 });
|
|
81
|
+
const script = `#!/bin/bash
|
|
82
|
+
# Запуск Cursor с --remote-debugging-port=9222 (нужен для Cursor Connect)
|
|
83
|
+
open -a Cursor --args --remote-debugging-port=9222
|
|
84
|
+
`;
|
|
85
|
+
writeFileSync(CURSOR_CDP_SCRIPT, script, { mode: 0o755 });
|
|
86
|
+
return CURSOR_CDP_SCRIPT;
|
|
87
|
+
}
|
package/dist/pairing-code.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { randomBytes } from 'crypto';
|
|
2
2
|
export const PAIRING_CODE_LENGTH = 6;
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Без визуально похожих пар: 0/O/D/Q, 1/I/L, 8/B, 2/Z, 5/S, 6/G.
|
|
5
|
+
* Синхронизировать с bridge/src/pairing-code.ts и relay/src/pairing-code.ts.
|
|
6
|
+
*/
|
|
7
|
+
export const PAIRING_CODE_ALPHABET = 'ACDEFHJKMNPRTUVWXY3479';
|
|
8
|
+
const ALPHABET_SET = new Set(PAIRING_CODE_ALPHABET.split(''));
|
|
5
9
|
export function generatePairingCode() {
|
|
6
10
|
const bytes = randomBytes(PAIRING_CODE_LENGTH);
|
|
11
|
+
const n = PAIRING_CODE_ALPHABET.length;
|
|
7
12
|
let out = '';
|
|
8
13
|
for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
|
|
9
|
-
out +=
|
|
14
|
+
out += PAIRING_CODE_ALPHABET[bytes[i] % n];
|
|
10
15
|
}
|
|
11
16
|
return out;
|
|
12
17
|
}
|
|
@@ -14,5 +19,9 @@ export function normalizePairingCode(raw) {
|
|
|
14
19
|
const code = raw.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
15
20
|
if (code.length !== PAIRING_CODE_LENGTH)
|
|
16
21
|
return null;
|
|
22
|
+
for (const ch of code) {
|
|
23
|
+
if (!ALPHABET_SET.has(ch))
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
17
26
|
return code;
|
|
18
27
|
}
|
package/dist/print-pairing.js
CHANGED
|
@@ -14,6 +14,8 @@ export function printPairingToTerminal(identity) {
|
|
|
14
14
|
console.log('');
|
|
15
15
|
console.log(' Введите код в приложении Cursor Connect:');
|
|
16
16
|
console.log('');
|
|
17
|
+
console.log(` ${code}`);
|
|
18
|
+
console.log('');
|
|
17
19
|
console.log(renderBigCode(code));
|
|
18
20
|
console.log('');
|
|
19
21
|
console.log(' Код одноразовый');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ensurePairingIdentity, refreshPairingCode, } from './pairing-identity.js';
|
|
2
|
+
import { isBridgeRunning, startBridge, } from './launch.js';
|
|
3
|
+
import { BRIDGE_LOG_FILE } from './paths.js';
|
|
4
|
+
const WATCH_MS = 8_000;
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
7
|
+
}
|
|
8
|
+
/** Держит Cursor Connect в фоне (для launchd KeepAlive). */
|
|
9
|
+
export async function runServiceLoop(bridgeEnv) {
|
|
10
|
+
const identity = ensurePairingIdentity();
|
|
11
|
+
refreshPairingCode(identity);
|
|
12
|
+
const boot = () => {
|
|
13
|
+
if (isBridgeRunning())
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
startBridge(bridgeEnv);
|
|
17
|
+
console.log(`[cursorconnect] service: bridge started → ${BRIDGE_LOG_FILE}`);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
console.error('[cursorconnect] service: start failed', e);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
boot();
|
|
24
|
+
for (;;) {
|
|
25
|
+
await sleep(WATCH_MS);
|
|
26
|
+
if (!isBridgeRunning()) {
|
|
27
|
+
console.error('[cursorconnect] service: bridge exited, restarting…');
|
|
28
|
+
boot();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { BRIDGE_LOG_FILE } from './paths.js';
|
|
3
|
+
import { waitBridgeHealth, waitRelayPairingReady } from './launch.js';
|
|
4
|
+
export async function fetchRelayRoomDiagnostics(relayUrl, roomId, token) {
|
|
5
|
+
const base = relayUrl.replace(/\/$/, '');
|
|
6
|
+
const q = new URLSearchParams({ roomId });
|
|
7
|
+
if (token)
|
|
8
|
+
q.set('token', token);
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(`${base}/api/diagnostics?${q}`, {
|
|
11
|
+
signal: AbortSignal.timeout(12_000),
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok)
|
|
14
|
+
return null;
|
|
15
|
+
const d = (await res.json());
|
|
16
|
+
return {
|
|
17
|
+
ok: Boolean(d.ok),
|
|
18
|
+
connectorInRoom: Boolean(d.connectorInRoom),
|
|
19
|
+
clientsInRoom: Number(d.clientsInRoom ?? 0),
|
|
20
|
+
pairingCodesActive: Number(d.pairingCodesActive ?? 0),
|
|
21
|
+
tokensRegistered: Number(d.tokensRegistered ?? 0),
|
|
22
|
+
tokenRegistered: typeof d.tokenRegistered === 'boolean' ? d.tokenRegistered : null,
|
|
23
|
+
tokenAccepted: typeof d.tokenAccepted === 'boolean' ? d.tokenAccepted : null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function normalizePairingCode(code) {
|
|
31
|
+
return code.replace(/[^A-Z0-9]/g, '').toUpperCase().slice(0, 6);
|
|
32
|
+
}
|
|
33
|
+
export function buildStartupReadiness(identity, local, relay, relayReachable) {
|
|
34
|
+
const code = normalizePairingCode(identity.pairingCode);
|
|
35
|
+
const codeExpiresSec = Math.max(0, Math.floor((identity.pairingCodeExpiresAt - Date.now()) / 1000));
|
|
36
|
+
const codeValid = code.length === 6 && codeExpiresSec > 0;
|
|
37
|
+
const connectorInRoom = Boolean(relay?.connectorInRoom);
|
|
38
|
+
const pairingCodesActive = relay?.pairingCodesActive ?? 0;
|
|
39
|
+
const tokensRegistered = relay?.tokensRegistered ?? 0;
|
|
40
|
+
const tokenRegistered = relay?.tokenRegistered === true;
|
|
41
|
+
const ready = local.ok &&
|
|
42
|
+
codeValid &&
|
|
43
|
+
relayReachable &&
|
|
44
|
+
connectorInRoom &&
|
|
45
|
+
pairingCodesActive > 0 &&
|
|
46
|
+
tokensRegistered > 0 &&
|
|
47
|
+
tokenRegistered;
|
|
48
|
+
return {
|
|
49
|
+
localBridgeOk: local.ok,
|
|
50
|
+
localCdp: local.cdp,
|
|
51
|
+
relayReachable,
|
|
52
|
+
connectorInRoom,
|
|
53
|
+
pairingCodesActive,
|
|
54
|
+
tokensRegistered,
|
|
55
|
+
tokenRegistered,
|
|
56
|
+
ready,
|
|
57
|
+
code,
|
|
58
|
+
codeExpiresSec,
|
|
59
|
+
machineLabel: identity.machineLabel,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Ждём bridge + relay (комната, код на сервере, токен зарегистрирован). */
|
|
63
|
+
export async function awaitStartupReadiness(relayUrl, identity, opts) {
|
|
64
|
+
const waitMs = opts?.waitMs ?? 45_000;
|
|
65
|
+
const health = await waitBridgeHealth(Math.min(waitMs, 25_000), {
|
|
66
|
+
requireCdp: opts?.requireCdp === true,
|
|
67
|
+
});
|
|
68
|
+
let relayReachable = false;
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${relayUrl.replace(/\/$/, '')}/health`, {
|
|
71
|
+
signal: AbortSignal.timeout(8_000),
|
|
72
|
+
});
|
|
73
|
+
relayReachable = res.ok;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
relayReachable = false;
|
|
77
|
+
}
|
|
78
|
+
await waitRelayPairingReady(relayUrl, identity.roomId, waitMs, identity.clientToken);
|
|
79
|
+
const relay = await fetchRelayRoomDiagnostics(relayUrl, identity.roomId, identity.clientToken);
|
|
80
|
+
return buildStartupReadiness(identity, { ok: health.ok, cdp: health.cdp }, relay, relayReachable);
|
|
81
|
+
}
|
|
82
|
+
/** Однократная проверка (для status). */
|
|
83
|
+
export async function snapshotStartupReadiness(relayUrl, identity) {
|
|
84
|
+
let local = { ok: false, cdp: false };
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch('http://127.0.0.1:3847/health', {
|
|
87
|
+
signal: AbortSignal.timeout(3_000),
|
|
88
|
+
});
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
const j = (await res.json());
|
|
91
|
+
local = { ok: Boolean(j.ok), cdp: Boolean(j.cdp) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* ignore */
|
|
96
|
+
}
|
|
97
|
+
let relayReachable = false;
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(`${relayUrl.replace(/\/$/, '')}/health`, {
|
|
100
|
+
signal: AbortSignal.timeout(8_000),
|
|
101
|
+
});
|
|
102
|
+
relayReachable = res.ok;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
relayReachable = false;
|
|
106
|
+
}
|
|
107
|
+
const relay = await fetchRelayRoomDiagnostics(relayUrl, identity.roomId, identity.clientToken);
|
|
108
|
+
return buildStartupReadiness(identity, local, relay, relayReachable);
|
|
109
|
+
}
|
|
110
|
+
export function formatStartupChecklist(r) {
|
|
111
|
+
const mark = (ok) => (ok ? '✓' : '✗');
|
|
112
|
+
const lines = [
|
|
113
|
+
`${mark(r.localBridgeOk)} Cursor Connect на Mac (localhost:3847)`,
|
|
114
|
+
r.localBridgeOk
|
|
115
|
+
? ` cdp: ${r.localCdp ? 'подключён — live-чаты из Cursor' : 'нет — только архив JSONL'}`
|
|
116
|
+
: ' запустите cursorconnect start',
|
|
117
|
+
`${mark(r.relayReachable)} Relay ${r.relayReachable ? 'доступен' : 'недоступен'}`,
|
|
118
|
+
`${mark(r.connectorInRoom)} Mac в вашей комнате на relay`,
|
|
119
|
+
`${mark(r.pairingCodesActive > 0)} Код на сервере (можно вводить в приложении)`,
|
|
120
|
+
r.pairingCodesActive > 0
|
|
121
|
+
? ` активных кодов: ${r.pairingCodesActive}`
|
|
122
|
+
: ' код не зарегистрирован — повторите start (не вводите старый код из терминала)',
|
|
123
|
+
`${mark(r.tokenRegistered)} Токен телефона зарегистрирован bridge'ем`,
|
|
124
|
+
];
|
|
125
|
+
if (r.ready) {
|
|
126
|
+
lines.push('', ` Готово к pairing: код ${r.code} · ~${Math.ceil(r.codeExpiresSec / 60)} мин · одноразовый`);
|
|
127
|
+
}
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
export function printStartupFailure(r, logPath = BRIDGE_LOG_FILE) {
|
|
131
|
+
console.error('');
|
|
132
|
+
console.error('[cursorconnect] Запуск не готов — приложение код не примет.');
|
|
133
|
+
console.error('');
|
|
134
|
+
for (const line of formatStartupChecklist(r)) {
|
|
135
|
+
console.error(` ${line}`);
|
|
136
|
+
}
|
|
137
|
+
console.error('');
|
|
138
|
+
if (!r.connectorInRoom) {
|
|
139
|
+
console.error(' Mac не в комнате: npm install -g cursorconnect@latest && cursorconnect stop && cursorconnect start');
|
|
140
|
+
}
|
|
141
|
+
else if (r.pairingCodesActive === 0) {
|
|
142
|
+
console.error(' Код не на relay (уже введён или bridge не успел): cursorconnect stop && cursorconnect start');
|
|
143
|
+
console.error(' Не вводите код из прошлого запуска — только свежий после start.');
|
|
144
|
+
}
|
|
145
|
+
else if (!r.tokenRegistered) {
|
|
146
|
+
console.error(' Токен не зарегистрирован — перезапустите bridge (cursorconnect start).');
|
|
147
|
+
}
|
|
148
|
+
if (!r.localBridgeOk) {
|
|
149
|
+
console.error(` Лог: ${logPath}`);
|
|
150
|
+
tailLogLines(logPath, 12).forEach((l) => console.error(` ${l}`));
|
|
151
|
+
}
|
|
152
|
+
console.error('');
|
|
153
|
+
}
|
|
154
|
+
function tailLogLines(path, n) {
|
|
155
|
+
if (!existsSync(path))
|
|
156
|
+
return ['(лог пуст)'];
|
|
157
|
+
try {
|
|
158
|
+
const raw = readFileSync(path, 'utf-8');
|
|
159
|
+
const lines = raw.trim().split('\n').filter(Boolean);
|
|
160
|
+
return lines.slice(-n);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return ['(не удалось прочитать лог)'];
|
|
164
|
+
}
|
|
165
|
+
}
|
package/package.json
CHANGED
package/version-policy.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"minCliVersion": "0.1.4",
|
|
3
3
|
"minAppVersion": "0.2.2",
|
|
4
|
-
"latestCliVersion": "0.1.
|
|
4
|
+
"latestCliVersion": "0.1.7",
|
|
5
5
|
"latestAppVersion": "0.2.2",
|
|
6
6
|
"updateCliCommand": "npm install -g cursorconnect@latest",
|
|
7
7
|
"updateAppHint": "Обновите CursorConnect в App Store / TestFlight до последней сборки."
|