@cursorpool-dev/cli 0.5.6
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/bin/cursor-pool.mjs +9 -0
- package/bin/cursor-pool.ts +169 -0
- package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
- package/node_modules/@cursor-pool/extension/package.json +64 -0
- package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
- package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
- package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
- package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
- package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
- package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
- package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
- package/node_modules/@cursor-pool/patcher/package.json +17 -0
- package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
- package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
- package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
- package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
- package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
- package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
- package/node_modules/@cursor-pool/service/package.json +17 -0
- package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
- package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
- package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
- package/node_modules/@cursor-pool/service/src/health.ts +10 -0
- package/node_modules/@cursor-pool/service/src/index.ts +29 -0
- package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
- package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
- package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
- package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
- package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
- package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
- package/node_modules/@cursor-pool/service/src/server.ts +939 -0
- package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
- package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
- package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
- package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
- package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
- package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
- package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
- package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
- package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
- package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
- package/node_modules/@cursor-pool/shared/package.json +17 -0
- package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
- package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
- package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
- package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
- package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
- package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
- package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
- package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
- package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
- package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
- package/package.json +28 -0
- package/src/adHocResign.ts +65 -0
- package/src/autostart.ts +240 -0
- package/src/compat.ts +282 -0
- package/src/confirm.ts +76 -0
- package/src/cursor.ts +94 -0
- package/src/diagnostics.ts +558 -0
- package/src/environment.ts +18 -0
- package/src/extensionBundle.ts +111 -0
- package/src/extensionLink.ts +168 -0
- package/src/index.ts +23 -0
- package/src/install.ts +614 -0
- package/src/installRecord.ts +105 -0
- package/src/launch.ts +182 -0
- package/src/patchSet.ts +182 -0
- package/src/platform.ts +132 -0
- package/src/repair.ts +383 -0
- package/src/restore.ts +153 -0
- package/src/serviceCommands.ts +79 -0
- package/src/serviceProcess.ts +188 -0
- package/src/status.ts +241 -0
- package/src/target.ts +37 -0
- package/src/trial.ts +133 -0
- package/src/uninstall.ts +213 -0
- package/test/autostart.test.ts +151 -0
- package/test/compat.test.ts +192 -0
- package/test/confirm.test.ts +114 -0
- package/test/cursor-pool-bin.test.ts +658 -0
- package/test/cursor.test.ts +20 -0
- package/test/diagnostics.test.ts +709 -0
- package/test/e2e-install.test.ts +773 -0
- package/test/extensionBundle.test.ts +161 -0
- package/test/extensionLink.test.ts +209 -0
- package/test/install.test.ts +862 -0
- package/test/installRecord.test.ts +107 -0
- package/test/launch.test.ts +138 -0
- package/test/platform.test.ts +226 -0
- package/test/repair.test.ts +575 -0
- package/test/restore.test.ts +211 -0
- package/test/serviceCommands.test.ts +135 -0
- package/test/serviceProcess.test.ts +280 -0
- package/test/status.test.ts +615 -0
- package/test/target.test.ts +49 -0
- package/test/trial.test.ts +146 -0
|
@@ -0,0 +1,2910 @@
|
|
|
1
|
+
// packages/extension/src/api.ts
|
|
2
|
+
import { arch, hostname, platform as osPlatform } from "node:os";
|
|
3
|
+
|
|
4
|
+
// packages/service/src/health.ts
|
|
5
|
+
function buildHealth(runtime) {
|
|
6
|
+
return {
|
|
7
|
+
ok: true,
|
|
8
|
+
host: runtime.host,
|
|
9
|
+
port: runtime.port,
|
|
10
|
+
runtimeId: runtime.runtimeId
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// packages/shared/src/metadata.ts
|
|
15
|
+
var SAFE_METADATA_KEYS = [
|
|
16
|
+
"requestId",
|
|
17
|
+
"model",
|
|
18
|
+
"requestType",
|
|
19
|
+
"source",
|
|
20
|
+
"cursorVersion",
|
|
21
|
+
"clientVersion",
|
|
22
|
+
"entrypoint"
|
|
23
|
+
];
|
|
24
|
+
function sanitizeRequestMetadata(input) {
|
|
25
|
+
const output = {};
|
|
26
|
+
for (const key of SAFE_METADATA_KEYS) {
|
|
27
|
+
if (input[key] !== void 0) {
|
|
28
|
+
output[key] = input[key];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// packages/service/src/metadata.ts
|
|
35
|
+
var EXTENSION_STATUS_KEYS = ["connected", "cursorVersion", "clientVersion"];
|
|
36
|
+
function sanitizeServiceMetadata(input) {
|
|
37
|
+
return {
|
|
38
|
+
model: "unknown",
|
|
39
|
+
...sanitizeRequestMetadata(input)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sanitizeExtensionStatus(input) {
|
|
43
|
+
const output = {};
|
|
44
|
+
for (const key of EXTENSION_STATUS_KEYS) {
|
|
45
|
+
if (input[key] !== void 0) {
|
|
46
|
+
output[key] = input[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// packages/service/src/platformSession.ts
|
|
53
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
54
|
+
import { homedir } from "node:os";
|
|
55
|
+
import { dirname, join } from "node:path";
|
|
56
|
+
var DEFAULT_PLATFORM_SESSION_FILE = "~/.cursor-pool/session.json";
|
|
57
|
+
var SAFE_POOL_TOKEN_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
58
|
+
var SECRET_LIKE_PATTERN = /(api[_-]?key|authorization|bearer|cursor[_-]?auth|provider[_-]?secret|secret|token|sk-[A-Za-z0-9])/i;
|
|
59
|
+
function resolvePlatformSessionFile(sessionFile = DEFAULT_PLATFORM_SESSION_FILE) {
|
|
60
|
+
if (sessionFile.startsWith("~/")) {
|
|
61
|
+
return join(homedir(), sessionFile.slice(2));
|
|
62
|
+
}
|
|
63
|
+
return sessionFile;
|
|
64
|
+
}
|
|
65
|
+
function cleanApiBaseUrl(apiBaseUrl) {
|
|
66
|
+
return apiBaseUrl.replace(/\/+$/, "");
|
|
67
|
+
}
|
|
68
|
+
function isSafePoolToken(value) {
|
|
69
|
+
return typeof value === "string" && SAFE_POOL_TOKEN_PATTERN.test(value) && !SECRET_LIKE_PATTERN.test(value);
|
|
70
|
+
}
|
|
71
|
+
function cleanPoolTokens(values) {
|
|
72
|
+
return values.filter(isSafePoolToken);
|
|
73
|
+
}
|
|
74
|
+
function sanitizePoolSessionSummary(value) {
|
|
75
|
+
return {
|
|
76
|
+
id: value.id,
|
|
77
|
+
productId: value.productId,
|
|
78
|
+
providerType: value.providerType,
|
|
79
|
+
status: value.status,
|
|
80
|
+
startedAt: value.startedAt,
|
|
81
|
+
expiresAt: value.expiresAt,
|
|
82
|
+
routeTokenExpiresAt: value.routeTokenExpiresAt,
|
|
83
|
+
routeStrategy: "platform-gateway",
|
|
84
|
+
bannedModels: cleanPoolTokens(value.bannedModels),
|
|
85
|
+
...value.availableModels !== void 0 ? { availableModels: cleanPoolTokens(value.availableModels) } : {},
|
|
86
|
+
capabilities: {
|
|
87
|
+
streaming: value.capabilities.streaming,
|
|
88
|
+
usageEstimate: value.capabilities.usageEstimate,
|
|
89
|
+
hardSpendLimit: value.capabilities.hardSpendLimit
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function asStatus(session) {
|
|
94
|
+
return {
|
|
95
|
+
state: "logged-in",
|
|
96
|
+
user: session.user,
|
|
97
|
+
device: session.device,
|
|
98
|
+
mode: platformModeFromSession(session)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function isPlatformModeReleaseReason(value) {
|
|
102
|
+
return value === "product-missing" || value === "product-unavailable" || value === "insufficient-credits" || value === "invalid-token" || value === "device-inactive";
|
|
103
|
+
}
|
|
104
|
+
function inactiveModeFromSession(session) {
|
|
105
|
+
if (isPlatformModeReleaseReason(session?.lastModeReleaseReason) && typeof session.lastModeReleasedAt === "string") {
|
|
106
|
+
return {
|
|
107
|
+
state: "inactive",
|
|
108
|
+
releaseReason: session.lastModeReleaseReason,
|
|
109
|
+
releasedAt: session.lastModeReleasedAt
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { state: "inactive" };
|
|
113
|
+
}
|
|
114
|
+
function platformModeFromSession(session) {
|
|
115
|
+
if (session?.platformMode === "active" && typeof session.activeProductId === "string" && typeof session.platformModeStartedAt === "string") {
|
|
116
|
+
return {
|
|
117
|
+
state: "active",
|
|
118
|
+
productId: session.activeProductId,
|
|
119
|
+
startedAt: session.platformModeStartedAt
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return inactiveModeFromSession(session);
|
|
123
|
+
}
|
|
124
|
+
function clearPlatformMode(session) {
|
|
125
|
+
const {
|
|
126
|
+
platformMode: _platformMode,
|
|
127
|
+
activeProductId: _activeProductId,
|
|
128
|
+
platformModeStartedAt: _platformModeStartedAt,
|
|
129
|
+
poolSession: _poolSession,
|
|
130
|
+
routeToken: _routeToken,
|
|
131
|
+
...inactiveSession
|
|
132
|
+
} = session;
|
|
133
|
+
return inactiveSession;
|
|
134
|
+
}
|
|
135
|
+
function clearModeRelease(session) {
|
|
136
|
+
const {
|
|
137
|
+
lastModeReleaseReason: _lastModeReleaseReason,
|
|
138
|
+
lastModeReleasedAt: _lastModeReleasedAt,
|
|
139
|
+
...activeSession
|
|
140
|
+
} = session;
|
|
141
|
+
return activeSession;
|
|
142
|
+
}
|
|
143
|
+
function releasePlatformMode(session, reason) {
|
|
144
|
+
const releasedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
145
|
+
return {
|
|
146
|
+
session: {
|
|
147
|
+
...clearPlatformMode(session),
|
|
148
|
+
lastModeReleaseReason: reason,
|
|
149
|
+
lastModeReleasedAt: releasedAt
|
|
150
|
+
},
|
|
151
|
+
mode: { state: "inactive", releaseReason: reason, releasedAt }
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function releaseActivePlatformMode(session, reason, options) {
|
|
155
|
+
const mode = platformModeFromSession(session);
|
|
156
|
+
if (mode.state !== "active") {
|
|
157
|
+
return { session, mode };
|
|
158
|
+
}
|
|
159
|
+
const released = releasePlatformMode(session, reason);
|
|
160
|
+
await writePlatformSession(released.session, options);
|
|
161
|
+
return released;
|
|
162
|
+
}
|
|
163
|
+
async function releaseForInactiveDevice(session, options) {
|
|
164
|
+
if (session.device.status === "active") {
|
|
165
|
+
return { session, mode: platformModeFromSession(session) };
|
|
166
|
+
}
|
|
167
|
+
return releaseActivePlatformMode(session, "device-inactive", options);
|
|
168
|
+
}
|
|
169
|
+
function isUserSummary(value) {
|
|
170
|
+
const user = value;
|
|
171
|
+
return typeof user?.id === "string" && typeof user.email === "string";
|
|
172
|
+
}
|
|
173
|
+
function isDeviceSummary(value) {
|
|
174
|
+
const device = value;
|
|
175
|
+
return typeof device?.id === "string" && typeof device.status === "string" && typeof device.lastHeartbeatAt === "string";
|
|
176
|
+
}
|
|
177
|
+
function isProductSummary(value) {
|
|
178
|
+
const product = value;
|
|
179
|
+
return typeof product?.id === "string" && typeof product.name === "string" && typeof product.description === "string" && (product.status === "available" || product.status === "unavailable") && Number.isInteger(product.minCredits) && product.minCredits >= 0 && typeof product.usageLabel === "string";
|
|
180
|
+
}
|
|
181
|
+
function sanitizeProductSummary(value) {
|
|
182
|
+
if (!isProductSummary(value)) {
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
id: value.id,
|
|
187
|
+
name: value.name,
|
|
188
|
+
description: value.description,
|
|
189
|
+
status: value.status,
|
|
190
|
+
minCredits: value.minCredits,
|
|
191
|
+
usageLabel: value.usageLabel
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function isPoolSessionSummary(value) {
|
|
195
|
+
const session = value;
|
|
196
|
+
return typeof session?.id === "string" && typeof session.productId === "string" && typeof session.providerType === "string" && (session.status === "active" || session.status === "refresh-required" || session.status === "stopping" || session.status === "stopped" || session.status === "expired" || session.status === "failed") && typeof session.startedAt === "string" && typeof session.expiresAt === "string" && typeof session.routeTokenExpiresAt === "string" && session.routeStrategy === "platform-gateway" && Array.isArray(session.bannedModels) && session.bannedModels.every((model) => typeof model === "string") && (session.availableModels === void 0 || Array.isArray(session.availableModels) && session.availableModels.every((model) => typeof model === "string")) && typeof session.capabilities?.streaming === "boolean" && typeof session.capabilities.usageEstimate === "boolean" && typeof session.capabilities.hardSpendLimit === "boolean";
|
|
197
|
+
}
|
|
198
|
+
function isLocalRouteTokenState(value) {
|
|
199
|
+
const route = value;
|
|
200
|
+
return typeof route?.token === "string" && typeof route.expiresAt === "string";
|
|
201
|
+
}
|
|
202
|
+
function isPlatformSession(value) {
|
|
203
|
+
const session = value;
|
|
204
|
+
return typeof session?.apiBaseUrl === "string" && typeof session.deviceToken === "string" && typeof session.createdAt === "string" && isUserSummary(session.user) && isDeviceSummary(session.device) && (session.selectedProductId === void 0 || typeof session.selectedProductId === "string") && (session.platformMode === void 0 || session.platformMode === "active" || session.platformMode === "inactive") && (session.activeProductId === void 0 || typeof session.activeProductId === "string") && (session.platformModeStartedAt === void 0 || typeof session.platformModeStartedAt === "string") && (session.lastModeReleaseReason === void 0 || isPlatformModeReleaseReason(session.lastModeReleaseReason)) && (session.lastModeReleasedAt === void 0 || typeof session.lastModeReleasedAt === "string") && (session.poolSession === void 0 || isPoolSessionSummary(session.poolSession)) && (session.routeToken === void 0 || isLocalRouteTokenState(session.routeToken));
|
|
205
|
+
}
|
|
206
|
+
async function readPlatformSessionSnapshot(options = {}) {
|
|
207
|
+
const sessionFile = resolvePlatformSessionFile(options.sessionFile);
|
|
208
|
+
try {
|
|
209
|
+
const session = JSON.parse(await readFile(sessionFile, "utf8"));
|
|
210
|
+
return isPlatformSession(session) ? { state: "valid", session } : { state: "invalid" };
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error.code === "ENOENT") {
|
|
213
|
+
return { state: "missing" };
|
|
214
|
+
}
|
|
215
|
+
if (error instanceof SyntaxError) {
|
|
216
|
+
return { state: "invalid" };
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function readPlatformGatewayForwardContext(options = {}) {
|
|
222
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
223
|
+
if (snapshot.state !== "valid") {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
apiBaseUrl: snapshot.session.apiBaseUrl,
|
|
228
|
+
deviceToken: snapshot.session.deviceToken,
|
|
229
|
+
routeToken: snapshot.session.routeToken?.token,
|
|
230
|
+
poolSessionId: snapshot.session.poolSession?.id
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async function readPlatformSession(options = {}) {
|
|
234
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
235
|
+
return snapshot.state === "valid" ? snapshot.session : null;
|
|
236
|
+
}
|
|
237
|
+
async function writePlatformSession(session, options = {}) {
|
|
238
|
+
const sessionFile = resolvePlatformSessionFile(options.sessionFile);
|
|
239
|
+
await mkdir(dirname(sessionFile), { recursive: true, mode: 448 });
|
|
240
|
+
await writeFile(sessionFile, `${JSON.stringify(session, null, 2)}
|
|
241
|
+
`, { encoding: "utf8", mode: 384 });
|
|
242
|
+
await chmod(sessionFile, 384);
|
|
243
|
+
}
|
|
244
|
+
async function clearPlatformSession(options = {}) {
|
|
245
|
+
await rm(resolvePlatformSessionFile(options.sessionFile), { force: true });
|
|
246
|
+
}
|
|
247
|
+
async function requestJson(url, options = {}) {
|
|
248
|
+
const headers = {};
|
|
249
|
+
if (options.body) {
|
|
250
|
+
headers["content-type"] = "application/json";
|
|
251
|
+
}
|
|
252
|
+
if (options.token) {
|
|
253
|
+
headers.authorization = `Bearer ${options.token}`;
|
|
254
|
+
}
|
|
255
|
+
const response = await fetch(url, {
|
|
256
|
+
method: options.method ?? "GET",
|
|
257
|
+
headers,
|
|
258
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const error = new Error(`Request failed with status ${response.status}`);
|
|
262
|
+
error.status = response.status;
|
|
263
|
+
try {
|
|
264
|
+
const body = await response.json();
|
|
265
|
+
const detail = body.detail;
|
|
266
|
+
if (typeof detail?.code === "string" && /^[A-Z0-9_:-]{1,96}$/.test(detail.code)) {
|
|
267
|
+
error.platformCode = detail.code;
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
return await response.json();
|
|
274
|
+
}
|
|
275
|
+
async function exchangeDeviceToken(request) {
|
|
276
|
+
return requestJson(`${cleanApiBaseUrl(request.apiBaseUrl)}/auth/device-token`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
body: {
|
|
279
|
+
code: request.code,
|
|
280
|
+
device: request.device
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function exchangePasswordDeviceToken(request) {
|
|
285
|
+
return requestJson(`${cleanApiBaseUrl(request.apiBaseUrl)}/auth/password-device-token`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: {
|
|
288
|
+
email: request.email,
|
|
289
|
+
password: request.password,
|
|
290
|
+
device: request.device
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async function fetchMe(session) {
|
|
295
|
+
return requestJson(`${cleanApiBaseUrl(session.apiBaseUrl)}/me`, {
|
|
296
|
+
token: session.deviceToken
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function fetchAccountSummary(session) {
|
|
300
|
+
return requestJson(`${cleanApiBaseUrl(session.apiBaseUrl)}/account/summary`, {
|
|
301
|
+
token: session.deviceToken
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function fetchProducts(session) {
|
|
305
|
+
const response = await requestJson(`${cleanApiBaseUrl(session.apiBaseUrl)}/products`, {
|
|
306
|
+
token: session.deviceToken
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
products: Array.isArray(response.products) ? response.products.flatMap((product) => {
|
|
310
|
+
const sanitized = sanitizeProductSummary(product);
|
|
311
|
+
return sanitized ? [sanitized] : [];
|
|
312
|
+
}) : []
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async function postHeartbeat(request) {
|
|
316
|
+
return requestJson(`${cleanApiBaseUrl(request.session.apiBaseUrl)}/devices/heartbeat`, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
body: request.payload,
|
|
319
|
+
token: request.session.deviceToken
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function postLogout(request) {
|
|
323
|
+
await requestJson(`${cleanApiBaseUrl(request.session.apiBaseUrl)}/auth/logout`, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
token: request.session.deviceToken
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function startPoolSession(request) {
|
|
329
|
+
return requestJson(
|
|
330
|
+
`${cleanApiBaseUrl(request.session.apiBaseUrl)}/pool-sessions/start`,
|
|
331
|
+
{
|
|
332
|
+
method: "POST",
|
|
333
|
+
token: request.session.deviceToken,
|
|
334
|
+
body: {
|
|
335
|
+
productId: request.productId,
|
|
336
|
+
...request.client ? { client: request.client } : {}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
async function stopPoolSession(request) {
|
|
342
|
+
return requestJson(
|
|
343
|
+
`${cleanApiBaseUrl(request.session.apiBaseUrl)}/pool-sessions/${request.poolSessionId}/stop`,
|
|
344
|
+
{
|
|
345
|
+
method: "POST",
|
|
346
|
+
token: request.session.deviceToken,
|
|
347
|
+
body: {
|
|
348
|
+
reason: request.reason
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
function poolFailureToModeResult(reason, product, currentCredits) {
|
|
354
|
+
if (reason === "INSUFFICIENT_CREDITS") {
|
|
355
|
+
return {
|
|
356
|
+
state: "insufficient-credits",
|
|
357
|
+
productId: product.id,
|
|
358
|
+
requiredCredits: product.minCredits,
|
|
359
|
+
currentCredits
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (reason === "DEVICE_INVALID") {
|
|
363
|
+
return { state: "device-inactive" };
|
|
364
|
+
}
|
|
365
|
+
if (reason === "PRODUCT_UNAVAILABLE") {
|
|
366
|
+
return { state: "product-unavailable", productId: product.id };
|
|
367
|
+
}
|
|
368
|
+
if (reason === "PROVIDER_UNAVAILABLE") {
|
|
369
|
+
return { state: "provider-unavailable" };
|
|
370
|
+
}
|
|
371
|
+
if (reason === "RATE_LIMITED") {
|
|
372
|
+
return { state: "provider-rate-limited" };
|
|
373
|
+
}
|
|
374
|
+
if (reason === "MODEL_BANNED") {
|
|
375
|
+
return { state: "model-banned" };
|
|
376
|
+
}
|
|
377
|
+
return { state: "manual-review-required" };
|
|
378
|
+
}
|
|
379
|
+
async function statusFromRequestError(error, session, options = {}) {
|
|
380
|
+
if (error.status === 401) {
|
|
381
|
+
const currentSession = await readPlatformSession(options) ?? session;
|
|
382
|
+
const { mode } = options.releaseActiveModeOnUnauthorized ? await releaseActivePlatformMode(currentSession, "invalid-token", options) : { mode: platformModeFromSession(currentSession) };
|
|
383
|
+
return { state: "invalid-token", user: currentSession.user, device: currentSession.device, mode };
|
|
384
|
+
}
|
|
385
|
+
return { state: "offline", user: session.user, device: session.device, mode: platformModeFromSession(session) };
|
|
386
|
+
}
|
|
387
|
+
async function loginWithCode(options) {
|
|
388
|
+
const tokenResponse = await (options.exchangeDeviceToken ?? exchangeDeviceToken)({
|
|
389
|
+
code: options.code,
|
|
390
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
391
|
+
device: options.device ?? {}
|
|
392
|
+
});
|
|
393
|
+
const session = {
|
|
394
|
+
apiBaseUrl: cleanApiBaseUrl(options.apiBaseUrl),
|
|
395
|
+
deviceToken: tokenResponse.deviceToken,
|
|
396
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
397
|
+
user: tokenResponse.user,
|
|
398
|
+
device: tokenResponse.device
|
|
399
|
+
};
|
|
400
|
+
await writePlatformSession(session, { sessionFile: options.sessionFile });
|
|
401
|
+
return asStatus(session);
|
|
402
|
+
}
|
|
403
|
+
async function loginWithPassword(options) {
|
|
404
|
+
const tokenResponse = await (options.exchangePasswordDeviceToken ?? exchangePasswordDeviceToken)({
|
|
405
|
+
email: options.email,
|
|
406
|
+
password: options.password,
|
|
407
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
408
|
+
device: options.device ?? {}
|
|
409
|
+
});
|
|
410
|
+
const session = {
|
|
411
|
+
apiBaseUrl: cleanApiBaseUrl(options.apiBaseUrl),
|
|
412
|
+
deviceToken: tokenResponse.deviceToken,
|
|
413
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
414
|
+
user: tokenResponse.user,
|
|
415
|
+
device: tokenResponse.device
|
|
416
|
+
};
|
|
417
|
+
await writePlatformSession(session, { sessionFile: options.sessionFile });
|
|
418
|
+
return asStatus(session);
|
|
419
|
+
}
|
|
420
|
+
async function platformStatus(options = {}) {
|
|
421
|
+
const session = await readPlatformSession(options);
|
|
422
|
+
if (!session) {
|
|
423
|
+
return { state: "logged-out" };
|
|
424
|
+
}
|
|
425
|
+
let me;
|
|
426
|
+
try {
|
|
427
|
+
me = await fetchMe(session);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return statusFromRequestError(error, session, {
|
|
430
|
+
...options,
|
|
431
|
+
releaseActiveModeOnUnauthorized: true
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const currentSession = await readPlatformSession(options);
|
|
435
|
+
if (!currentSession) {
|
|
436
|
+
return { state: "logged-out" };
|
|
437
|
+
}
|
|
438
|
+
const updatedSession = { ...currentSession, user: me.user, device: me.device };
|
|
439
|
+
await writePlatformSession(updatedSession, options);
|
|
440
|
+
const released = await releaseForInactiveDevice(updatedSession, options);
|
|
441
|
+
return {
|
|
442
|
+
state: "logged-in",
|
|
443
|
+
user: released.session.user,
|
|
444
|
+
device: released.session.device,
|
|
445
|
+
mode: released.mode
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function platformCatalog(options = {}) {
|
|
449
|
+
const session = await readPlatformSession(options);
|
|
450
|
+
if (!session) {
|
|
451
|
+
return { state: "logged-out", products: [] };
|
|
452
|
+
}
|
|
453
|
+
const [account, products] = await Promise.allSettled([
|
|
454
|
+
fetchAccountSummary(session),
|
|
455
|
+
fetchProducts(session)
|
|
456
|
+
]);
|
|
457
|
+
const failures = [account, products].filter((result) => result.status === "rejected");
|
|
458
|
+
if (failures.some((result) => result.reason.status === 401)) {
|
|
459
|
+
const { mode: mode2 } = await releaseActivePlatformMode(session, "invalid-token", options);
|
|
460
|
+
return { state: "invalid-token", mode: mode2, products: [] };
|
|
461
|
+
}
|
|
462
|
+
if (failures.length > 0) {
|
|
463
|
+
return { state: "offline", products: [] };
|
|
464
|
+
}
|
|
465
|
+
const catalogProducts = products.value.products;
|
|
466
|
+
const selectedProductId = catalogProducts.some((product) => product.id === session.selectedProductId) ? session.selectedProductId : void 0;
|
|
467
|
+
let updatedSession = session;
|
|
468
|
+
let mode = platformModeFromSession(session);
|
|
469
|
+
let shouldWriteSession = false;
|
|
470
|
+
if (session.selectedProductId && !selectedProductId) {
|
|
471
|
+
const { selectedProductId: _staleSelection, ...sessionWithoutStaleSelection } = session;
|
|
472
|
+
if (mode.state === "active") {
|
|
473
|
+
const released = releasePlatformMode(sessionWithoutStaleSelection, "product-missing");
|
|
474
|
+
updatedSession = released.session;
|
|
475
|
+
mode = released.mode;
|
|
476
|
+
} else {
|
|
477
|
+
updatedSession = clearPlatformMode(sessionWithoutStaleSelection);
|
|
478
|
+
mode = { state: "inactive" };
|
|
479
|
+
}
|
|
480
|
+
shouldWriteSession = true;
|
|
481
|
+
} else if (mode.state === "active") {
|
|
482
|
+
const activeProduct = catalogProducts.find((product) => product.id === mode.productId);
|
|
483
|
+
const releaseReason = !activeProduct ? "product-missing" : activeProduct.status !== "available" ? "product-unavailable" : account.value.credits < activeProduct.minCredits ? "insufficient-credits" : void 0;
|
|
484
|
+
if (releaseReason) {
|
|
485
|
+
const released = releasePlatformMode(updatedSession, releaseReason);
|
|
486
|
+
updatedSession = released.session;
|
|
487
|
+
mode = released.mode;
|
|
488
|
+
shouldWriteSession = true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (shouldWriteSession) {
|
|
492
|
+
await writePlatformSession(updatedSession, options);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
state: "logged-in",
|
|
496
|
+
account: { credits: account.value.credits },
|
|
497
|
+
mode,
|
|
498
|
+
...selectedProductId ? { selectedProductId } : {},
|
|
499
|
+
products: catalogProducts
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async function selectPlatformProduct(options) {
|
|
503
|
+
const session = await readPlatformSession(options);
|
|
504
|
+
if (!session) {
|
|
505
|
+
return { state: "logged-out" };
|
|
506
|
+
}
|
|
507
|
+
const catalog = await platformCatalog(options);
|
|
508
|
+
if (catalog.state === "invalid-token" || catalog.state === "offline") {
|
|
509
|
+
return { state: catalog.state };
|
|
510
|
+
}
|
|
511
|
+
if (catalog.state === "logged-out") {
|
|
512
|
+
return { state: "logged-out" };
|
|
513
|
+
}
|
|
514
|
+
const currentSession = await readPlatformSession(options);
|
|
515
|
+
if (!currentSession) {
|
|
516
|
+
return { state: "logged-out" };
|
|
517
|
+
}
|
|
518
|
+
const product = catalog.products.find((candidate) => candidate.id === options.productId);
|
|
519
|
+
if (!product) {
|
|
520
|
+
return { state: "product-not-found", productId: options.productId };
|
|
521
|
+
}
|
|
522
|
+
if (product.status !== "available") {
|
|
523
|
+
return { state: "product-unavailable", productId: options.productId };
|
|
524
|
+
}
|
|
525
|
+
await writePlatformSession({ ...currentSession, selectedProductId: options.productId }, options);
|
|
526
|
+
return { state: "selected", selectedProductId: options.productId };
|
|
527
|
+
}
|
|
528
|
+
async function startPlatformMode(options = {}) {
|
|
529
|
+
const session = await readPlatformSession(options);
|
|
530
|
+
if (!session) {
|
|
531
|
+
return { state: "logged-out" };
|
|
532
|
+
}
|
|
533
|
+
const catalog = await platformCatalog(options);
|
|
534
|
+
if (catalog.state === "logged-out" || catalog.state === "offline" || catalog.state === "invalid-token") {
|
|
535
|
+
return { state: catalog.state };
|
|
536
|
+
}
|
|
537
|
+
const currentSession = await readPlatformSession(options);
|
|
538
|
+
if (!currentSession) {
|
|
539
|
+
return { state: "logged-out" };
|
|
540
|
+
}
|
|
541
|
+
if (!currentSession.selectedProductId) {
|
|
542
|
+
return { state: "product-not-selected" };
|
|
543
|
+
}
|
|
544
|
+
const product = catalog.products.find((candidate) => candidate.id === currentSession.selectedProductId);
|
|
545
|
+
if (!product) {
|
|
546
|
+
return { state: "product-not-found", productId: currentSession.selectedProductId };
|
|
547
|
+
}
|
|
548
|
+
if (product.status !== "available") {
|
|
549
|
+
return { state: "product-unavailable", productId: product.id };
|
|
550
|
+
}
|
|
551
|
+
if (catalog.account.credits < product.minCredits) {
|
|
552
|
+
return {
|
|
553
|
+
state: "insufficient-credits",
|
|
554
|
+
productId: product.id,
|
|
555
|
+
requiredCredits: product.minCredits,
|
|
556
|
+
currentCredits: catalog.account.credits
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
let pool;
|
|
560
|
+
try {
|
|
561
|
+
pool = await (options.startPoolSession ?? startPoolSession)({
|
|
562
|
+
session: currentSession,
|
|
563
|
+
productId: product.id
|
|
564
|
+
});
|
|
565
|
+
} catch {
|
|
566
|
+
return { state: "offline" };
|
|
567
|
+
}
|
|
568
|
+
if (pool.state === "failed") {
|
|
569
|
+
return poolFailureToModeResult(pool.reason, product, catalog.account.credits);
|
|
570
|
+
}
|
|
571
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
572
|
+
await writePlatformSession({
|
|
573
|
+
...clearModeRelease(currentSession),
|
|
574
|
+
platformMode: "active",
|
|
575
|
+
activeProductId: product.id,
|
|
576
|
+
platformModeStartedAt: startedAt,
|
|
577
|
+
poolSession: sanitizePoolSessionSummary(pool.session),
|
|
578
|
+
routeToken: {
|
|
579
|
+
token: pool.routeToken,
|
|
580
|
+
expiresAt: pool.session.routeTokenExpiresAt
|
|
581
|
+
}
|
|
582
|
+
}, options);
|
|
583
|
+
return { state: "active", productId: product.id, startedAt };
|
|
584
|
+
}
|
|
585
|
+
async function refreshPlatformRoute(options = {}) {
|
|
586
|
+
const session = await readPlatformSession(options);
|
|
587
|
+
if (!session || session.device.status !== "active" || session.platformMode !== "active" || !session.activeProductId) {
|
|
588
|
+
return { state: "not-refreshable" };
|
|
589
|
+
}
|
|
590
|
+
let pool;
|
|
591
|
+
try {
|
|
592
|
+
pool = await (options.startPoolSession ?? startPoolSession)({
|
|
593
|
+
session,
|
|
594
|
+
productId: session.activeProductId
|
|
595
|
+
});
|
|
596
|
+
} catch {
|
|
597
|
+
return { state: "failed", reason: "offline" };
|
|
598
|
+
}
|
|
599
|
+
if (pool.state === "failed") {
|
|
600
|
+
return { state: "failed", reason: poolFailureToModeResult(pool.reason, {
|
|
601
|
+
id: session.activeProductId,
|
|
602
|
+
name: session.activeProductId,
|
|
603
|
+
description: "",
|
|
604
|
+
status: "available",
|
|
605
|
+
minCredits: 0,
|
|
606
|
+
usageLabel: ""
|
|
607
|
+
}, 0).state };
|
|
608
|
+
}
|
|
609
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
610
|
+
await writePlatformSession({
|
|
611
|
+
...clearModeRelease(session),
|
|
612
|
+
platformMode: "active",
|
|
613
|
+
activeProductId: session.activeProductId,
|
|
614
|
+
platformModeStartedAt: startedAt,
|
|
615
|
+
poolSession: sanitizePoolSessionSummary(pool.session),
|
|
616
|
+
routeToken: {
|
|
617
|
+
token: pool.routeToken,
|
|
618
|
+
expiresAt: pool.session.routeTokenExpiresAt
|
|
619
|
+
}
|
|
620
|
+
}, options);
|
|
621
|
+
return { state: "active", productId: session.activeProductId, startedAt };
|
|
622
|
+
}
|
|
623
|
+
async function stopPlatformMode(options = {}) {
|
|
624
|
+
const session = await readPlatformSession(options);
|
|
625
|
+
if (session) {
|
|
626
|
+
if (session.poolSession) {
|
|
627
|
+
try {
|
|
628
|
+
await (options.stopPoolSession ?? stopPoolSession)({
|
|
629
|
+
session,
|
|
630
|
+
poolSessionId: session.poolSession.id,
|
|
631
|
+
reason: "user-stop"
|
|
632
|
+
});
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
await writePlatformSession(clearPlatformMode(session), options);
|
|
637
|
+
}
|
|
638
|
+
return { state: "inactive" };
|
|
639
|
+
}
|
|
640
|
+
async function sendHeartbeat(options = {}) {
|
|
641
|
+
const session = await readPlatformSession(options);
|
|
642
|
+
if (!session) {
|
|
643
|
+
return { state: "logged-out" };
|
|
644
|
+
}
|
|
645
|
+
let heartbeat;
|
|
646
|
+
try {
|
|
647
|
+
heartbeat = await (options.postHeartbeat ?? postHeartbeat)({
|
|
648
|
+
session,
|
|
649
|
+
payload: options.payload ?? {}
|
|
650
|
+
});
|
|
651
|
+
} catch (error) {
|
|
652
|
+
return statusFromRequestError(error, session, {
|
|
653
|
+
...options,
|
|
654
|
+
releaseActiveModeOnUnauthorized: true
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const currentSession = await readPlatformSession(options);
|
|
658
|
+
if (!currentSession) {
|
|
659
|
+
return { state: "logged-out" };
|
|
660
|
+
}
|
|
661
|
+
const updatedSession = { ...currentSession, device: heartbeat.device };
|
|
662
|
+
await writePlatformSession(updatedSession, options);
|
|
663
|
+
const released = await releaseForInactiveDevice(updatedSession, options);
|
|
664
|
+
return {
|
|
665
|
+
state: "logged-in",
|
|
666
|
+
user: released.session.user,
|
|
667
|
+
device: released.session.device,
|
|
668
|
+
mode: released.mode
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
async function logoutPlatform(options = {}) {
|
|
672
|
+
const session = await readPlatformSession(options);
|
|
673
|
+
if (session) {
|
|
674
|
+
try {
|
|
675
|
+
await (options.postLogout ?? postLogout)({ session });
|
|
676
|
+
} catch {
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await clearPlatformSession(options);
|
|
680
|
+
return { state: "logged-out" };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// packages/service/src/runtime.ts
|
|
684
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
685
|
+
import { homedir as homedir2 } from "node:os";
|
|
686
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
687
|
+
import { randomUUID } from "node:crypto";
|
|
688
|
+
|
|
689
|
+
// packages/shared/src/runtime.ts
|
|
690
|
+
var DEFAULT_RUNTIME_FILE = "~/.cursor-pool/runtime.json";
|
|
691
|
+
var DEFAULT_DIAGNOSTICS_FILE = "~/.cursor-pool/diagnostics.jsonl";
|
|
692
|
+
|
|
693
|
+
// packages/service/src/runtime.ts
|
|
694
|
+
function createRuntimeId() {
|
|
695
|
+
return randomUUID();
|
|
696
|
+
}
|
|
697
|
+
function resolveRuntimeFile(runtimeFile = DEFAULT_RUNTIME_FILE) {
|
|
698
|
+
if (runtimeFile.startsWith("~/")) {
|
|
699
|
+
return join2(homedir2(), runtimeFile.slice(2));
|
|
700
|
+
}
|
|
701
|
+
return runtimeFile;
|
|
702
|
+
}
|
|
703
|
+
async function writeRuntimeInfo(runtime, options = {}) {
|
|
704
|
+
const runtimeFile = resolveRuntimeFile(options.runtimeFile);
|
|
705
|
+
await mkdir2(dirname2(runtimeFile), { recursive: true });
|
|
706
|
+
await writeFile2(runtimeFile, `${JSON.stringify(runtime, null, 2)}
|
|
707
|
+
`, "utf8");
|
|
708
|
+
}
|
|
709
|
+
async function readRuntimeInfo(options = {}) {
|
|
710
|
+
const runtimeFile = resolveRuntimeFile(options.runtimeFile);
|
|
711
|
+
try {
|
|
712
|
+
return JSON.parse(await readFile2(runtimeFile, "utf8"));
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (error.code === "ENOENT") {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
if (error instanceof SyntaxError) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// packages/service/src/requestCheck.ts
|
|
725
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
726
|
+
var SAFE_MODEL_PATTERN = /^[A-Za-z0-9._:-]{1,96}$/;
|
|
727
|
+
var SECRET_PATTERN = /(api[_-]?key|authorization|bearer|cursor[_-]?auth|provider[_-]?secret|secret|token|sk-[A-Za-z0-9])/i;
|
|
728
|
+
function isValidRequestId(value) {
|
|
729
|
+
return typeof value === "string" && value.length > 0 && value.length <= 128;
|
|
730
|
+
}
|
|
731
|
+
function sanitizeSource(value) {
|
|
732
|
+
return value === "cursor-agent-exec" || value === "manual-check" ? value : "manual-check";
|
|
733
|
+
}
|
|
734
|
+
function sanitizeModel(value) {
|
|
735
|
+
if (typeof value !== "string" || !SAFE_MODEL_PATTERN.test(value) || SECRET_PATTERN.test(value)) {
|
|
736
|
+
return "unknown";
|
|
737
|
+
}
|
|
738
|
+
return value;
|
|
739
|
+
}
|
|
740
|
+
function sanitizeAgentRequestCheck(input, options) {
|
|
741
|
+
const requestId = isValidRequestId(input.requestId) ? input.requestId : (options.requestId ?? randomUUID2)();
|
|
742
|
+
return {
|
|
743
|
+
requestId,
|
|
744
|
+
requestType: "agent",
|
|
745
|
+
source: sanitizeSource(input.source),
|
|
746
|
+
model: sanitizeModel(input.model),
|
|
747
|
+
receivedAt: (options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))(),
|
|
748
|
+
runtimeId: options.runtimeId,
|
|
749
|
+
decision: options.decision,
|
|
750
|
+
route: options.route ?? { state: "missing" }
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function createRequestCheckStore() {
|
|
754
|
+
let latestCheck = null;
|
|
755
|
+
return {
|
|
756
|
+
record(check) {
|
|
757
|
+
latestCheck = check;
|
|
758
|
+
return check;
|
|
759
|
+
},
|
|
760
|
+
latest() {
|
|
761
|
+
return latestCheck;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// packages/service/src/server.ts
|
|
767
|
+
import { createServer } from "node:http";
|
|
768
|
+
|
|
769
|
+
// packages/service/src/canary.ts
|
|
770
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
771
|
+
var isValidRequestId2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 128;
|
|
772
|
+
function sanitizeAgentCanary(input, options) {
|
|
773
|
+
const requestId = isValidRequestId2(input.requestId) ? input.requestId : (options.requestId ?? randomUUID3)();
|
|
774
|
+
const model = typeof input.model === "string" && input.model.length > 0 ? input.model : "unknown";
|
|
775
|
+
return {
|
|
776
|
+
requestId,
|
|
777
|
+
requestType: "agent",
|
|
778
|
+
source: "cursor-agent-exec",
|
|
779
|
+
model,
|
|
780
|
+
receivedAt: (options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))(),
|
|
781
|
+
runtimeId: options.runtimeId,
|
|
782
|
+
gate: options.gate
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function createCanaryStore() {
|
|
786
|
+
let latestCanary = null;
|
|
787
|
+
return {
|
|
788
|
+
record(canary) {
|
|
789
|
+
latestCanary = canary;
|
|
790
|
+
return canary;
|
|
791
|
+
},
|
|
792
|
+
latest() {
|
|
793
|
+
return latestCanary;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// packages/service/src/diagnostics.ts
|
|
799
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
800
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
801
|
+
import { homedir as homedir3 } from "node:os";
|
|
802
|
+
|
|
803
|
+
// packages/service/src/requestGateway.ts
|
|
804
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
805
|
+
var SAFE_MODEL_PATTERN2 = /^[A-Za-z0-9._:-]{1,96}$/;
|
|
806
|
+
var SAFE_REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
807
|
+
var SAFE_FORWARD_TOKEN_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
808
|
+
var SECRET_PATTERN2 = /(api[_-]?key|authorization|bearer|cursor[_-]?auth|provider[_-]?secret|secret|token|sk-[A-Za-z0-9])/i;
|
|
809
|
+
var MAX_RETRY_AFTER_MS = 864e5;
|
|
810
|
+
var MAX_FORWARD_CONTENT_LENGTH = 8e3;
|
|
811
|
+
var FORWARD_REJECT_REASONS = /* @__PURE__ */ new Set([
|
|
812
|
+
"route-token-invalid",
|
|
813
|
+
"pool-session-expired",
|
|
814
|
+
"product-unavailable",
|
|
815
|
+
"model-banned",
|
|
816
|
+
"provider-unavailable",
|
|
817
|
+
"rate-limited",
|
|
818
|
+
"insufficient-credits",
|
|
819
|
+
"manual-review-required"
|
|
820
|
+
]);
|
|
821
|
+
function isValidRequestId3(value) {
|
|
822
|
+
return typeof value === "string" && SAFE_REQUEST_ID_PATTERN.test(value) && !SECRET_PATTERN2.test(value);
|
|
823
|
+
}
|
|
824
|
+
function sanitizeSource2(value) {
|
|
825
|
+
return value === "cursor-agent-exec" || value === "manual-check" ? value : "unknown";
|
|
826
|
+
}
|
|
827
|
+
function sanitizeModel2(value) {
|
|
828
|
+
if (typeof value !== "string" || !SAFE_MODEL_PATTERN2.test(value) || SECRET_PATTERN2.test(value)) {
|
|
829
|
+
return "unknown";
|
|
830
|
+
}
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
function isSafeForwardToken(value) {
|
|
834
|
+
return typeof value === "string" && SAFE_FORWARD_TOKEN_PATTERN.test(value) && !SECRET_PATTERN2.test(value);
|
|
835
|
+
}
|
|
836
|
+
function isSafeIsoTimestamp(value) {
|
|
837
|
+
return isSafeForwardToken(value) && !Number.isNaN(Date.parse(value));
|
|
838
|
+
}
|
|
839
|
+
function safeForwardContent(value) {
|
|
840
|
+
if (typeof value !== "string") {
|
|
841
|
+
return void 0;
|
|
842
|
+
}
|
|
843
|
+
const content = value.trim();
|
|
844
|
+
if (!content || SECRET_PATTERN2.test(content)) {
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
847
|
+
return content.slice(0, MAX_FORWARD_CONTENT_LENGTH);
|
|
848
|
+
}
|
|
849
|
+
function evaluateGatewayDecision(gate, route) {
|
|
850
|
+
if (gate.state === "blocked") {
|
|
851
|
+
return {
|
|
852
|
+
...gate,
|
|
853
|
+
route: { state: "missing" }
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
if (route.state === "ready") {
|
|
857
|
+
return {
|
|
858
|
+
state: "accepted",
|
|
859
|
+
productId: gate.productId,
|
|
860
|
+
modeStartedAt: gate.modeStartedAt,
|
|
861
|
+
route
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
if (route.state === "expired") {
|
|
865
|
+
return {
|
|
866
|
+
state: "route-expired",
|
|
867
|
+
productId: gate.productId,
|
|
868
|
+
modeStartedAt: gate.modeStartedAt,
|
|
869
|
+
route
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
state: "route-missing",
|
|
874
|
+
productId: gate.productId,
|
|
875
|
+
modeStartedAt: gate.modeStartedAt,
|
|
876
|
+
route
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function sanitizeAgentGateway(input, options) {
|
|
880
|
+
const requestId = isValidRequestId3(input.requestId) ? input.requestId : (options.requestId ?? randomUUID4)();
|
|
881
|
+
return {
|
|
882
|
+
requestId,
|
|
883
|
+
requestType: "agent",
|
|
884
|
+
source: sanitizeSource2(input.source),
|
|
885
|
+
model: sanitizeModel2(input.model),
|
|
886
|
+
receivedAt: (options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))(),
|
|
887
|
+
runtimeId: options.runtimeId,
|
|
888
|
+
decision: options.decision,
|
|
889
|
+
forward: { state: "unknown" }
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function sanitizeGatewayForward(input) {
|
|
893
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
894
|
+
return { state: "unknown" };
|
|
895
|
+
}
|
|
896
|
+
const forward = input;
|
|
897
|
+
if (forward.state === "skipped") {
|
|
898
|
+
return forward.reason === "not-accepted" ? { state: "skipped", reason: "not-accepted" } : { state: "unknown" };
|
|
899
|
+
}
|
|
900
|
+
if (forward.state === "forwarded") {
|
|
901
|
+
if (!isSafeForwardToken(forward.upstreamRequestId) || !isSafeIsoTimestamp(forward.acceptedAt)) {
|
|
902
|
+
return { state: "unknown" };
|
|
903
|
+
}
|
|
904
|
+
const content = safeForwardContent(forward.content);
|
|
905
|
+
return {
|
|
906
|
+
state: "forwarded",
|
|
907
|
+
upstreamRequestId: forward.upstreamRequestId,
|
|
908
|
+
acceptedAt: forward.acceptedAt,
|
|
909
|
+
...content ? { content } : {}
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (forward.state === "rejected") {
|
|
913
|
+
if (typeof forward.reason !== "string" || !FORWARD_REJECT_REASONS.has(forward.reason)) {
|
|
914
|
+
return { state: "unknown" };
|
|
915
|
+
}
|
|
916
|
+
const retryAfterMs = forward.retryAfterMs;
|
|
917
|
+
return {
|
|
918
|
+
state: "rejected",
|
|
919
|
+
reason: forward.reason,
|
|
920
|
+
...Number.isInteger(retryAfterMs) && retryAfterMs >= 0 && retryAfterMs <= MAX_RETRY_AFTER_MS ? { retryAfterMs } : {}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
if (forward.state === "not-configured" || forward.state === "timeout" || forward.state === "network-error") {
|
|
924
|
+
return { state: forward.state };
|
|
925
|
+
}
|
|
926
|
+
return { state: "unknown" };
|
|
927
|
+
}
|
|
928
|
+
async function resolveGatewayForward(options) {
|
|
929
|
+
const { safe } = await resolveGatewayForwardResult(options);
|
|
930
|
+
return safe;
|
|
931
|
+
}
|
|
932
|
+
async function resolveGatewayForwardResult(options) {
|
|
933
|
+
if (options.gateway.decision.state !== "accepted") {
|
|
934
|
+
return {
|
|
935
|
+
result: { state: "not-configured" },
|
|
936
|
+
safe: { state: "skipped", reason: "not-accepted" }
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
if (!options.forwarder) {
|
|
940
|
+
return {
|
|
941
|
+
result: { state: "not-configured" },
|
|
942
|
+
safe: { state: "not-configured" }
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
const result = await options.forwarder({
|
|
947
|
+
gateway: options.gateway,
|
|
948
|
+
routeToken: options.routeToken,
|
|
949
|
+
poolSessionId: options.poolSessionId,
|
|
950
|
+
productId: options.productId,
|
|
951
|
+
model: options.model,
|
|
952
|
+
requestId: options.requestId,
|
|
953
|
+
settlementMode: options.settlementMode,
|
|
954
|
+
...options.openAiRequest ? { openAiRequest: options.openAiRequest } : {}
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
result,
|
|
958
|
+
safe: sanitizeGatewayForward(result)
|
|
959
|
+
};
|
|
960
|
+
} catch {
|
|
961
|
+
return {
|
|
962
|
+
result: { state: "network-error" },
|
|
963
|
+
safe: { state: "network-error" }
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function cleanApiBaseUrl2(apiBaseUrl) {
|
|
968
|
+
return apiBaseUrl.replace(/\/+$/, "");
|
|
969
|
+
}
|
|
970
|
+
function createGatewayHttpForwarder(options) {
|
|
971
|
+
return async (request) => {
|
|
972
|
+
if (!options.apiBaseUrl || !options.deviceToken) {
|
|
973
|
+
return { state: "not-configured" };
|
|
974
|
+
}
|
|
975
|
+
const response = await fetch(`${cleanApiBaseUrl2(options.apiBaseUrl)}/api/client/gateway/agent`, {
|
|
976
|
+
method: "POST",
|
|
977
|
+
headers: {
|
|
978
|
+
"content-type": "application/json",
|
|
979
|
+
authorization: `Bearer ${options.deviceToken}`
|
|
980
|
+
},
|
|
981
|
+
body: JSON.stringify({
|
|
982
|
+
gateway: request.gateway,
|
|
983
|
+
routeToken: request.routeToken,
|
|
984
|
+
...request.poolSessionId ? { poolSessionId: request.poolSessionId } : {},
|
|
985
|
+
productId: request.productId,
|
|
986
|
+
model: request.model,
|
|
987
|
+
requestId: request.requestId,
|
|
988
|
+
settlementMode: request.settlementMode ?? "provider_forwarded",
|
|
989
|
+
...request.openAiRequest ? { openAiRequest: request.openAiRequest } : {}
|
|
990
|
+
})
|
|
991
|
+
});
|
|
992
|
+
if (!response.ok) {
|
|
993
|
+
return { state: "network-error" };
|
|
994
|
+
}
|
|
995
|
+
return await response.json();
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
async function completeGatewayForward(options) {
|
|
999
|
+
if (!options.apiBaseUrl || !options.deviceToken) {
|
|
1000
|
+
return { state: "not-configured" };
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
const response = await fetch(
|
|
1004
|
+
`${cleanApiBaseUrl2(options.apiBaseUrl)}/api/client/gateway/agent/${encodeURIComponent(options.requestId)}/complete`,
|
|
1005
|
+
{
|
|
1006
|
+
method: "POST",
|
|
1007
|
+
headers: {
|
|
1008
|
+
"content-type": "application/json",
|
|
1009
|
+
authorization: `Bearer ${options.deviceToken}`
|
|
1010
|
+
},
|
|
1011
|
+
body: JSON.stringify({
|
|
1012
|
+
routeToken: options.routeToken,
|
|
1013
|
+
poolSessionId: options.poolSessionId,
|
|
1014
|
+
productId: options.productId
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
);
|
|
1018
|
+
if (!response.ok) {
|
|
1019
|
+
return { state: "network-error" };
|
|
1020
|
+
}
|
|
1021
|
+
return await response.json();
|
|
1022
|
+
} catch {
|
|
1023
|
+
return { state: "network-error" };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function createGatewayStore() {
|
|
1027
|
+
let latestGateway = null;
|
|
1028
|
+
return {
|
|
1029
|
+
record(gateway) {
|
|
1030
|
+
latestGateway = gateway;
|
|
1031
|
+
return gateway;
|
|
1032
|
+
},
|
|
1033
|
+
latest() {
|
|
1034
|
+
return latestGateway;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// packages/service/src/diagnostics.ts
|
|
1040
|
+
var DEFAULT_MAX_DIAGNOSTICS = 20;
|
|
1041
|
+
var BLOCKED_GATE_REASONS = /* @__PURE__ */ new Set([
|
|
1042
|
+
"logged-out",
|
|
1043
|
+
"mode-inactive",
|
|
1044
|
+
"mode-released",
|
|
1045
|
+
"device-inactive",
|
|
1046
|
+
"invalid-session"
|
|
1047
|
+
]);
|
|
1048
|
+
var RELEASE_REASONS = /* @__PURE__ */ new Set([
|
|
1049
|
+
"product-missing",
|
|
1050
|
+
"product-unavailable",
|
|
1051
|
+
"insufficient-credits",
|
|
1052
|
+
"invalid-token",
|
|
1053
|
+
"device-inactive"
|
|
1054
|
+
]);
|
|
1055
|
+
function resolveDiagnosticsFile(diagnosticsFile = DEFAULT_DIAGNOSTICS_FILE) {
|
|
1056
|
+
if (diagnosticsFile.startsWith("~/")) {
|
|
1057
|
+
return join3(homedir3(), diagnosticsFile.slice(2));
|
|
1058
|
+
}
|
|
1059
|
+
return diagnosticsFile;
|
|
1060
|
+
}
|
|
1061
|
+
function sanitizeCanaryForDiagnostics(input) {
|
|
1062
|
+
return {
|
|
1063
|
+
requestId: input.requestId,
|
|
1064
|
+
requestType: "agent",
|
|
1065
|
+
source: "cursor-agent-exec",
|
|
1066
|
+
model: input.model,
|
|
1067
|
+
receivedAt: input.receivedAt,
|
|
1068
|
+
runtimeId: input.runtimeId,
|
|
1069
|
+
gate: sanitizeGateForDiagnostics(input.gate)
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function sanitizeRequestCheckForDiagnostics(input) {
|
|
1073
|
+
return {
|
|
1074
|
+
kind: "agent-request-check",
|
|
1075
|
+
requestId: input.requestId,
|
|
1076
|
+
requestType: "agent",
|
|
1077
|
+
source: input.source === "cursor-agent-exec" ? "cursor-agent-exec" : "manual-check",
|
|
1078
|
+
model: input.model,
|
|
1079
|
+
receivedAt: input.receivedAt,
|
|
1080
|
+
runtimeId: input.runtimeId,
|
|
1081
|
+
decision: sanitizeGateForDiagnostics(input.decision),
|
|
1082
|
+
route: sanitizeRouteForDiagnostics(input.route)
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function sanitizeGatewayForDiagnostics(input) {
|
|
1086
|
+
return {
|
|
1087
|
+
kind: "agent-request-gateway",
|
|
1088
|
+
requestId: input.requestId,
|
|
1089
|
+
requestType: "agent",
|
|
1090
|
+
source: input.source,
|
|
1091
|
+
model: input.model,
|
|
1092
|
+
receivedAt: input.receivedAt,
|
|
1093
|
+
runtimeId: input.runtimeId,
|
|
1094
|
+
decision: sanitizeGatewayDecisionForDiagnostics(input.decision),
|
|
1095
|
+
forward: sanitizeGatewayForward(input.forward)
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function sanitizeGatewayDecisionForDiagnostics(decision) {
|
|
1099
|
+
return sanitizeHistoricalGatewayDecision(decision);
|
|
1100
|
+
}
|
|
1101
|
+
function sanitizeDiagnosticEntry(input) {
|
|
1102
|
+
if (!isRecord(input)) {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
if (input.kind === "agent-request-gateway") {
|
|
1106
|
+
const decision = sanitizeHistoricalGatewayDecision(input.decision);
|
|
1107
|
+
const forward = sanitizeGatewayForward(input.forward);
|
|
1108
|
+
if (typeof input.requestId !== "string" || input.requestType !== "agent" || input.source !== "cursor-agent-exec" && input.source !== "manual-check" && input.source !== "unknown" || typeof input.model !== "string" || typeof input.receivedAt !== "string" || typeof input.runtimeId !== "string") {
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
kind: "agent-request-gateway",
|
|
1113
|
+
requestId: input.requestId,
|
|
1114
|
+
requestType: "agent",
|
|
1115
|
+
source: input.source,
|
|
1116
|
+
model: input.model,
|
|
1117
|
+
receivedAt: input.receivedAt,
|
|
1118
|
+
runtimeId: input.runtimeId,
|
|
1119
|
+
decision,
|
|
1120
|
+
forward
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
if (input.kind === "agent-request-check") {
|
|
1124
|
+
const decision = sanitizeHistoricalGate(input.decision);
|
|
1125
|
+
const route = sanitizeHistoricalRoute(input.route);
|
|
1126
|
+
if (typeof input.requestId !== "string" || input.requestType !== "agent" || input.source !== "cursor-agent-exec" && input.source !== "manual-check" || typeof input.model !== "string" || typeof input.receivedAt !== "string" || typeof input.runtimeId !== "string" || decision === null) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
kind: "agent-request-check",
|
|
1131
|
+
requestId: input.requestId,
|
|
1132
|
+
requestType: "agent",
|
|
1133
|
+
source: input.source,
|
|
1134
|
+
model: input.model,
|
|
1135
|
+
receivedAt: input.receivedAt,
|
|
1136
|
+
runtimeId: input.runtimeId,
|
|
1137
|
+
decision,
|
|
1138
|
+
route
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
const gate = sanitizeHistoricalGate(input.gate);
|
|
1142
|
+
if (typeof input.requestId !== "string" || input.requestType !== "agent" || input.source !== "cursor-agent-exec" || typeof input.model !== "string" || typeof input.receivedAt !== "string" || typeof input.runtimeId !== "string" || gate === null) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
requestId: input.requestId,
|
|
1147
|
+
requestType: "agent",
|
|
1148
|
+
source: "cursor-agent-exec",
|
|
1149
|
+
model: input.model,
|
|
1150
|
+
receivedAt: input.receivedAt,
|
|
1151
|
+
runtimeId: input.runtimeId,
|
|
1152
|
+
gate
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function sanitizeGateForDiagnostics(gate) {
|
|
1156
|
+
if (gate.state === "allowed") {
|
|
1157
|
+
return {
|
|
1158
|
+
state: "allowed",
|
|
1159
|
+
productId: gate.productId,
|
|
1160
|
+
modeStartedAt: gate.modeStartedAt
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
state: "blocked",
|
|
1165
|
+
reason: gate.reason,
|
|
1166
|
+
...gate.releaseReason !== void 0 ? { releaseReason: gate.releaseReason } : {},
|
|
1167
|
+
...gate.releasedAt !== void 0 ? { releasedAt: gate.releasedAt } : {}
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function sanitizeRouteForDiagnostics(route) {
|
|
1171
|
+
if (route.state === "ready" || route.state === "expired") {
|
|
1172
|
+
return { state: route.state, expiresAt: route.expiresAt };
|
|
1173
|
+
}
|
|
1174
|
+
return { state: "missing" };
|
|
1175
|
+
}
|
|
1176
|
+
function sanitizeHistoricalRoute(input) {
|
|
1177
|
+
if (!isRecord(input)) {
|
|
1178
|
+
return { state: "missing" };
|
|
1179
|
+
}
|
|
1180
|
+
if ((input.state === "ready" || input.state === "expired") && typeof input.expiresAt === "string") {
|
|
1181
|
+
return { state: input.state, expiresAt: input.expiresAt };
|
|
1182
|
+
}
|
|
1183
|
+
return { state: "missing" };
|
|
1184
|
+
}
|
|
1185
|
+
function sanitizeHistoricalGate(input) {
|
|
1186
|
+
if (!isRecord(input)) {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
if (input.state === "allowed") {
|
|
1190
|
+
if (typeof input.productId !== "string" || typeof input.modeStartedAt !== "string") {
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
state: "allowed",
|
|
1195
|
+
productId: input.productId,
|
|
1196
|
+
modeStartedAt: input.modeStartedAt
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
if (input.state === "blocked") {
|
|
1200
|
+
if (typeof input.reason !== "string" || !BLOCKED_GATE_REASONS.has(input.reason)) {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
state: "blocked",
|
|
1205
|
+
reason: input.reason,
|
|
1206
|
+
...typeof input.releaseReason === "string" && RELEASE_REASONS.has(input.releaseReason) ? { releaseReason: input.releaseReason } : {},
|
|
1207
|
+
...typeof input.releasedAt === "string" ? { releasedAt: input.releasedAt } : {}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
function sanitizeHistoricalGatewayDecision(input) {
|
|
1213
|
+
if (!isRecord(input)) {
|
|
1214
|
+
return { state: "unknown" };
|
|
1215
|
+
}
|
|
1216
|
+
if (input.state === "accepted") {
|
|
1217
|
+
const route = sanitizeHistoricalRoute(input.route);
|
|
1218
|
+
if (typeof input.productId === "string" && typeof input.modeStartedAt === "string" && route.state === "ready") {
|
|
1219
|
+
return { state: "accepted", productId: input.productId, modeStartedAt: input.modeStartedAt, route };
|
|
1220
|
+
}
|
|
1221
|
+
return { state: "unknown" };
|
|
1222
|
+
}
|
|
1223
|
+
if (input.state === "route-missing") {
|
|
1224
|
+
if (typeof input.productId === "string" && typeof input.modeStartedAt === "string") {
|
|
1225
|
+
return {
|
|
1226
|
+
state: "route-missing",
|
|
1227
|
+
productId: input.productId,
|
|
1228
|
+
modeStartedAt: input.modeStartedAt,
|
|
1229
|
+
route: { state: "missing" }
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
return { state: "unknown" };
|
|
1233
|
+
}
|
|
1234
|
+
if (input.state === "route-expired") {
|
|
1235
|
+
const route = sanitizeHistoricalRoute(input.route);
|
|
1236
|
+
if (typeof input.productId === "string" && typeof input.modeStartedAt === "string" && route.state === "expired") {
|
|
1237
|
+
return { state: "route-expired", productId: input.productId, modeStartedAt: input.modeStartedAt, route };
|
|
1238
|
+
}
|
|
1239
|
+
return { state: "unknown" };
|
|
1240
|
+
}
|
|
1241
|
+
if (input.state === "blocked") {
|
|
1242
|
+
const gate = sanitizeHistoricalGate(input);
|
|
1243
|
+
if (gate?.state === "blocked") {
|
|
1244
|
+
return { ...gate, route: { state: "missing" } };
|
|
1245
|
+
}
|
|
1246
|
+
return { state: "unknown" };
|
|
1247
|
+
}
|
|
1248
|
+
return { state: "unknown" };
|
|
1249
|
+
}
|
|
1250
|
+
function isRecord(input) {
|
|
1251
|
+
return typeof input === "object" && input !== null && !Array.isArray(input);
|
|
1252
|
+
}
|
|
1253
|
+
async function readDiagnosticEntries(diagnosticsFile) {
|
|
1254
|
+
try {
|
|
1255
|
+
const content = await readFile3(diagnosticsFile, "utf8");
|
|
1256
|
+
return content.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
1257
|
+
try {
|
|
1258
|
+
const entry = sanitizeDiagnosticEntry(JSON.parse(line));
|
|
1259
|
+
return entry === null ? [] : [entry];
|
|
1260
|
+
} catch {
|
|
1261
|
+
return [];
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
if (error.code === "ENOENT") {
|
|
1266
|
+
return [];
|
|
1267
|
+
}
|
|
1268
|
+
throw error;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async function appendAgentCanaryDiagnostic(canary, options = {}) {
|
|
1272
|
+
const diagnosticsFile = resolveDiagnosticsFile(options.diagnosticsFile);
|
|
1273
|
+
const maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_DIAGNOSTICS);
|
|
1274
|
+
const entries = await readDiagnosticEntries(diagnosticsFile);
|
|
1275
|
+
const nextEntries = [...entries, sanitizeCanaryForDiagnostics(canary)].slice(-maxEntries);
|
|
1276
|
+
const content = `${nextEntries.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
1277
|
+
`;
|
|
1278
|
+
await mkdir3(dirname3(diagnosticsFile), { recursive: true });
|
|
1279
|
+
await writeFile3(diagnosticsFile, content, "utf8");
|
|
1280
|
+
}
|
|
1281
|
+
async function appendAgentRequestCheckDiagnostic(check, options = {}) {
|
|
1282
|
+
const diagnosticsFile = resolveDiagnosticsFile(options.diagnosticsFile);
|
|
1283
|
+
const maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_DIAGNOSTICS);
|
|
1284
|
+
const entries = await readDiagnosticEntries(diagnosticsFile);
|
|
1285
|
+
const nextEntries = [...entries, sanitizeRequestCheckForDiagnostics(check)].slice(-maxEntries);
|
|
1286
|
+
const content = `${nextEntries.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
1287
|
+
`;
|
|
1288
|
+
await mkdir3(dirname3(diagnosticsFile), { recursive: true });
|
|
1289
|
+
await writeFile3(diagnosticsFile, content, "utf8");
|
|
1290
|
+
}
|
|
1291
|
+
async function appendAgentGatewayDiagnostic(gateway, options = {}) {
|
|
1292
|
+
const diagnosticsFile = resolveDiagnosticsFile(options.diagnosticsFile);
|
|
1293
|
+
const maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_DIAGNOSTICS);
|
|
1294
|
+
const entries = await readDiagnosticEntries(diagnosticsFile);
|
|
1295
|
+
const nextEntries = [...entries, sanitizeGatewayForDiagnostics(gateway)].slice(-maxEntries);
|
|
1296
|
+
const content = `${nextEntries.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
1297
|
+
`;
|
|
1298
|
+
await mkdir3(dirname3(diagnosticsFile), { recursive: true });
|
|
1299
|
+
await writeFile3(diagnosticsFile, content, "utf8");
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// packages/service/src/requestGate.ts
|
|
1303
|
+
var INVALID_SESSION_GATE = { state: "blocked", reason: "invalid-session" };
|
|
1304
|
+
function invalidSessionGate() {
|
|
1305
|
+
return { ...INVALID_SESSION_GATE };
|
|
1306
|
+
}
|
|
1307
|
+
function hasNonEmptyString(value) {
|
|
1308
|
+
return typeof value === "string" && value.length > 0;
|
|
1309
|
+
}
|
|
1310
|
+
async function evaluateRequestGate(options = {}) {
|
|
1311
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
1312
|
+
if (snapshot.state === "missing") {
|
|
1313
|
+
return { state: "blocked", reason: "logged-out" };
|
|
1314
|
+
}
|
|
1315
|
+
if (snapshot.state === "invalid") {
|
|
1316
|
+
return invalidSessionGate();
|
|
1317
|
+
}
|
|
1318
|
+
const { session } = snapshot;
|
|
1319
|
+
if (session.device.status !== "active") {
|
|
1320
|
+
return { state: "blocked", reason: "device-inactive" };
|
|
1321
|
+
}
|
|
1322
|
+
if (session.platformMode !== "active" && session.lastModeReleaseReason && session.lastModeReleasedAt) {
|
|
1323
|
+
return {
|
|
1324
|
+
state: "blocked",
|
|
1325
|
+
reason: "mode-released",
|
|
1326
|
+
releaseReason: session.lastModeReleaseReason,
|
|
1327
|
+
releasedAt: session.lastModeReleasedAt
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
if (session.platformMode !== "active" || !hasNonEmptyString(session.activeProductId) || !hasNonEmptyString(session.platformModeStartedAt)) {
|
|
1331
|
+
return { state: "blocked", reason: "mode-inactive" };
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
state: "allowed",
|
|
1335
|
+
productId: session.activeProductId,
|
|
1336
|
+
modeStartedAt: session.platformModeStartedAt
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
async function evaluateRouteState(options = {}) {
|
|
1340
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
1341
|
+
if (snapshot.state !== "valid") {
|
|
1342
|
+
return { state: "missing" };
|
|
1343
|
+
}
|
|
1344
|
+
const { routeToken } = snapshot.session;
|
|
1345
|
+
if (!routeToken) {
|
|
1346
|
+
return { state: "missing" };
|
|
1347
|
+
}
|
|
1348
|
+
const expiresAt = Date.parse(routeToken.expiresAt);
|
|
1349
|
+
const now = Date.parse((options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))());
|
|
1350
|
+
if (Number.isNaN(expiresAt) || Number.isNaN(now)) {
|
|
1351
|
+
return { state: "missing" };
|
|
1352
|
+
}
|
|
1353
|
+
if (expiresAt <= now) {
|
|
1354
|
+
return { state: "expired", expiresAt: routeToken.expiresAt };
|
|
1355
|
+
}
|
|
1356
|
+
return { state: "ready", expiresAt: routeToken.expiresAt };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// packages/service/src/takeover.ts
|
|
1360
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
1361
|
+
var SAFE_TOKEN_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1362
|
+
var SAFE_MODEL_PATTERN3 = /^[A-Za-z0-9._:-]{1,96}$/;
|
|
1363
|
+
var SECRET_PATTERN3 = /(api[_-]?key|authorization|bearer|cookie|cursor[_-]?auth|provider[_-]?secret|secret|token|prompt|messages|sk-[A-Za-z0-9])/i;
|
|
1364
|
+
function safeToken(value, fallback) {
|
|
1365
|
+
return typeof value === "string" && SAFE_TOKEN_PATTERN.test(value) && !SECRET_PATTERN3.test(value) ? value : fallback;
|
|
1366
|
+
}
|
|
1367
|
+
function safeSource(value) {
|
|
1368
|
+
return value === "cursor-always-local" || value === "cursor-agent-exec" ? value : "unknown";
|
|
1369
|
+
}
|
|
1370
|
+
function safeModel(value) {
|
|
1371
|
+
return typeof value === "string" && SAFE_MODEL_PATTERN3.test(value) && !SECRET_PATTERN3.test(value) ? value : "unknown";
|
|
1372
|
+
}
|
|
1373
|
+
function buildAgentTakeoverResponse(input, gate, route, forward) {
|
|
1374
|
+
const requestId = safeToken(input.requestId, randomUUID5());
|
|
1375
|
+
if (gate.state === "blocked") {
|
|
1376
|
+
return {
|
|
1377
|
+
state: "rejected",
|
|
1378
|
+
requestId,
|
|
1379
|
+
reason: gate.reason
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
if (route.state === "missing") {
|
|
1383
|
+
return { state: "rejected", requestId, reason: "route-missing" };
|
|
1384
|
+
}
|
|
1385
|
+
if (route.state === "expired") {
|
|
1386
|
+
return { state: "rejected", requestId, reason: "route-expired" };
|
|
1387
|
+
}
|
|
1388
|
+
const content = forward?.state === "forwarded" ? forward.content : void 0;
|
|
1389
|
+
return {
|
|
1390
|
+
state: "answered",
|
|
1391
|
+
requestId,
|
|
1392
|
+
source: safeSource(input.source),
|
|
1393
|
+
model: safeModel(input.model),
|
|
1394
|
+
content: content ?? "Cursor Pool provider \u6682\u65F6\u6CA1\u6709\u8FD4\u56DE\u53EF\u663E\u793A\u5185\u5BB9"
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function createAgentTakeoverStore() {
|
|
1398
|
+
let latestTakeover = null;
|
|
1399
|
+
return {
|
|
1400
|
+
record(takeover) {
|
|
1401
|
+
latestTakeover = takeover;
|
|
1402
|
+
return takeover;
|
|
1403
|
+
},
|
|
1404
|
+
latest() {
|
|
1405
|
+
return latestTakeover;
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// packages/shared/src/clientConfig.ts
|
|
1411
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
1412
|
+
import { homedir as homedir4 } from "node:os";
|
|
1413
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
1414
|
+
var DEFAULT_CLIENT_CONFIG_FILE = "~/.cursor-pool/client-config.json";
|
|
1415
|
+
function resolveClientConfigFile(configFile = DEFAULT_CLIENT_CONFIG_FILE) {
|
|
1416
|
+
if (configFile.startsWith("~/")) {
|
|
1417
|
+
return join4(homedir4(), configFile.slice(2));
|
|
1418
|
+
}
|
|
1419
|
+
return configFile;
|
|
1420
|
+
}
|
|
1421
|
+
function normalizeConfig(value) {
|
|
1422
|
+
const record = value;
|
|
1423
|
+
return {
|
|
1424
|
+
...typeof record?.apiBaseUrl === "string" && record.apiBaseUrl.trim() !== "" ? { apiBaseUrl: record.apiBaseUrl.trim().replace(/\/+$/, "") } : {}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
async function readClientConfig(options = {}) {
|
|
1428
|
+
try {
|
|
1429
|
+
return normalizeConfig(JSON.parse(await readFile4(resolveClientConfigFile(options.configFile), "utf8")));
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError) {
|
|
1432
|
+
return {};
|
|
1433
|
+
}
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// packages/service/src/server.ts
|
|
1439
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
1440
|
+
var DEFAULT_PORT = 56393;
|
|
1441
|
+
var localServices = /* @__PURE__ */ new Map();
|
|
1442
|
+
async function resolvePlatformApiBaseUrl(options) {
|
|
1443
|
+
if (options.platformApiBaseUrl) {
|
|
1444
|
+
return options.platformApiBaseUrl;
|
|
1445
|
+
}
|
|
1446
|
+
if (process.env.CURSOR_POOL_API_BASE_URL) {
|
|
1447
|
+
return process.env.CURSOR_POOL_API_BASE_URL;
|
|
1448
|
+
}
|
|
1449
|
+
const config = await readClientConfig({ configFile: options.clientConfigFile });
|
|
1450
|
+
return config.apiBaseUrl;
|
|
1451
|
+
}
|
|
1452
|
+
function runtimeKey(runtime) {
|
|
1453
|
+
return `${runtime.host}:${runtime.port}:${runtime.runtimeId}`;
|
|
1454
|
+
}
|
|
1455
|
+
async function readJsonRequest(request) {
|
|
1456
|
+
const chunks = [];
|
|
1457
|
+
for await (const chunk of request) {
|
|
1458
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1459
|
+
}
|
|
1460
|
+
if (chunks.length === 0) {
|
|
1461
|
+
return {};
|
|
1462
|
+
}
|
|
1463
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1464
|
+
if (!body) {
|
|
1465
|
+
return {};
|
|
1466
|
+
}
|
|
1467
|
+
return JSON.parse(body);
|
|
1468
|
+
}
|
|
1469
|
+
function writeJson(response, statusCode, payload) {
|
|
1470
|
+
const body = JSON.stringify(payload);
|
|
1471
|
+
response.writeHead(statusCode, {
|
|
1472
|
+
"content-type": "application/json",
|
|
1473
|
+
"content-length": Buffer.byteLength(body),
|
|
1474
|
+
"access-control-allow-origin": "*",
|
|
1475
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
1476
|
+
"access-control-allow-headers": "content-type"
|
|
1477
|
+
});
|
|
1478
|
+
response.end(body);
|
|
1479
|
+
}
|
|
1480
|
+
function writeCorsPreflight(response) {
|
|
1481
|
+
response.writeHead(204, {
|
|
1482
|
+
"access-control-allow-origin": "*",
|
|
1483
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
1484
|
+
"access-control-allow-headers": "content-type",
|
|
1485
|
+
"access-control-max-age": "600"
|
|
1486
|
+
});
|
|
1487
|
+
response.end();
|
|
1488
|
+
}
|
|
1489
|
+
function writeEventStream(response, events) {
|
|
1490
|
+
const body = `${events.map((event) => `data: ${JSON.stringify(event)}
|
|
1491
|
+
|
|
1492
|
+
`).join("")}data: [DONE]
|
|
1493
|
+
|
|
1494
|
+
`;
|
|
1495
|
+
response.writeHead(200, {
|
|
1496
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
1497
|
+
"cache-control": "no-cache",
|
|
1498
|
+
"connection": "keep-alive",
|
|
1499
|
+
"content-length": Buffer.byteLength(body),
|
|
1500
|
+
"access-control-allow-origin": "*",
|
|
1501
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
1502
|
+
"access-control-allow-headers": "content-type,authorization"
|
|
1503
|
+
});
|
|
1504
|
+
response.end(body);
|
|
1505
|
+
}
|
|
1506
|
+
function asRecord(value) {
|
|
1507
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1508
|
+
}
|
|
1509
|
+
function safeOpenAiModel(value) {
|
|
1510
|
+
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,96}$/.test(value) ? value : "unknown";
|
|
1511
|
+
}
|
|
1512
|
+
function openAiModelsResponse() {
|
|
1513
|
+
return {
|
|
1514
|
+
object: "list",
|
|
1515
|
+
data: [
|
|
1516
|
+
{
|
|
1517
|
+
id: "gpt-test",
|
|
1518
|
+
object: "model",
|
|
1519
|
+
created: 0,
|
|
1520
|
+
owned_by: "cursor-pool",
|
|
1521
|
+
api_types: ["chat_completions"],
|
|
1522
|
+
capabilities: {
|
|
1523
|
+
supports_tool_use: true,
|
|
1524
|
+
supports_streaming: true,
|
|
1525
|
+
output_modalities: ["text"],
|
|
1526
|
+
context_length: 128e3,
|
|
1527
|
+
max_output_tokens: 8192,
|
|
1528
|
+
supports_reasoning: false,
|
|
1529
|
+
supports_vision: false
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
]
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
async function resolveOpenAiGatewayForward(body, runtime, options, retryAfterRefresh = true) {
|
|
1536
|
+
let gate;
|
|
1537
|
+
try {
|
|
1538
|
+
gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
|
|
1539
|
+
} catch {
|
|
1540
|
+
gate = { ...INVALID_SESSION_GATE };
|
|
1541
|
+
}
|
|
1542
|
+
const route = gate.state === "allowed" ? await evaluateRouteState({ sessionFile: options.platformSessionFile }) : { state: "missing" };
|
|
1543
|
+
const decision = evaluateGatewayDecision(gate, route);
|
|
1544
|
+
const requestId = typeof body.requestId === "string" ? body.requestId : `openai-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1545
|
+
const model = safeOpenAiModel(body.model);
|
|
1546
|
+
const gateway = sanitizeAgentGateway({
|
|
1547
|
+
requestId,
|
|
1548
|
+
source: "cursor-agent-exec",
|
|
1549
|
+
model
|
|
1550
|
+
}, {
|
|
1551
|
+
runtimeId: runtime.runtimeId,
|
|
1552
|
+
decision
|
|
1553
|
+
});
|
|
1554
|
+
const forwardContext = decision.state === "accepted" ? await readPlatformGatewayForwardContext({ sessionFile: options.platformSessionFile }) : {};
|
|
1555
|
+
const forwarder = options.gatewayForwarder ?? (decision.state === "accepted" ? createGatewayHttpForwarder({
|
|
1556
|
+
apiBaseUrl: forwardContext.apiBaseUrl,
|
|
1557
|
+
deviceToken: forwardContext.deviceToken
|
|
1558
|
+
}) : void 0);
|
|
1559
|
+
const { result, safe } = await resolveGatewayForwardResult({
|
|
1560
|
+
gateway,
|
|
1561
|
+
routeToken: forwardContext.routeToken ?? "",
|
|
1562
|
+
poolSessionId: forwardContext.poolSessionId,
|
|
1563
|
+
productId: decision.state === "accepted" ? decision.productId : "unknown",
|
|
1564
|
+
model,
|
|
1565
|
+
requestId: gateway.requestId,
|
|
1566
|
+
settlementMode: "client_response",
|
|
1567
|
+
openAiRequest: body,
|
|
1568
|
+
forwarder
|
|
1569
|
+
});
|
|
1570
|
+
if (retryAfterRefresh && (decision.state === "route-expired" || safe.state === "skipped" && safe.reason === "not-accepted" || safe.state === "rejected" && safe.reason === "pool-session-expired")) {
|
|
1571
|
+
const refreshed = await refreshPlatformRoute({
|
|
1572
|
+
sessionFile: options.platformSessionFile,
|
|
1573
|
+
startPoolSession: options.platformStartPoolSession
|
|
1574
|
+
});
|
|
1575
|
+
if (refreshed.state === "active") {
|
|
1576
|
+
return resolveOpenAiGatewayForward(body, runtime, options, false);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
gateway: {
|
|
1581
|
+
...gateway,
|
|
1582
|
+
forward: safe
|
|
1583
|
+
},
|
|
1584
|
+
forward: safe,
|
|
1585
|
+
forwardResult: result,
|
|
1586
|
+
completionContext: {
|
|
1587
|
+
apiBaseUrl: forwardContext.apiBaseUrl,
|
|
1588
|
+
deviceToken: forwardContext.deviceToken,
|
|
1589
|
+
routeToken: forwardContext.routeToken,
|
|
1590
|
+
poolSessionId: forwardContext.poolSessionId,
|
|
1591
|
+
productId: decision.state === "accepted" ? decision.productId : void 0,
|
|
1592
|
+
requestId: gateway.requestId
|
|
1593
|
+
},
|
|
1594
|
+
model
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function openAiChatCompletionResponse(model, content) {
|
|
1598
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1599
|
+
return {
|
|
1600
|
+
id: `chatcmpl-${now}`,
|
|
1601
|
+
object: "chat.completion",
|
|
1602
|
+
created: now,
|
|
1603
|
+
model,
|
|
1604
|
+
choices: [
|
|
1605
|
+
{
|
|
1606
|
+
index: 0,
|
|
1607
|
+
message: {
|
|
1608
|
+
role: "assistant",
|
|
1609
|
+
content
|
|
1610
|
+
},
|
|
1611
|
+
finish_reason: "stop"
|
|
1612
|
+
}
|
|
1613
|
+
],
|
|
1614
|
+
usage: {
|
|
1615
|
+
prompt_tokens: 0,
|
|
1616
|
+
completion_tokens: 0,
|
|
1617
|
+
total_tokens: 0
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function openAiChatCompletionStreamEvents(model, content) {
|
|
1622
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1623
|
+
const id = `chatcmpl-${now}`;
|
|
1624
|
+
return [
|
|
1625
|
+
{
|
|
1626
|
+
id,
|
|
1627
|
+
object: "chat.completion.chunk",
|
|
1628
|
+
created: now,
|
|
1629
|
+
model,
|
|
1630
|
+
choices: [
|
|
1631
|
+
{
|
|
1632
|
+
index: 0,
|
|
1633
|
+
delta: {
|
|
1634
|
+
role: "assistant"
|
|
1635
|
+
},
|
|
1636
|
+
finish_reason: null
|
|
1637
|
+
}
|
|
1638
|
+
]
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
id,
|
|
1642
|
+
object: "chat.completion.chunk",
|
|
1643
|
+
created: now,
|
|
1644
|
+
model,
|
|
1645
|
+
choices: [
|
|
1646
|
+
{
|
|
1647
|
+
index: 0,
|
|
1648
|
+
delta: {
|
|
1649
|
+
content
|
|
1650
|
+
},
|
|
1651
|
+
finish_reason: null
|
|
1652
|
+
}
|
|
1653
|
+
]
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
id,
|
|
1657
|
+
object: "chat.completion.chunk",
|
|
1658
|
+
created: now,
|
|
1659
|
+
model,
|
|
1660
|
+
choices: [
|
|
1661
|
+
{
|
|
1662
|
+
index: 0,
|
|
1663
|
+
delta: {},
|
|
1664
|
+
finish_reason: "stop"
|
|
1665
|
+
}
|
|
1666
|
+
]
|
|
1667
|
+
}
|
|
1668
|
+
];
|
|
1669
|
+
}
|
|
1670
|
+
function openAiBypassResponse() {
|
|
1671
|
+
return {
|
|
1672
|
+
error: {
|
|
1673
|
+
message: "Cursor Pool platform mode is inactive; use the official Cursor path.",
|
|
1674
|
+
type: "cursor_pool_bypass",
|
|
1675
|
+
code: "cursor_pool_bypass"
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function openAiFailClosedContent(forward) {
|
|
1680
|
+
if (forward.state === "rejected") {
|
|
1681
|
+
return `Cursor Pool \u53F7\u6C60 provider \u672A\u5C31\u7EEA\uFF1A${forward.reason}`;
|
|
1682
|
+
}
|
|
1683
|
+
if (forward.state === "not-configured") {
|
|
1684
|
+
return "Cursor Pool \u53F7\u6C60 provider \u672A\u914D\u7F6E\uFF0C\u5F53\u524D\u8BF7\u6C42\u5DF2\u88AB\u5E73\u53F0\u63A5\u7BA1\u4F46\u65E0\u6CD5\u8F6C\u53D1\u3002";
|
|
1685
|
+
}
|
|
1686
|
+
if (forward.state === "timeout") {
|
|
1687
|
+
return "Cursor Pool \u53F7\u6C60 provider \u54CD\u5E94\u8D85\u65F6\uFF0C\u5F53\u524D\u8BF7\u6C42\u5DF2\u88AB\u5E73\u53F0\u63A5\u7BA1\u4F46\u6CA1\u6709\u5B8C\u6210\u3002";
|
|
1688
|
+
}
|
|
1689
|
+
if (forward.state === "network-error") {
|
|
1690
|
+
return "Cursor Pool \u53F7\u6C60 provider \u7F51\u7EDC\u4E0D\u53EF\u7528\uFF0C\u5F53\u524D\u8BF7\u6C42\u5DF2\u88AB\u5E73\u53F0\u63A5\u7BA1\u4F46\u65E0\u6CD5\u8F6C\u53D1\u3002";
|
|
1691
|
+
}
|
|
1692
|
+
if (forward.state === "skipped") {
|
|
1693
|
+
return "Cursor Pool \u5E73\u53F0\u6A21\u5F0F\u672A\u5141\u8BB8\u672C\u6B21\u8BF7\u6C42\u3002";
|
|
1694
|
+
}
|
|
1695
|
+
return "Cursor Pool provider \u6682\u65F6\u6CA1\u6709\u8FD4\u56DE\u53EF\u663E\u793A\u5185\u5BB9";
|
|
1696
|
+
}
|
|
1697
|
+
function isOpenAiObject(value) {
|
|
1698
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1699
|
+
}
|
|
1700
|
+
function forwardedOpenAiResponse(result) {
|
|
1701
|
+
return result.state === "forwarded" && isOpenAiObject(result.openAiResponse) ? result.openAiResponse : void 0;
|
|
1702
|
+
}
|
|
1703
|
+
function forwardedOpenAiStreamEvents(result) {
|
|
1704
|
+
if (result.state !== "forwarded" || !Array.isArray(result.openAiStreamEvents)) {
|
|
1705
|
+
return void 0;
|
|
1706
|
+
}
|
|
1707
|
+
const events = result.openAiStreamEvents.filter(isOpenAiObject);
|
|
1708
|
+
return events.length > 0 ? events : void 0;
|
|
1709
|
+
}
|
|
1710
|
+
async function completeForwardAfterClientResponse(context) {
|
|
1711
|
+
if (!context.routeToken || !context.poolSessionId || !context.productId) {
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
await completeGatewayForward({
|
|
1715
|
+
apiBaseUrl: context.apiBaseUrl,
|
|
1716
|
+
deviceToken: context.deviceToken,
|
|
1717
|
+
routeToken: context.routeToken,
|
|
1718
|
+
poolSessionId: context.poolSessionId,
|
|
1719
|
+
productId: context.productId,
|
|
1720
|
+
requestId: context.requestId
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
async function routeRequest(request, response, runtime, options, canaryStore, requestCheckStore, gatewayStore, takeoverStore, stop) {
|
|
1724
|
+
try {
|
|
1725
|
+
if (request.method === "OPTIONS") {
|
|
1726
|
+
writeCorsPreflight(response);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
if (request.method === "GET" && request.url === "/health") {
|
|
1730
|
+
writeJson(response, 200, buildHealth(runtime));
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (request.method === "GET" && request.url === "/models") {
|
|
1734
|
+
writeJson(response, 200, openAiModelsResponse());
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if (request.method === "POST" && request.url === "/chat/completions") {
|
|
1738
|
+
let clientClosed = false;
|
|
1739
|
+
let responseFinished = false;
|
|
1740
|
+
response.once("finish", () => {
|
|
1741
|
+
responseFinished = true;
|
|
1742
|
+
});
|
|
1743
|
+
response.once("close", () => {
|
|
1744
|
+
if (!responseFinished) {
|
|
1745
|
+
clientClosed = true;
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
const clientConnected = () => !clientClosed && !response.destroyed && !response.writableEnded;
|
|
1749
|
+
const body = await readJsonRequest(request);
|
|
1750
|
+
const { gateway, forward, forwardResult, completionContext, model } = await resolveOpenAiGatewayForward(body, runtime, options);
|
|
1751
|
+
const recordedGateway = gatewayStore.record(gateway);
|
|
1752
|
+
try {
|
|
1753
|
+
await appendAgentGatewayDiagnostic(recordedGateway, {
|
|
1754
|
+
diagnosticsFile: options.diagnosticsFile,
|
|
1755
|
+
maxEntries: options.maxDiagnostics
|
|
1756
|
+
});
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
if (!clientConnected()) {
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
if (gateway.decision.state !== "accepted") {
|
|
1763
|
+
writeJson(response, 409, openAiBypassResponse());
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (forward.state !== "forwarded") {
|
|
1767
|
+
const content = openAiFailClosedContent(forward);
|
|
1768
|
+
if (body.stream === true) {
|
|
1769
|
+
writeEventStream(response, openAiChatCompletionStreamEvents(model, content));
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
writeJson(response, 200, openAiChatCompletionResponse(model, content));
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (body.stream === true) {
|
|
1776
|
+
const providerEvents = forwardedOpenAiStreamEvents(forwardResult);
|
|
1777
|
+
if (providerEvents) {
|
|
1778
|
+
writeEventStream(response, providerEvents);
|
|
1779
|
+
await completeForwardAfterClientResponse(completionContext);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
writeEventStream(
|
|
1783
|
+
response,
|
|
1784
|
+
openAiChatCompletionStreamEvents(
|
|
1785
|
+
model,
|
|
1786
|
+
forward.content ?? "Cursor Pool provider \u6682\u65F6\u6CA1\u6709\u8FD4\u56DE\u53EF\u663E\u793A\u5185\u5BB9"
|
|
1787
|
+
)
|
|
1788
|
+
);
|
|
1789
|
+
await completeForwardAfterClientResponse(completionContext);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const providerResponse = forwardedOpenAiResponse(forwardResult);
|
|
1793
|
+
if (providerResponse) {
|
|
1794
|
+
writeJson(response, 200, providerResponse);
|
|
1795
|
+
await completeForwardAfterClientResponse(completionContext);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
writeJson(
|
|
1799
|
+
response,
|
|
1800
|
+
200,
|
|
1801
|
+
openAiChatCompletionResponse(
|
|
1802
|
+
model,
|
|
1803
|
+
forward.content ?? "Cursor Pool provider \u6682\u65F6\u6CA1\u6709\u8FD4\u56DE\u53EF\u663E\u793A\u5185\u5BB9"
|
|
1804
|
+
)
|
|
1805
|
+
);
|
|
1806
|
+
await completeForwardAfterClientResponse(completionContext);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (request.method === "POST" && request.url === "/cursor-metadata") {
|
|
1810
|
+
const metadata = sanitizeServiceMetadata(await readJsonRequest(request));
|
|
1811
|
+
await options.onMetadata?.(metadata);
|
|
1812
|
+
writeJson(response, 200, { ok: true, metadata });
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
if (request.method === "POST" && request.url === "/agent/canary") {
|
|
1816
|
+
let gate;
|
|
1817
|
+
try {
|
|
1818
|
+
gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
|
|
1819
|
+
} catch {
|
|
1820
|
+
gate = { ...INVALID_SESSION_GATE };
|
|
1821
|
+
}
|
|
1822
|
+
const canary = canaryStore.record(
|
|
1823
|
+
sanitizeAgentCanary(await readJsonRequest(request), { runtimeId: runtime.runtimeId, gate })
|
|
1824
|
+
);
|
|
1825
|
+
try {
|
|
1826
|
+
await appendAgentCanaryDiagnostic(canary, {
|
|
1827
|
+
diagnosticsFile: options.diagnosticsFile,
|
|
1828
|
+
maxEntries: options.maxDiagnostics
|
|
1829
|
+
});
|
|
1830
|
+
} catch {
|
|
1831
|
+
}
|
|
1832
|
+
writeJson(response, 200, { ok: true, canary });
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (request.method === "GET" && request.url === "/agent/canary/latest") {
|
|
1836
|
+
writeJson(response, 200, { ok: true, canary: canaryStore.latest() });
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
if (request.method === "POST" && request.url === "/agent/request-check") {
|
|
1840
|
+
let decision;
|
|
1841
|
+
try {
|
|
1842
|
+
decision = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
|
|
1843
|
+
} catch {
|
|
1844
|
+
decision = { ...INVALID_SESSION_GATE };
|
|
1845
|
+
}
|
|
1846
|
+
const route = decision.state === "allowed" ? await evaluateRouteState({ sessionFile: options.platformSessionFile }) : { state: "missing" };
|
|
1847
|
+
const check = requestCheckStore.record(
|
|
1848
|
+
sanitizeAgentRequestCheck(await readJsonRequest(request), {
|
|
1849
|
+
runtimeId: runtime.runtimeId,
|
|
1850
|
+
decision,
|
|
1851
|
+
route
|
|
1852
|
+
})
|
|
1853
|
+
);
|
|
1854
|
+
try {
|
|
1855
|
+
await appendAgentRequestCheckDiagnostic(check, {
|
|
1856
|
+
diagnosticsFile: options.diagnosticsFile,
|
|
1857
|
+
maxEntries: options.maxDiagnostics
|
|
1858
|
+
});
|
|
1859
|
+
} catch {
|
|
1860
|
+
}
|
|
1861
|
+
writeJson(response, 200, { ok: decision.state === "allowed", check });
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (request.method === "GET" && request.url === "/agent/request-check/latest") {
|
|
1865
|
+
writeJson(response, 200, { ok: true, check: requestCheckStore.latest() });
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
if (request.method === "POST" && request.url === "/agent/request-gateway") {
|
|
1869
|
+
let gate;
|
|
1870
|
+
try {
|
|
1871
|
+
gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
|
|
1872
|
+
} catch {
|
|
1873
|
+
gate = { ...INVALID_SESSION_GATE };
|
|
1874
|
+
}
|
|
1875
|
+
const route = gate.state === "allowed" ? await evaluateRouteState({ sessionFile: options.platformSessionFile }) : { state: "missing" };
|
|
1876
|
+
const decision = evaluateGatewayDecision(gate, route);
|
|
1877
|
+
const body = await readJsonRequest(request);
|
|
1878
|
+
const gateway = sanitizeAgentGateway(body, {
|
|
1879
|
+
runtimeId: runtime.runtimeId,
|
|
1880
|
+
decision
|
|
1881
|
+
});
|
|
1882
|
+
const forwardContext = decision.state === "accepted" ? await readPlatformGatewayForwardContext({ sessionFile: options.platformSessionFile }) : {};
|
|
1883
|
+
const forwarder = options.gatewayForwarder ?? (decision.state === "accepted" ? createGatewayHttpForwarder({
|
|
1884
|
+
apiBaseUrl: forwardContext.apiBaseUrl,
|
|
1885
|
+
deviceToken: forwardContext.deviceToken
|
|
1886
|
+
}) : void 0);
|
|
1887
|
+
gateway.forward = await resolveGatewayForward({
|
|
1888
|
+
gateway,
|
|
1889
|
+
routeToken: forwardContext.routeToken ?? "",
|
|
1890
|
+
poolSessionId: forwardContext.poolSessionId,
|
|
1891
|
+
productId: decision.state === "accepted" ? decision.productId : "unknown",
|
|
1892
|
+
model: gateway.model,
|
|
1893
|
+
requestId: gateway.requestId,
|
|
1894
|
+
forwarder
|
|
1895
|
+
});
|
|
1896
|
+
const recordedGateway = gatewayStore.record(gateway);
|
|
1897
|
+
try {
|
|
1898
|
+
await appendAgentGatewayDiagnostic(recordedGateway, {
|
|
1899
|
+
diagnosticsFile: options.diagnosticsFile,
|
|
1900
|
+
maxEntries: options.maxDiagnostics
|
|
1901
|
+
});
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
writeJson(response, 200, {
|
|
1905
|
+
ok: decision.state === "accepted" && recordedGateway.forward.state === "forwarded",
|
|
1906
|
+
gateway: recordedGateway
|
|
1907
|
+
});
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (request.method === "GET" && request.url === "/agent/request-gateway/latest") {
|
|
1911
|
+
writeJson(response, 200, { ok: true, gateway: gatewayStore.latest() });
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (request.method === "POST" && request.url === "/agent/takeover") {
|
|
1915
|
+
const body = await readJsonRequest(request);
|
|
1916
|
+
let gate;
|
|
1917
|
+
try {
|
|
1918
|
+
gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
|
|
1919
|
+
} catch {
|
|
1920
|
+
gate = { ...INVALID_SESSION_GATE };
|
|
1921
|
+
}
|
|
1922
|
+
const route = gate.state === "allowed" ? await evaluateRouteState({ sessionFile: options.platformSessionFile }) : { state: "missing" };
|
|
1923
|
+
let forward;
|
|
1924
|
+
if (route.state === "ready") {
|
|
1925
|
+
const decision = evaluateGatewayDecision(gate, route);
|
|
1926
|
+
const gateway = sanitizeAgentGateway(body, {
|
|
1927
|
+
runtimeId: runtime.runtimeId,
|
|
1928
|
+
decision
|
|
1929
|
+
});
|
|
1930
|
+
const forwardContext = await readPlatformGatewayForwardContext({ sessionFile: options.platformSessionFile });
|
|
1931
|
+
const forwarder = options.gatewayForwarder ?? createGatewayHttpForwarder({
|
|
1932
|
+
apiBaseUrl: forwardContext.apiBaseUrl,
|
|
1933
|
+
deviceToken: forwardContext.deviceToken
|
|
1934
|
+
});
|
|
1935
|
+
forward = await resolveGatewayForward({
|
|
1936
|
+
gateway,
|
|
1937
|
+
routeToken: forwardContext.routeToken ?? "",
|
|
1938
|
+
poolSessionId: forwardContext.poolSessionId,
|
|
1939
|
+
productId: decision.state === "accepted" ? decision.productId : "unknown",
|
|
1940
|
+
model: gateway.model,
|
|
1941
|
+
requestId: gateway.requestId,
|
|
1942
|
+
forwarder
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
const takeover = takeoverStore.record(
|
|
1946
|
+
buildAgentTakeoverResponse(body, gate, route, forward)
|
|
1947
|
+
);
|
|
1948
|
+
writeJson(response, 200, takeover);
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (request.method === "GET" && request.url === "/agent/takeover/latest") {
|
|
1952
|
+
writeJson(response, 200, { ok: true, takeover: takeoverStore.latest() });
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
if (request.method === "POST" && request.url === "/extension/status") {
|
|
1956
|
+
const status = sanitizeExtensionStatus(await readJsonRequest(request));
|
|
1957
|
+
writeJson(response, 200, { ok: true, status });
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
if (request.method === "GET" && request.url === "/platform/status") {
|
|
1961
|
+
writeJson(response, 200, await platformStatus({ sessionFile: options.platformSessionFile }));
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (request.method === "GET" && request.url === "/platform/catalog") {
|
|
1965
|
+
writeJson(response, 200, await platformCatalog({ sessionFile: options.platformSessionFile }));
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
if (request.method === "POST" && request.url === "/platform/selection") {
|
|
1969
|
+
const body = await readJsonRequest(request);
|
|
1970
|
+
if (typeof body.productId !== "string" || body.productId.trim() === "") {
|
|
1971
|
+
throw new Error("invalid platform selection request");
|
|
1972
|
+
}
|
|
1973
|
+
const selection = await selectPlatformProduct({
|
|
1974
|
+
sessionFile: options.platformSessionFile,
|
|
1975
|
+
productId: body.productId
|
|
1976
|
+
});
|
|
1977
|
+
writeJson(response, 200, selection);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (request.method === "POST" && request.url === "/platform/login") {
|
|
1981
|
+
const body = await readJsonRequest(request);
|
|
1982
|
+
const apiBaseUrl = await resolvePlatformApiBaseUrl(options);
|
|
1983
|
+
if (typeof body.email === "string" || typeof body.password === "string") {
|
|
1984
|
+
if (typeof body.email !== "string" || typeof body.password !== "string" || typeof apiBaseUrl !== "string" || apiBaseUrl.trim() === "") {
|
|
1985
|
+
throw new Error("invalid platform login request");
|
|
1986
|
+
}
|
|
1987
|
+
let login;
|
|
1988
|
+
try {
|
|
1989
|
+
login = await loginWithPassword({
|
|
1990
|
+
email: body.email,
|
|
1991
|
+
password: body.password,
|
|
1992
|
+
apiBaseUrl,
|
|
1993
|
+
sessionFile: options.platformSessionFile,
|
|
1994
|
+
device: asRecord(body.device),
|
|
1995
|
+
exchangePasswordDeviceToken: options.platformExchangePasswordDeviceToken
|
|
1996
|
+
});
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
const platformCode = error.platformCode;
|
|
1999
|
+
writeJson(response, 400, {
|
|
2000
|
+
ok: false,
|
|
2001
|
+
error: "platform login failed",
|
|
2002
|
+
...typeof platformCode === "string" ? { code: platformCode } : {}
|
|
2003
|
+
});
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
writeJson(response, 200, login);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (typeof body.code !== "string" || typeof body.apiBaseUrl !== "string") {
|
|
2010
|
+
throw new Error("invalid platform login request");
|
|
2011
|
+
}
|
|
2012
|
+
writeJson(response, 200, await loginWithCode({
|
|
2013
|
+
code: body.code,
|
|
2014
|
+
apiBaseUrl: body.apiBaseUrl,
|
|
2015
|
+
sessionFile: options.platformSessionFile,
|
|
2016
|
+
device: asRecord(body.device),
|
|
2017
|
+
exchangeDeviceToken: options.platformExchangeDeviceToken
|
|
2018
|
+
}));
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (request.method === "POST" && request.url === "/platform/heartbeat") {
|
|
2022
|
+
writeJson(response, 200, await sendHeartbeat({
|
|
2023
|
+
sessionFile: options.platformSessionFile,
|
|
2024
|
+
payload: await readJsonRequest(request),
|
|
2025
|
+
postHeartbeat: options.platformPostHeartbeat
|
|
2026
|
+
}));
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
if (request.method === "POST" && request.url === "/platform/logout") {
|
|
2030
|
+
writeJson(response, 200, await logoutPlatform({
|
|
2031
|
+
sessionFile: options.platformSessionFile,
|
|
2032
|
+
postLogout: options.platformPostLogout
|
|
2033
|
+
}));
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (request.method === "POST" && request.url === "/mode/start") {
|
|
2037
|
+
writeJson(response, 200, await startPlatformMode({
|
|
2038
|
+
sessionFile: options.platformSessionFile,
|
|
2039
|
+
startPoolSession: options.platformStartPoolSession
|
|
2040
|
+
}));
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (request.method === "POST" && request.url === "/mode/stop") {
|
|
2044
|
+
writeJson(response, 200, await stopPlatformMode({
|
|
2045
|
+
sessionFile: options.platformSessionFile,
|
|
2046
|
+
stopPoolSession: options.platformStopPoolSession
|
|
2047
|
+
}));
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (request.method === "POST" && request.url === "/shutdown") {
|
|
2051
|
+
writeJson(response, 200, { ok: true });
|
|
2052
|
+
response.once("finish", () => {
|
|
2053
|
+
void stop();
|
|
2054
|
+
});
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
writeJson(response, 404, { ok: false, error: "not found" });
|
|
2058
|
+
} catch {
|
|
2059
|
+
writeJson(response, 400, { ok: false, error: "invalid request" });
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
function listen(server, port) {
|
|
2063
|
+
return new Promise((resolve, reject) => {
|
|
2064
|
+
const onError = (error) => {
|
|
2065
|
+
server.off("listening", onListening);
|
|
2066
|
+
reject(error);
|
|
2067
|
+
};
|
|
2068
|
+
const onListening = () => {
|
|
2069
|
+
server.off("error", onError);
|
|
2070
|
+
resolve();
|
|
2071
|
+
};
|
|
2072
|
+
server.once("error", onError);
|
|
2073
|
+
server.once("listening", onListening);
|
|
2074
|
+
server.listen(port, LOOPBACK_HOST);
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
async function closeServer(server) {
|
|
2078
|
+
if (!server.listening) {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
await new Promise((resolve, reject) => {
|
|
2082
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
async function startServer(options = {}) {
|
|
2086
|
+
let requestedPort = options.port ?? DEFAULT_PORT;
|
|
2087
|
+
for (; ; ) {
|
|
2088
|
+
const runtime = {
|
|
2089
|
+
host: LOOPBACK_HOST,
|
|
2090
|
+
port: requestedPort,
|
|
2091
|
+
runtimeId: createRuntimeId()
|
|
2092
|
+
};
|
|
2093
|
+
const canaryStore = createCanaryStore();
|
|
2094
|
+
const requestCheckStore = createRequestCheckStore();
|
|
2095
|
+
const gatewayStore = createGatewayStore();
|
|
2096
|
+
const takeoverStore = createAgentTakeoverStore();
|
|
2097
|
+
const server = createServer((request, response) => {
|
|
2098
|
+
void routeRequest(
|
|
2099
|
+
request,
|
|
2100
|
+
response,
|
|
2101
|
+
runtime,
|
|
2102
|
+
options,
|
|
2103
|
+
canaryStore,
|
|
2104
|
+
requestCheckStore,
|
|
2105
|
+
gatewayStore,
|
|
2106
|
+
takeoverStore,
|
|
2107
|
+
stopService
|
|
2108
|
+
);
|
|
2109
|
+
});
|
|
2110
|
+
const stopService = async () => {
|
|
2111
|
+
await closeServer(server);
|
|
2112
|
+
localServices.delete(runtimeKey(runtime));
|
|
2113
|
+
};
|
|
2114
|
+
try {
|
|
2115
|
+
await listen(server, requestedPort);
|
|
2116
|
+
const address = server.address();
|
|
2117
|
+
if (typeof address !== "object" || address === null) {
|
|
2118
|
+
await closeServer(server);
|
|
2119
|
+
throw new Error("service did not bind a TCP address");
|
|
2120
|
+
}
|
|
2121
|
+
runtime.port = address.port;
|
|
2122
|
+
await writeRuntimeInfo(runtime, { runtimeFile: options.runtimeFile });
|
|
2123
|
+
const service = {
|
|
2124
|
+
...runtime,
|
|
2125
|
+
server,
|
|
2126
|
+
stop: stopService
|
|
2127
|
+
};
|
|
2128
|
+
localServices.set(runtimeKey(runtime), service);
|
|
2129
|
+
return service;
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
await closeServer(server);
|
|
2132
|
+
if (error.code !== "EADDRINUSE") {
|
|
2133
|
+
throw error;
|
|
2134
|
+
}
|
|
2135
|
+
requestedPort = 0;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// packages/extension/src/runtime.ts
|
|
2141
|
+
async function readRuntimeInfo2(options = {}) {
|
|
2142
|
+
return readRuntimeInfo({ runtimeFile: options.runtimeFile });
|
|
2143
|
+
}
|
|
2144
|
+
async function writeRuntimeInfo2(runtime, options = {}) {
|
|
2145
|
+
await writeRuntimeInfo(runtime, { runtimeFile: options.runtimeFile });
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// packages/extension/src/api.ts
|
|
2149
|
+
var DEFAULT_SERVICE_PORT = 56393;
|
|
2150
|
+
var localExtensionServices = /* @__PURE__ */ new Map();
|
|
2151
|
+
function buildExtensionDeviceInfo() {
|
|
2152
|
+
return {
|
|
2153
|
+
name: hostname(),
|
|
2154
|
+
os: osPlatform(),
|
|
2155
|
+
arch: arch(),
|
|
2156
|
+
extensionVersion: "0.5.6"
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
function serviceUrl(runtime, path) {
|
|
2160
|
+
return `http://${runtime.host}:${runtime.port}${path}`;
|
|
2161
|
+
}
|
|
2162
|
+
async function probeRuntime(runtime) {
|
|
2163
|
+
try {
|
|
2164
|
+
const response = await fetch(serviceUrl(runtime, "/health"));
|
|
2165
|
+
const health = await response.json();
|
|
2166
|
+
if (!response.ok || health.ok !== true || typeof health.runtimeId !== "string") {
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
if (runtime.runtimeId && health.runtimeId !== runtime.runtimeId) {
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
return { ...runtime, runtimeId: health.runtimeId };
|
|
2173
|
+
} catch {
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
async function resolveHealthyRuntime(options = {}) {
|
|
2178
|
+
const runtime = await readRuntimeInfo2({ runtimeFile: options.runtimeFile });
|
|
2179
|
+
const allowFallback = options.runtimeFile === void 0 || options.fallbackPort !== void 0;
|
|
2180
|
+
if (runtime) {
|
|
2181
|
+
const healthyRuntime = await probeRuntime(runtime);
|
|
2182
|
+
if (healthyRuntime) {
|
|
2183
|
+
return healthyRuntime;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if (!allowFallback) {
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
const fallbackRuntime = {
|
|
2190
|
+
host: "127.0.0.1",
|
|
2191
|
+
port: options.fallbackPort ?? DEFAULT_SERVICE_PORT,
|
|
2192
|
+
runtimeId: ""
|
|
2193
|
+
};
|
|
2194
|
+
const healthyFallback = await probeRuntime(fallbackRuntime);
|
|
2195
|
+
if (!healthyFallback) {
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
await writeRuntimeInfo2(healthyFallback, { runtimeFile: options.runtimeFile });
|
|
2199
|
+
return healthyFallback;
|
|
2200
|
+
}
|
|
2201
|
+
async function ensureLocalService(runtimeFile, options = {}) {
|
|
2202
|
+
const healthy = await resolveHealthyRuntime({ runtimeFile });
|
|
2203
|
+
if (healthy) {
|
|
2204
|
+
return healthy;
|
|
2205
|
+
}
|
|
2206
|
+
const key = runtimeFile ?? "~/.cursor-pool/runtime.json";
|
|
2207
|
+
const existing = localExtensionServices.get(key);
|
|
2208
|
+
if (existing) {
|
|
2209
|
+
return { host: existing.host, port: existing.port, runtimeId: existing.runtimeId };
|
|
2210
|
+
}
|
|
2211
|
+
const clientConfig = await readClientConfig();
|
|
2212
|
+
const service = await startServer({
|
|
2213
|
+
runtimeFile,
|
|
2214
|
+
platformApiBaseUrl: options.platformApiBaseUrl ?? process.env.CURSOR_POOL_API_BASE_URL ?? clientConfig.apiBaseUrl
|
|
2215
|
+
});
|
|
2216
|
+
localExtensionServices.set(key, service);
|
|
2217
|
+
return { host: service.host, port: service.port, runtimeId: service.runtimeId };
|
|
2218
|
+
}
|
|
2219
|
+
async function resolveRuntimeForRequest(options = {}) {
|
|
2220
|
+
const runtime = await readRuntimeInfo2({ runtimeFile: options.runtimeFile });
|
|
2221
|
+
const allowFallback = options.runtimeFile === void 0 || options.fallbackPort !== void 0;
|
|
2222
|
+
if (runtime && !allowFallback) {
|
|
2223
|
+
return runtime;
|
|
2224
|
+
}
|
|
2225
|
+
return resolveHealthyRuntime(options);
|
|
2226
|
+
}
|
|
2227
|
+
function isUserSummary2(value) {
|
|
2228
|
+
const user = value;
|
|
2229
|
+
return typeof user?.id === "string" && typeof user.email === "string";
|
|
2230
|
+
}
|
|
2231
|
+
function isDeviceSummary2(value) {
|
|
2232
|
+
const device = value;
|
|
2233
|
+
return typeof device?.id === "string" && typeof device.status === "string" && typeof device.lastHeartbeatAt === "string";
|
|
2234
|
+
}
|
|
2235
|
+
function isProductSummary2(value) {
|
|
2236
|
+
const product = value;
|
|
2237
|
+
return typeof product?.id === "string" && typeof product.name === "string" && typeof product.description === "string" && (product.status === "available" || product.status === "unavailable") && Number.isInteger(product.minCredits) && product.minCredits >= 0 && typeof product.usageLabel === "string";
|
|
2238
|
+
}
|
|
2239
|
+
function isPlatformModeReleaseReason2(value) {
|
|
2240
|
+
return value === "product-missing" || value === "product-unavailable" || value === "insufficient-credits" || value === "invalid-token" || value === "device-inactive";
|
|
2241
|
+
}
|
|
2242
|
+
function normalizePlatformMode(value) {
|
|
2243
|
+
const mode = value;
|
|
2244
|
+
if (mode?.state === "inactive") {
|
|
2245
|
+
return {
|
|
2246
|
+
state: "inactive",
|
|
2247
|
+
...isPlatformModeReleaseReason2(mode.releaseReason) && typeof mode.releasedAt === "string" ? { releaseReason: mode.releaseReason, releasedAt: mode.releasedAt } : {}
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
if (mode?.state === "active" && typeof mode.productId === "string" && typeof mode.startedAt === "string") {
|
|
2251
|
+
return { state: "active", productId: mode.productId, startedAt: mode.startedAt };
|
|
2252
|
+
}
|
|
2253
|
+
return null;
|
|
2254
|
+
}
|
|
2255
|
+
function normalizePlatformModeResult(value) {
|
|
2256
|
+
const mode = normalizePlatformMode(value);
|
|
2257
|
+
if (mode) {
|
|
2258
|
+
return mode;
|
|
2259
|
+
}
|
|
2260
|
+
const result = value;
|
|
2261
|
+
if (result?.state === "logged-out" || result?.state === "offline" || result?.state === "invalid-token" || result?.state === "product-not-selected") {
|
|
2262
|
+
return { state: result.state };
|
|
2263
|
+
}
|
|
2264
|
+
if ((result?.state === "product-not-found" || result?.state === "product-unavailable") && typeof result.productId === "string") {
|
|
2265
|
+
return { state: result.state, productId: result.productId };
|
|
2266
|
+
}
|
|
2267
|
+
if (result?.state === "insufficient-credits" && typeof result.productId === "string" && Number.isFinite(result.requiredCredits) && Number.isFinite(result.currentCredits)) {
|
|
2268
|
+
return {
|
|
2269
|
+
state: "insufficient-credits",
|
|
2270
|
+
productId: result.productId,
|
|
2271
|
+
requiredCredits: result.requiredCredits,
|
|
2272
|
+
currentCredits: result.currentCredits
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
return { state: "offline" };
|
|
2276
|
+
}
|
|
2277
|
+
function isPlatformStatus(value) {
|
|
2278
|
+
const status = value;
|
|
2279
|
+
if (status?.state === "logged-out") {
|
|
2280
|
+
return true;
|
|
2281
|
+
}
|
|
2282
|
+
if (status?.state === "logged-in") {
|
|
2283
|
+
return isUserSummary2(status.user) && isDeviceSummary2(status.device) && (status.mode === void 0 || normalizePlatformMode(status.mode) !== null);
|
|
2284
|
+
}
|
|
2285
|
+
if (status?.state === "offline" || status?.state === "invalid-token") {
|
|
2286
|
+
return (status.user === void 0 || isUserSummary2(status.user)) && (status.device === void 0 || isDeviceSummary2(status.device)) && (status.mode === void 0 || normalizePlatformMode(status.mode) !== null);
|
|
2287
|
+
}
|
|
2288
|
+
return false;
|
|
2289
|
+
}
|
|
2290
|
+
function normalizePlatformCatalog(value) {
|
|
2291
|
+
const catalog = value;
|
|
2292
|
+
if (catalog?.state === "logged-out") {
|
|
2293
|
+
return { state: "logged-out", products: [] };
|
|
2294
|
+
}
|
|
2295
|
+
if (catalog?.state === "offline") {
|
|
2296
|
+
const mode = normalizePlatformMode(catalog.mode);
|
|
2297
|
+
return { state: "offline", ...mode ? { mode } : {}, products: [] };
|
|
2298
|
+
}
|
|
2299
|
+
if (catalog?.state === "invalid-token") {
|
|
2300
|
+
const mode = normalizePlatformMode(catalog.mode);
|
|
2301
|
+
return { state: "invalid-token", ...mode ? { mode } : {}, products: [] };
|
|
2302
|
+
}
|
|
2303
|
+
if (catalog?.state === "logged-in" && typeof catalog.account?.credits === "number" && Number.isFinite(catalog.account.credits) && Array.isArray(catalog.products)) {
|
|
2304
|
+
return {
|
|
2305
|
+
state: "logged-in",
|
|
2306
|
+
account: { credits: catalog.account.credits },
|
|
2307
|
+
...normalizePlatformMode(catalog.mode) ? { mode: normalizePlatformMode(catalog.mode) ?? void 0 } : {},
|
|
2308
|
+
...typeof catalog.selectedProductId === "string" ? { selectedProductId: catalog.selectedProductId } : {},
|
|
2309
|
+
products: catalog.products.filter(isProductSummary2)
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
return { state: "offline", products: [] };
|
|
2313
|
+
}
|
|
2314
|
+
function normalizePlatformSelectionResult(value) {
|
|
2315
|
+
const result = value;
|
|
2316
|
+
if (result?.state === "selected" && typeof result.selectedProductId === "string") {
|
|
2317
|
+
return { state: "selected", selectedProductId: result.selectedProductId };
|
|
2318
|
+
}
|
|
2319
|
+
if (result?.state === "logged-out" || result?.state === "offline" || result?.state === "invalid-token") {
|
|
2320
|
+
return { state: result.state };
|
|
2321
|
+
}
|
|
2322
|
+
if ((result?.state === "product-not-found" || result?.state === "product-unavailable") && typeof result.productId === "string") {
|
|
2323
|
+
return { state: result.state, productId: result.productId };
|
|
2324
|
+
}
|
|
2325
|
+
return { state: "offline" };
|
|
2326
|
+
}
|
|
2327
|
+
function platformLoginErrorMessage(code) {
|
|
2328
|
+
if (code === "PASSWORD_LOGIN_INVALID") {
|
|
2329
|
+
return "\u767B\u5F55\u5931\u8D25\uFF1A\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E";
|
|
2330
|
+
}
|
|
2331
|
+
if (code === "PASSWORD_LOGIN_NOT_CONFIGURED") {
|
|
2332
|
+
return "\u767B\u5F55\u5931\u8D25\uFF1A\u8FD9\u4E2A\u8D26\u53F7\u8FD8\u6CA1\u6709\u8BBE\u7F6E\u7F51\u9875\u767B\u5F55\u5BC6\u7801\uFF0C\u8BF7\u5148\u5728\u7528\u6237\u7AEF\u5B8C\u6210\u6CE8\u518C\u6216\u91CD\u7F6E\u5BC6\u7801";
|
|
2333
|
+
}
|
|
2334
|
+
if (code === "DEVICE_LIMIT_REACHED") {
|
|
2335
|
+
return "\u767B\u5F55\u5931\u8D25\uFF1A\u8BBE\u5907\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u5148\u5728\u7528\u6237\u7AEF\u79FB\u9664\u65E7\u8BBE\u5907";
|
|
2336
|
+
}
|
|
2337
|
+
return "\u767B\u5F55\u5931\u8D25\uFF1A\u672C\u5730\u670D\u52A1\u6CA1\u6709\u8FDE\u4E0A\u5E73\u53F0 API\uFF0C\u6216\u8D26\u53F7\u5BC6\u7801\u4E0D\u6B63\u786E";
|
|
2338
|
+
}
|
|
2339
|
+
async function postJson(options, path, body) {
|
|
2340
|
+
const runtime = await resolveRuntimeForRequest(options);
|
|
2341
|
+
if (!runtime) {
|
|
2342
|
+
throw new Error("Cursor Pool service runtime is unavailable");
|
|
2343
|
+
}
|
|
2344
|
+
const response = await fetch(serviceUrl(runtime, path), {
|
|
2345
|
+
method: "POST",
|
|
2346
|
+
headers: { "content-type": "application/json" },
|
|
2347
|
+
body: JSON.stringify(body ?? {})
|
|
2348
|
+
});
|
|
2349
|
+
if (!response.ok) {
|
|
2350
|
+
if (response.status === 400 && path === "/platform/login") {
|
|
2351
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
2352
|
+
throw new Error(platformLoginErrorMessage(errorBody.code));
|
|
2353
|
+
}
|
|
2354
|
+
throw new Error(`\u672C\u5730\u670D\u52A1\u8BF7\u6C42\u5931\u8D25\uFF1A${response.status}`);
|
|
2355
|
+
}
|
|
2356
|
+
return await response.json();
|
|
2357
|
+
}
|
|
2358
|
+
async function getServiceStatus(runtimeFile, options = {}) {
|
|
2359
|
+
const runtime = await resolveHealthyRuntime({ runtimeFile, ...options });
|
|
2360
|
+
if (!runtime) {
|
|
2361
|
+
return { service: "stopped", runtime: null };
|
|
2362
|
+
}
|
|
2363
|
+
try {
|
|
2364
|
+
const response = await fetch(serviceUrl(runtime, "/extension/status"), {
|
|
2365
|
+
method: "POST",
|
|
2366
|
+
headers: { "content-type": "application/json" },
|
|
2367
|
+
body: JSON.stringify({ connected: true })
|
|
2368
|
+
});
|
|
2369
|
+
if (!response.ok) {
|
|
2370
|
+
return { service: "stopped", runtime };
|
|
2371
|
+
}
|
|
2372
|
+
return { service: "running", runtime };
|
|
2373
|
+
} catch {
|
|
2374
|
+
return { service: "stopped", runtime };
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
async function getPlatformStatus(runtimeFile, options = {}) {
|
|
2378
|
+
const runtime = await resolveRuntimeForRequest({ runtimeFile, ...options });
|
|
2379
|
+
if (!runtime) {
|
|
2380
|
+
return { state: "logged-out" };
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
const response = await fetch(serviceUrl(runtime, "/platform/status"));
|
|
2384
|
+
if (!response.ok) {
|
|
2385
|
+
return { state: "offline" };
|
|
2386
|
+
}
|
|
2387
|
+
const status = await response.json();
|
|
2388
|
+
return isPlatformStatus(status) ? status : { state: "offline" };
|
|
2389
|
+
} catch {
|
|
2390
|
+
return { state: "offline" };
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
async function getPlatformCatalog(runtimeFile, options = {}) {
|
|
2394
|
+
const runtime = await resolveRuntimeForRequest({ runtimeFile, ...options });
|
|
2395
|
+
if (!runtime) {
|
|
2396
|
+
return { state: "logged-out", products: [] };
|
|
2397
|
+
}
|
|
2398
|
+
try {
|
|
2399
|
+
const response = await fetch(serviceUrl(runtime, "/platform/catalog"));
|
|
2400
|
+
if (!response.ok) {
|
|
2401
|
+
return { state: "offline", products: [] };
|
|
2402
|
+
}
|
|
2403
|
+
return normalizePlatformCatalog(await response.json());
|
|
2404
|
+
} catch {
|
|
2405
|
+
return { state: "offline", products: [] };
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
async function startMode(runtimeFile, options = {}) {
|
|
2409
|
+
try {
|
|
2410
|
+
return normalizePlatformModeResult(await postJson({ runtimeFile, ...options }, "/mode/start"));
|
|
2411
|
+
} catch {
|
|
2412
|
+
return { state: "offline" };
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
async function stopMode(runtimeFile, options = {}) {
|
|
2416
|
+
try {
|
|
2417
|
+
return normalizePlatformModeResult(await postJson({ runtimeFile, ...options }, "/mode/stop"));
|
|
2418
|
+
} catch {
|
|
2419
|
+
return { state: "offline" };
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
async function loginPlatform(options) {
|
|
2423
|
+
return postJson(options, "/platform/login", {
|
|
2424
|
+
email: options.email,
|
|
2425
|
+
password: options.password,
|
|
2426
|
+
device: buildExtensionDeviceInfo()
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
async function selectPlatformProduct2(options) {
|
|
2430
|
+
const runtime = await resolveRuntimeForRequest(options);
|
|
2431
|
+
if (!runtime) {
|
|
2432
|
+
return { state: "offline" };
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
const response = await fetch(serviceUrl(runtime, "/platform/selection"), {
|
|
2436
|
+
method: "POST",
|
|
2437
|
+
headers: { "content-type": "application/json" },
|
|
2438
|
+
body: JSON.stringify({ productId: options.productId })
|
|
2439
|
+
});
|
|
2440
|
+
if (!response.ok) {
|
|
2441
|
+
return { state: "offline" };
|
|
2442
|
+
}
|
|
2443
|
+
return normalizePlatformSelectionResult(await response.json());
|
|
2444
|
+
} catch {
|
|
2445
|
+
return { state: "offline" };
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
async function logoutPlatform2(runtimeFile, options = {}) {
|
|
2449
|
+
return postJson({ runtimeFile, ...options }, "/platform/logout");
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// packages/extension/src/panel.ts
|
|
2453
|
+
function createPanelViewModel(status) {
|
|
2454
|
+
const rows = [
|
|
2455
|
+
{ label: "patch", value: status.patch },
|
|
2456
|
+
{ label: "service", value: status.service },
|
|
2457
|
+
{ label: "compat", value: status.compat },
|
|
2458
|
+
{ label: "mode", value: status.mode },
|
|
2459
|
+
{ label: "platform", value: status.platform ?? "logged-out" }
|
|
2460
|
+
];
|
|
2461
|
+
if (status.user) {
|
|
2462
|
+
rows.push({ label: "user", value: status.user });
|
|
2463
|
+
}
|
|
2464
|
+
if (status.device) {
|
|
2465
|
+
rows.push({ label: "device", value: status.device });
|
|
2466
|
+
}
|
|
2467
|
+
if (status.heartbeat) {
|
|
2468
|
+
rows.push({ label: "heartbeat", value: status.heartbeat });
|
|
2469
|
+
}
|
|
2470
|
+
if (status.catalog) {
|
|
2471
|
+
rows.push({ label: "catalog", value: status.catalog });
|
|
2472
|
+
}
|
|
2473
|
+
if (status.credits) {
|
|
2474
|
+
rows.push({ label: "credits", value: status.credits });
|
|
2475
|
+
}
|
|
2476
|
+
if (status.selectedProductId) {
|
|
2477
|
+
rows.push({ label: "current product", value: status.selectedProductId });
|
|
2478
|
+
}
|
|
2479
|
+
const platformState = status.platform ?? "logged-out";
|
|
2480
|
+
const takeoverActive = status.mode.startsWith("active ");
|
|
2481
|
+
const platformLoggedIn = platformState === "logged-in" || platformState === "offline" || platformState === "invalid-token";
|
|
2482
|
+
return {
|
|
2483
|
+
title: "Cursor Pool \u53F7\u6C60\u5BA2\u6237\u7AEF",
|
|
2484
|
+
rows,
|
|
2485
|
+
platformControls: {
|
|
2486
|
+
showLogin: platformState === "logged-out" || platformState === "offline",
|
|
2487
|
+
showLogout: platformLoggedIn,
|
|
2488
|
+
showRefresh: true,
|
|
2489
|
+
showEnableTakeover: platformState === "logged-in" && !takeoverActive,
|
|
2490
|
+
showDisableTakeover: platformLoggedIn && takeoverActive
|
|
2491
|
+
},
|
|
2492
|
+
consoleCommand: "cursorPool.openConsole",
|
|
2493
|
+
selectedProductId: status.selectedProductId,
|
|
2494
|
+
products: status.products ?? [],
|
|
2495
|
+
message: status.message,
|
|
2496
|
+
error: status.error
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
function formatPanelMode(mode) {
|
|
2500
|
+
if (mode?.state === "active") {
|
|
2501
|
+
return `active ${mode.productId}`;
|
|
2502
|
+
}
|
|
2503
|
+
if (mode?.state === "inactive" && mode.releaseReason) {
|
|
2504
|
+
return `inactive ${mode.releaseReason}`;
|
|
2505
|
+
}
|
|
2506
|
+
if (mode?.state === "inactive") {
|
|
2507
|
+
return "inactive";
|
|
2508
|
+
}
|
|
2509
|
+
return void 0;
|
|
2510
|
+
}
|
|
2511
|
+
function formatPanelPlatformStatus(status) {
|
|
2512
|
+
const mode = formatPanelMode(status.mode);
|
|
2513
|
+
return {
|
|
2514
|
+
platform: status.state,
|
|
2515
|
+
user: status.user?.email,
|
|
2516
|
+
device: status.device ? `${status.device.status} ${status.device.id}` : void 0,
|
|
2517
|
+
heartbeat: status.device?.lastHeartbeatAt,
|
|
2518
|
+
...mode ? { mode } : {}
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
function formatPanelPlatformCatalog(catalog) {
|
|
2522
|
+
const mode = catalog.state === "logged-in" || catalog.state === "offline" || catalog.state === "invalid-token" ? formatPanelMode(catalog.mode) : void 0;
|
|
2523
|
+
return {
|
|
2524
|
+
catalog: catalog.state,
|
|
2525
|
+
credits: catalog.state === "logged-in" ? String(catalog.account.credits) : void 0,
|
|
2526
|
+
selectedProductId: catalog.state === "logged-in" ? catalog.selectedProductId : void 0,
|
|
2527
|
+
products: catalog.state === "logged-in" ? catalog.products : [],
|
|
2528
|
+
...mode ? { mode } : {}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function platformSelectionMessage(result) {
|
|
2532
|
+
if (result.state === "selected") {
|
|
2533
|
+
return { message: "\u5546\u54C1\u5DF2\u5207\u6362" };
|
|
2534
|
+
}
|
|
2535
|
+
if (result.state === "logged-out") {
|
|
2536
|
+
return { error: "\u8BF7\u5148\u767B\u5F55\u5E73\u53F0" };
|
|
2537
|
+
}
|
|
2538
|
+
if (result.state === "invalid-token") {
|
|
2539
|
+
return { error: "\u5E73\u53F0\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" };
|
|
2540
|
+
}
|
|
2541
|
+
if (result.state === "product-not-found") {
|
|
2542
|
+
return { error: "\u5F53\u524D\u5546\u54C1\u5217\u8868\u4E2D\u6CA1\u6709\u8FD9\u4E2A\u5546\u54C1" };
|
|
2543
|
+
}
|
|
2544
|
+
if (result.state === "product-unavailable") {
|
|
2545
|
+
return { error: "\u5546\u54C1\u5F53\u524D\u4E0D\u53EF\u7528" };
|
|
2546
|
+
}
|
|
2547
|
+
return { error: "\u5E73\u53F0\u670D\u52A1\u6216\u7F51\u9875\u7AEF API \u6682\u65F6\u4E0D\u53EF\u7528" };
|
|
2548
|
+
}
|
|
2549
|
+
function platformModeMessage(result, action) {
|
|
2550
|
+
if (result.state === "active") {
|
|
2551
|
+
return { message: "\u5DF2\u5F00\u542F\u5E73\u53F0\u63A5\u7BA1" };
|
|
2552
|
+
}
|
|
2553
|
+
if (result.state === "inactive") {
|
|
2554
|
+
return { message: "\u5DF2\u5207\u56DE\u5B98\u65B9\u6A21\u5F0F" };
|
|
2555
|
+
}
|
|
2556
|
+
if (result.state === "logged-out") {
|
|
2557
|
+
return { error: "\u8BF7\u5148\u767B\u5F55\u5E73\u53F0" };
|
|
2558
|
+
}
|
|
2559
|
+
if (result.state === "invalid-token") {
|
|
2560
|
+
return { error: "\u5E73\u53F0\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" };
|
|
2561
|
+
}
|
|
2562
|
+
if (result.state === "product-not-selected") {
|
|
2563
|
+
return { error: "\u5F00\u542F\u5E73\u53F0\u63A5\u7BA1\u524D\u8BF7\u5148\u9009\u62E9\u5546\u54C1" };
|
|
2564
|
+
}
|
|
2565
|
+
if (result.state === "product-not-found") {
|
|
2566
|
+
return { error: "\u6240\u9009\u5546\u54C1\u4E0D\u5728\u5F53\u524D\u5546\u54C1\u5217\u8868\u4E2D" };
|
|
2567
|
+
}
|
|
2568
|
+
if (result.state === "product-unavailable") {
|
|
2569
|
+
return { error: "\u6240\u9009\u5546\u54C1\u5F53\u524D\u4E0D\u53EF\u7528" };
|
|
2570
|
+
}
|
|
2571
|
+
if (result.state === "insufficient-credits") {
|
|
2572
|
+
return { error: "\u79EF\u5206\u4E0D\u8DB3\uFF0C\u8BF7\u6253\u5F00\u7F51\u9875\u7AEF\u5904\u7406" };
|
|
2573
|
+
}
|
|
2574
|
+
return { error: action === "start" ? "\u5E73\u53F0\u670D\u52A1\u6216\u7F51\u9875\u7AEF API \u6682\u65F6\u4E0D\u53EF\u7528" : "\u5E73\u53F0\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528" };
|
|
2575
|
+
}
|
|
2576
|
+
function escapeHtml(value) {
|
|
2577
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
2578
|
+
}
|
|
2579
|
+
function buildPanelHtml(viewModel) {
|
|
2580
|
+
const valueFor = (label, fallback = "-") => viewModel.rows.find((row) => row.label === label)?.value ?? fallback;
|
|
2581
|
+
const statusItems = [
|
|
2582
|
+
{ label: "\u672C\u5730\u670D\u52A1", value: valueFor("service") },
|
|
2583
|
+
...valueFor("user", "") ? [{ label: "\u767B\u5F55\u8D26\u53F7", value: valueFor("user") }] : [],
|
|
2584
|
+
...valueFor("credits", "") ? [{ label: "\u79EF\u5206\u4F59\u989D", value: valueFor("credits") }] : [],
|
|
2585
|
+
...valueFor("user", "") ? [{
|
|
2586
|
+
label: "\u5F53\u524D\u6A21\u5F0F",
|
|
2587
|
+
value: valueFor("mode").startsWith("active ") ? "\u5E73\u53F0\u63A5\u7BA1" : "\u5B98\u65B9\u6A21\u5F0F"
|
|
2588
|
+
}] : []
|
|
2589
|
+
];
|
|
2590
|
+
const rows = statusItems.map((row) => `<li><span>${escapeHtml(row.label)}</span><strong>${escapeHtml(row.value)}</strong></li>`).join("");
|
|
2591
|
+
const currentProductName = viewModel.products.find((product) => product.id === viewModel.selectedProductId)?.name;
|
|
2592
|
+
const currentProduct = currentProductName ? `<li><span>\u5F53\u524D\u4F7F\u7528</span><strong>${escapeHtml(currentProductName)}</strong></li>` : "";
|
|
2593
|
+
const platformControls = [
|
|
2594
|
+
viewModel.platformControls.showLogin ? `<section class="panel-section platform-login">
|
|
2595
|
+
<h2>\u53F7\u6C60\u767B\u5F55</h2>
|
|
2596
|
+
<input name="email" placeholder="\u8D26\u53F7/\u90AE\u7BB1" autocomplete="username">
|
|
2597
|
+
<input name="password" type="password" placeholder="\u5BC6\u7801" autocomplete="current-password">
|
|
2598
|
+
<button type="button" data-command="platform/login">\u767B\u5F55</button>
|
|
2599
|
+
</section>` : "",
|
|
2600
|
+
viewModel.platformControls.showLogout ? '<button type="button" data-command="platform/logout">\u9000\u51FA\u767B\u5F55</button>' : "",
|
|
2601
|
+
viewModel.platformControls.showEnableTakeover ? '<button type="button" data-command="takeover/enable">\u5F00\u542F\u5E73\u53F0\u63A5\u7BA1</button>' : "",
|
|
2602
|
+
viewModel.platformControls.showDisableTakeover ? '<button type="button" data-command="takeover/disable">\u5207\u56DE\u5B98\u65B9\u6A21\u5F0F</button>' : "",
|
|
2603
|
+
viewModel.platformControls.showRefresh ? '<button type="button" data-command="platform/refresh">\u5237\u65B0\u72B6\u6001</button>' : ""
|
|
2604
|
+
].join("");
|
|
2605
|
+
const message = viewModel.message ? `<p class="message">${escapeHtml(viewModel.message)}</p>` : "";
|
|
2606
|
+
const error = viewModel.error ? `<p class="error">${escapeHtml(viewModel.error)}</p>` : "";
|
|
2607
|
+
const productItems = viewModel.products.map(
|
|
2608
|
+
(product) => {
|
|
2609
|
+
const rowClass = product.id === viewModel.selectedProductId ? "product-row is-selected" : "product-row";
|
|
2610
|
+
return `<li class="${rowClass}" title="${escapeHtml(product.description)}">
|
|
2611
|
+
<div class="product-main">
|
|
2612
|
+
<strong>${escapeHtml(product.name)}</strong>
|
|
2613
|
+
<span class="product-description">${escapeHtml(product.description)}</span>
|
|
2614
|
+
</div>
|
|
2615
|
+
<div class="product-meta">
|
|
2616
|
+
<span>${escapeHtml(product.status)}</span>
|
|
2617
|
+
<span>\u51C6\u5165 ${escapeHtml(String(product.minCredits))}</span>
|
|
2618
|
+
<span>${escapeHtml(product.usageLabel)}</span>
|
|
2619
|
+
</div>
|
|
2620
|
+
<div class="product-action">
|
|
2621
|
+
${product.id === viewModel.selectedProductId ? '<span class="current-badge">\u5F53\u524D\u4F7F\u7528</span>' : product.status === "available" ? `<button type="button" data-command="platform/select-product" data-product-id="${escapeHtml(product.id)}">\u4F7F\u7528</button>` : ""}
|
|
2622
|
+
</div>
|
|
2623
|
+
</li>`;
|
|
2624
|
+
}
|
|
2625
|
+
).join("");
|
|
2626
|
+
const products = viewModel.products.length > 0 ? `<section class="panel-section products"><h2>\u53EF\u7528\u5546\u54C1</h2><ul class="product-list">${productItems}</ul></section>` : "";
|
|
2627
|
+
return `<!doctype html>
|
|
2628
|
+
<html lang="zh-CN">
|
|
2629
|
+
<head>
|
|
2630
|
+
<meta charset="utf-8">
|
|
2631
|
+
<style>
|
|
2632
|
+
body { font-family: var(--vscode-font-family, sans-serif); padding: 12px; color: var(--vscode-foreground); }
|
|
2633
|
+
h1 { font-size: 15px; margin: 0 0 10px; }
|
|
2634
|
+
h2 { font-size: 12px; margin: 0 0 8px; }
|
|
2635
|
+
ul { list-style: none; margin: 0; padding: 0; }
|
|
2636
|
+
li { margin: 0; }
|
|
2637
|
+
.status-list { display: grid; gap: 6px; }
|
|
2638
|
+
.status-list li { display: flex; justify-content: space-between; gap: 10px; min-width: 0; }
|
|
2639
|
+
.status-list span { color: var(--vscode-descriptionForeground, #777); }
|
|
2640
|
+
.status-list strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2641
|
+
strong { font-weight: 600; }
|
|
2642
|
+
button { border: 1px solid var(--vscode-button-border, transparent); border-radius: 4px; padding: 3px 8px; color: var(--vscode-button-foreground); background: var(--vscode-button-background); }
|
|
2643
|
+
input { box-sizing: border-box; display: block; width: 100%; margin: 6px 0; }
|
|
2644
|
+
.panel-section { border-top: 1px solid var(--vscode-panel-border, #ddd); padding-top: 10px; margin-top: 10px; }
|
|
2645
|
+
.products { margin-top: 10px; }
|
|
2646
|
+
.product-list { max-height: 260px; overflow: auto; border: 1px solid var(--vscode-panel-border, #ddd); border-radius: 6px; }
|
|
2647
|
+
.product-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; align-items: center; padding: 8px; border-top: 1px solid var(--vscode-panel-border, #ddd); }
|
|
2648
|
+
.product-row:first-child { border-top: 0; }
|
|
2649
|
+
.product-row.is-selected { background: var(--vscode-list-activeSelectionBackground, rgba(45, 91, 209, 0.14)); }
|
|
2650
|
+
.product-main { min-width: 0; }
|
|
2651
|
+
.product-main strong, .product-description { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2652
|
+
.product-description, .product-meta { color: var(--vscode-descriptionForeground, #777); font-size: 11px; }
|
|
2653
|
+
.product-meta { display: grid; gap: 1px; white-space: nowrap; }
|
|
2654
|
+
.product-action { min-width: 46px; text-align: right; }
|
|
2655
|
+
.current-badge { color: var(--vscode-testing-iconPassed, #17633f); font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
2656
|
+
.platform-controls { margin-top: 10px; }
|
|
2657
|
+
.platform-controls button { margin-right: 8px; margin-bottom: 8px; }
|
|
2658
|
+
.message { color: var(--vscode-descriptionForeground, #666); }
|
|
2659
|
+
.error { color: var(--vscode-errorForeground, #b00020); }
|
|
2660
|
+
</style>
|
|
2661
|
+
</head>
|
|
2662
|
+
<body>
|
|
2663
|
+
<h1>${escapeHtml(viewModel.title)}</h1>
|
|
2664
|
+
<section class="panel-section"><h2>\u8FD0\u884C\u72B6\u6001</h2><ul class="status-list">${rows}${currentProduct}</ul></section>
|
|
2665
|
+
${message}
|
|
2666
|
+
${error}
|
|
2667
|
+
${products}
|
|
2668
|
+
<div class="platform-controls">${platformControls}</div>
|
|
2669
|
+
<script>
|
|
2670
|
+
const vscode = acquireVsCodeApi();
|
|
2671
|
+
document.addEventListener('click', (event) => {
|
|
2672
|
+
const target = event.target;
|
|
2673
|
+
if (!(target instanceof HTMLElement) || !target.dataset.command) {
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const payload = { command: target.dataset.command };
|
|
2678
|
+
if (target.dataset.command === 'platform/login') {
|
|
2679
|
+
payload.email = document.querySelector('[name="email"]')?.value || '';
|
|
2680
|
+
payload.password = document.querySelector('[name="password"]')?.value || '';
|
|
2681
|
+
}
|
|
2682
|
+
if (target.dataset.productId) {
|
|
2683
|
+
payload.productId = target.dataset.productId;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
vscode.postMessage(payload);
|
|
2687
|
+
});
|
|
2688
|
+
</script>
|
|
2689
|
+
</body>
|
|
2690
|
+
</html>`;
|
|
2691
|
+
}
|
|
2692
|
+
function registerStatusPanel(context, vscode, options = {}) {
|
|
2693
|
+
const provider = {
|
|
2694
|
+
async resolveWebviewView(view) {
|
|
2695
|
+
const readServiceStatus = options.getServiceStatus ?? getServiceStatus;
|
|
2696
|
+
const ensureService = options.ensureService ?? (async (runtimeFile) => {
|
|
2697
|
+
await ensureLocalService(runtimeFile);
|
|
2698
|
+
});
|
|
2699
|
+
const readPlatformStatus = options.getPlatformStatus ?? getPlatformStatus;
|
|
2700
|
+
const readPlatformCatalog = options.getPlatformCatalog ?? getPlatformCatalog;
|
|
2701
|
+
const doLoginPlatform = options.loginPlatform ?? loginPlatform;
|
|
2702
|
+
const doLogoutPlatform = options.logoutPlatform ?? logoutPlatform2;
|
|
2703
|
+
const doSelectPlatformProduct = options.selectPlatformProduct ?? selectPlatformProduct2;
|
|
2704
|
+
const doStartMode = options.startMode ?? startMode;
|
|
2705
|
+
const doStopMode = options.stopMode ?? stopMode;
|
|
2706
|
+
let serviceEnsured = false;
|
|
2707
|
+
let initialServiceStatus;
|
|
2708
|
+
view.webview.options = {
|
|
2709
|
+
...view.webview.options,
|
|
2710
|
+
enableScripts: true
|
|
2711
|
+
};
|
|
2712
|
+
const render = async (feedback = {}, serviceStatus) => {
|
|
2713
|
+
const service = options.status ? options.status.service : (serviceStatus ?? await readServiceStatus(options.runtimeFile)).service;
|
|
2714
|
+
const platform = options.status ? {} : formatPanelPlatformStatus(await readPlatformStatus(options.runtimeFile));
|
|
2715
|
+
const catalog = options.status ? {} : formatPanelPlatformCatalog(await readPlatformCatalog(options.runtimeFile));
|
|
2716
|
+
const status = options.status ? { ...options.status, ...feedback } : {
|
|
2717
|
+
patch: "unknown",
|
|
2718
|
+
service,
|
|
2719
|
+
compat: "unknown",
|
|
2720
|
+
mode: "unknown",
|
|
2721
|
+
...platform,
|
|
2722
|
+
...catalog,
|
|
2723
|
+
...feedback
|
|
2724
|
+
};
|
|
2725
|
+
view.webview.html = buildPanelHtml(createPanelViewModel(status));
|
|
2726
|
+
};
|
|
2727
|
+
const renderError = async (error) => {
|
|
2728
|
+
try {
|
|
2729
|
+
const message = error instanceof Error && error.message === "fetch failed" ? "\u672C\u5730\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 Cursor Pool \u672C\u5730\u670D\u52A1" : error instanceof Error ? error.message : "Cursor Pool service request failed";
|
|
2730
|
+
await render({
|
|
2731
|
+
error: message
|
|
2732
|
+
});
|
|
2733
|
+
} catch {
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
const ensureServiceForPanel = async () => {
|
|
2737
|
+
if (serviceEnsured) {
|
|
2738
|
+
return void 0;
|
|
2739
|
+
}
|
|
2740
|
+
const service = await readServiceStatus(options.runtimeFile);
|
|
2741
|
+
if (service.service === "stopped") {
|
|
2742
|
+
await ensureService(options.runtimeFile);
|
|
2743
|
+
serviceEnsured = true;
|
|
2744
|
+
return void 0;
|
|
2745
|
+
}
|
|
2746
|
+
serviceEnsured = true;
|
|
2747
|
+
return service;
|
|
2748
|
+
};
|
|
2749
|
+
if (!options.status) {
|
|
2750
|
+
try {
|
|
2751
|
+
initialServiceStatus = await ensureServiceForPanel();
|
|
2752
|
+
} catch {
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
await render({}, initialServiceStatus);
|
|
2756
|
+
view.webview.onDidReceiveMessage?.(async (message) => {
|
|
2757
|
+
try {
|
|
2758
|
+
if (message.command === "mode/start" || message.command === "takeover/enable") {
|
|
2759
|
+
const result = await doStartMode(options.runtimeFile);
|
|
2760
|
+
await render(platformModeMessage(result, "start"));
|
|
2761
|
+
if (result.state === "active") {
|
|
2762
|
+
await vscode.commands?.executeCommand?.("workbench.action.reloadWindow");
|
|
2763
|
+
}
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
if (message.command === "mode/stop" || message.command === "takeover/disable") {
|
|
2767
|
+
const result = await doStopMode(options.runtimeFile);
|
|
2768
|
+
await render(platformModeMessage(result, "stop"));
|
|
2769
|
+
if (result.state === "inactive") {
|
|
2770
|
+
await vscode.commands?.executeCommand?.("workbench.action.reloadWindow");
|
|
2771
|
+
}
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
if (message.command === "platform/login") {
|
|
2775
|
+
if (!message.email) {
|
|
2776
|
+
await render({ error: "\u8BF7\u8F93\u5165\u8D26\u53F7/\u90AE\u7BB1" });
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
if (!message.password) {
|
|
2780
|
+
await render({ error: "\u8BF7\u8F93\u5165\u5BC6\u7801" });
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
await ensureServiceForPanel();
|
|
2784
|
+
try {
|
|
2785
|
+
await doLoginPlatform({
|
|
2786
|
+
runtimeFile: options.runtimeFile,
|
|
2787
|
+
email: message.email,
|
|
2788
|
+
password: message.password
|
|
2789
|
+
});
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
const current = await readPlatformStatus(options.runtimeFile).catch(() => null);
|
|
2792
|
+
if (current?.state === "logged-in") {
|
|
2793
|
+
await render({ message: "\u5E73\u53F0\u72B6\u6001\u5DF2\u5237\u65B0" });
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
throw error;
|
|
2797
|
+
}
|
|
2798
|
+
await render({ message: "\u5E73\u53F0\u767B\u5F55\u6210\u529F" });
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
if (message.command === "platform/logout") {
|
|
2802
|
+
await doLogoutPlatform(options.runtimeFile);
|
|
2803
|
+
await render({ message: "\u5DF2\u9000\u51FA\u5E73\u53F0\u767B\u5F55" });
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
if (message.command === "platform/select-product") {
|
|
2807
|
+
if (!message.productId) {
|
|
2808
|
+
await render({ error: "\u8BF7\u9009\u62E9\u5546\u54C1" });
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const result = await doSelectPlatformProduct({
|
|
2812
|
+
runtimeFile: options.runtimeFile,
|
|
2813
|
+
productId: message.productId
|
|
2814
|
+
});
|
|
2815
|
+
await render(platformSelectionMessage(result));
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
if (message.command === "platform/refresh") {
|
|
2819
|
+
await render({ message: "\u5E73\u53F0\u72B6\u6001\u5DF2\u5237\u65B0" });
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
if (message.command === "cursorPool.openConsole") {
|
|
2823
|
+
await vscode.commands?.executeCommand?.("workbench.action.webview.openDeveloperTools");
|
|
2824
|
+
}
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
await renderError(error);
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
const subscription = vscode.window?.registerWebviewViewProvider?.("cursorPool.statusPanel", provider);
|
|
2832
|
+
if (subscription) {
|
|
2833
|
+
context.subscriptions?.push(subscription);
|
|
2834
|
+
}
|
|
2835
|
+
return provider;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// packages/extension/src/extension.ts
|
|
2839
|
+
async function appendDiagnostic(message) {
|
|
2840
|
+
try {
|
|
2841
|
+
const os = await new Function("specifier", "return import(specifier)")("node:os");
|
|
2842
|
+
const path = await new Function("specifier", "return import(specifier)")("node:path");
|
|
2843
|
+
const fs = await new Function("specifier", "return import(specifier)")("node:fs/promises");
|
|
2844
|
+
const logDir = path.join(os.homedir(), ".cursor-pool/logs");
|
|
2845
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
2846
|
+
await fs.appendFile(
|
|
2847
|
+
path.join(logDir, "extension.log"),
|
|
2848
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} ${message}
|
|
2849
|
+
`,
|
|
2850
|
+
"utf8"
|
|
2851
|
+
);
|
|
2852
|
+
} catch {
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
function registerExtensionCommands(context, vscode, options = {}) {
|
|
2856
|
+
void appendDiagnostic("registerExtensionCommands");
|
|
2857
|
+
const openClientSubscription = vscode.commands?.registerCommand?.("cursorPool.openClient", () => {
|
|
2858
|
+
void appendDiagnostic("cursorPool.openClient invoked");
|
|
2859
|
+
try {
|
|
2860
|
+
const result = vscode.commands?.executeCommand?.("workbench.view.extension.cursorPoolClient");
|
|
2861
|
+
void appendDiagnostic("workbench.view.extension.cursorPoolClient dispatched");
|
|
2862
|
+
return result;
|
|
2863
|
+
} catch (error) {
|
|
2864
|
+
void appendDiagnostic(`workbench.view.extension.cursorPoolClient failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2865
|
+
throw error;
|
|
2866
|
+
}
|
|
2867
|
+
});
|
|
2868
|
+
if (openClientSubscription) {
|
|
2869
|
+
void appendDiagnostic("cursorPool.openClient registered");
|
|
2870
|
+
context.subscriptions?.push(openClientSubscription);
|
|
2871
|
+
} else {
|
|
2872
|
+
void appendDiagnostic("cursorPool.openClient not registered");
|
|
2873
|
+
}
|
|
2874
|
+
const openConsoleSubscription = vscode.commands?.registerCommand?.("cursorPool.openConsole", () => {
|
|
2875
|
+
vscode.window?.showInformationMessage?.("\u8BF7\u5728\u7F51\u9875\u7AEF\u67E5\u770B\u5145\u503C\u3001\u8BB0\u5F55\u3001\u8BBE\u5907\u548C\u8D26\u53F7\u4FE1\u606F\u3002");
|
|
2876
|
+
});
|
|
2877
|
+
if (openConsoleSubscription) {
|
|
2878
|
+
context.subscriptions?.push(openConsoleSubscription);
|
|
2879
|
+
}
|
|
2880
|
+
const reportStatusSubscription = vscode.commands?.registerCommand?.(
|
|
2881
|
+
"cursorPool.reportStatus",
|
|
2882
|
+
async () => {
|
|
2883
|
+
const readStatus = options.getServiceStatus ?? getServiceStatus;
|
|
2884
|
+
const status = await readStatus(options.runtimeFile);
|
|
2885
|
+
vscode.window?.showInformationMessage?.(`Cursor Pool service: ${status.service}`);
|
|
2886
|
+
}
|
|
2887
|
+
);
|
|
2888
|
+
if (reportStatusSubscription) {
|
|
2889
|
+
context.subscriptions?.push(reportStatusSubscription);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
async function activate(context, injectedVscode, options = {}) {
|
|
2893
|
+
void appendDiagnostic("activate start");
|
|
2894
|
+
try {
|
|
2895
|
+
const vscode = injectedVscode ?? await new Function("specifier", "return import(specifier)")("vscode");
|
|
2896
|
+
registerExtensionCommands(context, vscode, options);
|
|
2897
|
+
registerStatusPanel(context, vscode, options);
|
|
2898
|
+
void appendDiagnostic("activate complete");
|
|
2899
|
+
} catch (error) {
|
|
2900
|
+
void appendDiagnostic(`activate failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
function deactivate() {
|
|
2905
|
+
}
|
|
2906
|
+
export {
|
|
2907
|
+
activate,
|
|
2908
|
+
deactivate,
|
|
2909
|
+
registerExtensionCommands
|
|
2910
|
+
};
|