anygate 0.1.0

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.
@@ -0,0 +1,1452 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BACKENDS,
4
+ MAX_MODEL_CATALOG,
5
+ VERSION,
6
+ addCustomEndpointProvider,
7
+ addProviderFromTemplate,
8
+ buildAntigravityAuthUrl,
9
+ buildDedupedModelRows,
10
+ checkForUpdates,
11
+ completeAntigravityExchange,
12
+ createGatewayModelCatalog,
13
+ favoriteProviderDisplayName,
14
+ fetchProviderCatalog,
15
+ filterServerModelsByFavorites,
16
+ filterServerModelsByFreeStatus,
17
+ filterServerModelsByProviders,
18
+ findBinaryOnPath,
19
+ findClaudeApp,
20
+ findCodexApp,
21
+ freeStatusLabel,
22
+ gatewayProviderLabel,
23
+ getAppHome,
24
+ getAppPathOverride,
25
+ getLocalIps,
26
+ getSavedServerPassword,
27
+ getServerExposedProviders,
28
+ getServerFavoritesOnly,
29
+ getServerFreeModelsOnly,
30
+ getServerListenMode,
31
+ getServerMaskGatewayIds,
32
+ getUiDebugLogPath,
33
+ guiCallbackRedirectUri,
34
+ loadPreferences,
35
+ loadRegistry,
36
+ loadServerModels,
37
+ makeTraceLogger,
38
+ openAiDeviceCodeUrl,
39
+ pollGithubDeviceCodeToken,
40
+ pollOpenAiDeviceCodeToken,
41
+ pollXaiDeviceCodeToken,
42
+ providerOptionsFromCatalog,
43
+ readBody,
44
+ recordLaunchFolder,
45
+ refreshAllProviderModels,
46
+ refreshProviderModels,
47
+ removeProviderFromRegistry,
48
+ requestGithubDeviceCode,
49
+ requestOpenAiDeviceCode,
50
+ requestXaiDeviceCode,
51
+ resolveProviderCredential,
52
+ resolveServerUpstreamApiKey,
53
+ saveNativeOAuthCredential,
54
+ savePreferences,
55
+ saveProviderCredential,
56
+ sendJson,
57
+ setAppPathOverride,
58
+ setSavedServerPassword,
59
+ setServerExposedProviders,
60
+ setServerFavoritesOnly,
61
+ setServerFreeModelsOnly,
62
+ setServerListenMode,
63
+ setServerMaskGatewayIds,
64
+ startServer,
65
+ summarizeServerProviders,
66
+ validateCustomEndpointUrl,
67
+ writeSecureLogLine
68
+ } from "./chunk-OACEC5EH.js";
69
+ import {
70
+ getTemplateById,
71
+ listAddableTemplates,
72
+ listVisibleOAuthTemplates
73
+ } from "./chunk-YYSUTRMV.js";
74
+
75
+ // src/ui/command.ts
76
+ import { createServer } from "http";
77
+ import { readFileSync, readdirSync, writeFileSync as writeFileSync2, unlinkSync, existsSync as existsSync3, mkdirSync } from "fs";
78
+ import { join as join2 } from "path";
79
+ import { fileURLToPath } from "url";
80
+ import { dirname } from "path";
81
+ import pc from "picocolors";
82
+ import * as p from "@clack/prompts";
83
+
84
+ // src/agents/shared/native-launcher.ts
85
+ import { chmodSync, existsSync, mkdtempSync, writeFileSync } from "fs";
86
+ import { homedir } from "os";
87
+ import { tmpdir } from "os";
88
+ import { join } from "path";
89
+ var isWindows = process.platform === "win32";
90
+ var isMac = process.platform === "darwin";
91
+ var SUPPORTED_APPS = [
92
+ { id: "claude", name: "Claude Code CLI", type: "cli", detectId: "claude", gatewayCommand: "claude" },
93
+ { id: "codex", name: "Codex CLI", type: "cli", detectId: "codex", gatewayCommand: "codex" },
94
+ { id: "gemini", name: "Gemini CLI", type: "cli", detectId: "gemini", gatewayCommand: "gemini" },
95
+ { id: "agy", name: "Antigravity CLI", type: "cli", detectId: "agy", gatewayCommand: "agy" },
96
+ {
97
+ id: "antigravity",
98
+ name: "Antigravity (App)",
99
+ type: "app",
100
+ detectId: "antigravity",
101
+ gatewayCommand: "antigravity"
102
+ },
103
+ {
104
+ id: "antigravity-ide",
105
+ name: "Antigravity IDE (App)",
106
+ type: "app",
107
+ detectId: "antigravity-ide",
108
+ gatewayCommand: "antigravity-ide"
109
+ },
110
+ {
111
+ id: "claude-app",
112
+ name: "Claude Code Desktop",
113
+ type: "app",
114
+ detectId: "claude-app",
115
+ gatewayCommand: "claude-app"
116
+ },
117
+ {
118
+ id: "codex-app",
119
+ name: "ChatGPT Desktop (Codex)",
120
+ type: "app",
121
+ detectId: "codex-app",
122
+ gatewayCommand: "codex-app"
123
+ }
124
+ ];
125
+ function fallbackPathsForApp(id, platform = process.platform) {
126
+ const windows = platform === "win32";
127
+ const mac = platform === "darwin";
128
+ const appData = process.env["APPDATA"] ?? homedir();
129
+ const localAppData = process.env["LOCALAPPDATA"] ?? join(homedir(), "AppData", "Local");
130
+ switch (id) {
131
+ case "claude":
132
+ return windows ? [
133
+ join(appData, "npm", "claude.cmd"),
134
+ join(appData, "npm", "claude")
135
+ ] : [
136
+ join(homedir(), ".local", "bin", "claude"),
137
+ join(homedir(), ".npm", "bin", "claude"),
138
+ "/usr/local/bin/claude",
139
+ "/opt/homebrew/bin/claude"
140
+ ];
141
+ case "codex":
142
+ return windows ? [
143
+ join(appData, "npm", "codex.cmd"),
144
+ join(appData, "npm", "codex")
145
+ ] : [
146
+ join(homedir(), ".local", "bin", "codex"),
147
+ join(homedir(), ".npm", "bin", "codex"),
148
+ "/usr/local/bin/codex",
149
+ "/opt/homebrew/bin/codex"
150
+ ];
151
+ case "gemini":
152
+ return windows ? [
153
+ join(appData, "npm", "gemini.cmd"),
154
+ join(appData, "npm", "gemini")
155
+ ] : [
156
+ join(homedir(), ".local", "bin", "gemini"),
157
+ join(homedir(), ".npm", "bin", "gemini"),
158
+ "/usr/local/bin/gemini",
159
+ "/opt/homebrew/bin/gemini"
160
+ ];
161
+ case "agy":
162
+ return windows ? [
163
+ join(appData, "npm", "agy.cmd"),
164
+ join(appData, "npm", "agy"),
165
+ join(localAppData, "Antigravity", "agy.exe")
166
+ ] : [
167
+ join(homedir(), ".local", "bin", "agy"),
168
+ join(homedir(), ".npm", "bin", "agy"),
169
+ "/usr/local/bin/agy",
170
+ "/opt/homebrew/bin/agy"
171
+ ];
172
+ case "antigravity-ide":
173
+ if (mac) {
174
+ return [
175
+ "/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide",
176
+ join(homedir(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide")
177
+ ];
178
+ }
179
+ if (windows) {
180
+ return [
181
+ join(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe"),
182
+ join(localAppData, "Programs", "antigravity-ide", "Antigravity IDE.exe"),
183
+ join(localAppData, "Programs", "Antigravity", "Antigravity IDE.exe")
184
+ ];
185
+ }
186
+ return ["/opt/antigravity-ide/Antigravity-IDE"];
187
+ case "antigravity":
188
+ if (mac) {
189
+ return [
190
+ "/Applications/Antigravity.app/Contents/MacOS/Antigravity",
191
+ join(homedir(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity")
192
+ ];
193
+ }
194
+ if (windows) {
195
+ return [
196
+ join(localAppData, "Programs", "Antigravity", "Antigravity.exe")
197
+ ];
198
+ }
199
+ return [
200
+ "/opt/antigravity/antigravity",
201
+ "/usr/local/bin/antigravity",
202
+ "/usr/bin/antigravity"
203
+ ];
204
+ case "claude-app":
205
+ if (mac) {
206
+ return [
207
+ "/Applications/Claude.app/Contents/MacOS/Claude",
208
+ join(homedir(), "Applications", "Claude.app", "Contents", "MacOS", "Claude")
209
+ ];
210
+ }
211
+ return windows ? [join(localAppData, "Programs", "claude", "Claude.exe")] : [];
212
+ case "codex-app":
213
+ if (mac) {
214
+ return [
215
+ "/Applications/ChatGPT.app",
216
+ join(homedir(), "Applications", "ChatGPT.app"),
217
+ "/Applications/Codex.app",
218
+ join(homedir(), "Applications", "Codex.app")
219
+ ];
220
+ }
221
+ return windows ? [
222
+ join(localAppData, "Programs", "ChatGPT", "ChatGPT.exe"),
223
+ join(localAppData, "Programs", "OpenAI ChatGPT", "ChatGPT.exe"),
224
+ join(localAppData, "openai-chatgpt-electron", "ChatGPT.exe"),
225
+ join(localAppData, "Programs", "Codex", "Codex.exe"),
226
+ join(localAppData, "Programs", "OpenAI Codex", "Codex.exe"),
227
+ join(localAppData, "openai-codex-electron", "Codex.exe")
228
+ ] : [];
229
+ default:
230
+ return [];
231
+ }
232
+ }
233
+ var FALLBACKS = Object.fromEntries(
234
+ SUPPORTED_APPS.map((app) => [app.detectId, fallbackPathsForApp(app.detectId)])
235
+ );
236
+ function getSupportedApp(id) {
237
+ return SUPPORTED_APPS.find((app) => app.id === id);
238
+ }
239
+ function detectApp(id) {
240
+ const override = getAppPathOverride(id);
241
+ if (override) {
242
+ return existsSync(override) ? { installed: true, path: override, pathSource: "override" } : { installed: false, path: override, pathSource: "override" };
243
+ }
244
+ const resolvedPath = findBinaryOnPath(id, FALLBACKS[id] ?? [], { verifyWhichResult: true });
245
+ if (resolvedPath) {
246
+ return { installed: true, path: resolvedPath, pathSource: "auto" };
247
+ }
248
+ const appFinder = id === "claude-app" ? findClaudeApp : id === "codex-app" ? findCodexApp : null;
249
+ if (appFinder) {
250
+ const appPath = appFinder();
251
+ if (appPath) return { installed: true, path: appPath, pathSource: "auto" };
252
+ }
253
+ return { installed: false, path: null, pathSource: null };
254
+ }
255
+ function getTerminalLaunchCommand(binPath, args, opts = {}) {
256
+ const fullCmd = [binPath, ...args].map((arg) => {
257
+ if (!/^[a-zA-Z0-9\-_./:]+$/.test(arg)) {
258
+ throw new Error(`Unsafe launch argument: ${JSON.stringify(arg)}`);
259
+ }
260
+ return arg;
261
+ }).join(" ");
262
+ const cwdPrefix = opts.cwd ? `cd ${quoteShellArg(opts.cwd)} && ` : "";
263
+ const runCmd = `${cwdPrefix}${fullCmd}`;
264
+ if (isMac) {
265
+ const dir2 = mkdtempSync(join(tmpdir(), "anygate-launch-"));
266
+ const scriptPath2 = join(dir2, "launch.command");
267
+ const displayCommand = opts.displayCommand ?? [binPath, ...args].join(" ");
268
+ writeFileSync(scriptPath2, [
269
+ "#!/bin/sh",
270
+ `trap 'rm -f "$0"; rmdir "$(dirname "$0")" 2>/dev/null' EXIT`,
271
+ "clear",
272
+ opts.cwd ? `cd ${quoteShellArg(opts.cwd)} || exit 1` : "",
273
+ `printf '%s\\n\\n' ${quoteShellArg(`$ ${displayCommand}`)}`,
274
+ fullCmd,
275
+ "status=$?",
276
+ 'printf "\\nanygate session exited with code %s. Press Return to close this window. " "$status"',
277
+ "read _",
278
+ 'exit "$status"',
279
+ ""
280
+ ].join("\n"), { encoding: "utf8", mode: 448 });
281
+ chmodSync(scriptPath2, 448);
282
+ return `open -a Terminal ${quoteShellArg(scriptPath2)}`;
283
+ }
284
+ if (isWindows) {
285
+ const dirFlag = opts.cwd ? `/d "${opts.cwd}" ` : "";
286
+ return `start "anygate Terminal" ${dirFlag}cmd.exe /k "${fullCmd}"`;
287
+ }
288
+ const dir = mkdtempSync(join(tmpdir(), "anygate-launch-"));
289
+ const scriptPath = join(dir, "launch.sh");
290
+ writeFileSync(scriptPath, [
291
+ "#!/bin/sh",
292
+ runCmd,
293
+ "exec sh",
294
+ ""
295
+ ].join("\n"), { encoding: "utf8", mode: 448 });
296
+ chmodSync(scriptPath, 448);
297
+ const scriptArg = quoteShellArg(scriptPath);
298
+ return `x-terminal-emulator -e sh ${scriptArg} || gnome-terminal -- sh ${scriptArg} || xterm -e sh ${scriptArg}`;
299
+ }
300
+ function quoteShellArg(value) {
301
+ if (/^[a-zA-Z0-9\-_./]+$/.test(value)) return value;
302
+ return `'${value.replace(/'/g, "'\\''")}'`;
303
+ }
304
+ function gatewayCliPath() {
305
+ return "anygate";
306
+ }
307
+ function getGatewayLaunchCommand(appId, options = {}) {
308
+ const app = getSupportedApp(appId);
309
+ if (!app) throw new Error(`Unsupported app: ${appId}`);
310
+ const args = [app.gatewayCommand];
311
+ if (options.trace) {
312
+ args.push("--trace");
313
+ }
314
+ if (options.providerId && options.modelId) {
315
+ args.push("--provider", options.providerId, "--model", options.modelId);
316
+ } else if (options.providerId || options.modelId) {
317
+ throw new Error("Both providerId and modelId are required for an explicit anygate launch.");
318
+ }
319
+ return getTerminalLaunchCommand(gatewayCliPath(), args, {
320
+ cwd: options.cwd,
321
+ displayCommand: ["anygate", ...args].join(" ")
322
+ });
323
+ }
324
+ function getSupportedApps() {
325
+ return SUPPORTED_APPS.map((app) => {
326
+ const { installed, path, pathSource } = detectApp(app.detectId);
327
+ return {
328
+ id: app.id,
329
+ name: app.name,
330
+ type: app.type,
331
+ installed,
332
+ path,
333
+ pathSource,
334
+ gatewayCommand: app.gatewayCommand,
335
+ launchCommand: installed ? getGatewayLaunchCommand(app.id) : null
336
+ };
337
+ });
338
+ }
339
+
340
+ // src/ui/api.ts
341
+ import { exec } from "child_process";
342
+ import { promisify } from "util";
343
+ import { existsSync as existsSync2, statSync } from "fs";
344
+ import { randomUUID } from "crypto";
345
+
346
+ // src/ui/server-control.ts
347
+ var running = null;
348
+ var startInFlight = null;
349
+ var SAVED_PASSWORD_CACHE_TTL_MS = 3e4;
350
+ var hasSavedPasswordCache = null;
351
+ async function hasSavedPasswordCached() {
352
+ const now = Date.now();
353
+ if (hasSavedPasswordCache && hasSavedPasswordCache.expiresAt > now) return hasSavedPasswordCache.value;
354
+ const value = Boolean(await getSavedServerPassword());
355
+ hasSavedPasswordCache = { value, expiresAt: now + SAVED_PASSWORD_CACHE_TTL_MS };
356
+ return value;
357
+ }
358
+ function buildModelRows(models, gateway) {
359
+ const groups = /* @__PURE__ */ new Map();
360
+ for (const model of models) {
361
+ const label = gatewayProviderLabel(model);
362
+ const list = groups.get(label);
363
+ if (list) list.push(model);
364
+ else groups.set(label, [model]);
365
+ }
366
+ const rows = [];
367
+ for (const [providerLabel, groupModels] of groups) {
368
+ for (const row of buildDedupedModelRows(groupModels, gateway)) rows.push({ providerLabel, ...row });
369
+ }
370
+ return rows.sort((a, b) => a.providerLabel.localeCompare(b.providerLabel) || a.name.localeCompare(b.name));
371
+ }
372
+ async function buildSavedConfig() {
373
+ return {
374
+ favoritesOnly: getServerFavoritesOnly(),
375
+ freeModelsOnly: getServerFreeModelsOnly(),
376
+ exposedProviders: getServerExposedProviders(),
377
+ maskGatewayIds: getServerMaskGatewayIds(),
378
+ listenMode: getServerListenMode(),
379
+ hasSavedPassword: await hasSavedPasswordCached()
380
+ };
381
+ }
382
+ async function getServerStatus() {
383
+ const saved = await buildSavedConfig();
384
+ if (!running) return { running: false, saved };
385
+ const { handle, config, serverPassword, providerSummary, modelRows } = running;
386
+ const payload = {
387
+ running: true,
388
+ saved,
389
+ listenMode: config.listenMode,
390
+ anthropicUrl: `http://127.0.0.1:${handle.port}/anthropic`,
391
+ openaiUrl: `http://127.0.0.1:${handle.port}/openai/v1`,
392
+ exposedProviders: config.exposedProviders,
393
+ favoritesOnly: config.favoritesOnly,
394
+ freeModelsOnly: config.freeModelsOnly,
395
+ maskGatewayIds: config.maskGatewayIds,
396
+ providerSummary,
397
+ models: modelRows
398
+ };
399
+ if (config.listenMode === "network") {
400
+ payload.networkUrls = getLocalIps().map(({ name, address }) => ({
401
+ name,
402
+ anthropicUrl: `http://${address}:${handle.port}/anthropic`,
403
+ openaiUrl: `http://${address}:${handle.port}/openai/v1`
404
+ }));
405
+ payload.apiKey = serverPassword ?? void 0;
406
+ } else {
407
+ payload.apiKey = "any non-empty value";
408
+ }
409
+ return payload;
410
+ }
411
+ function startGatewayServer(req) {
412
+ if (running) return Promise.resolve({ ok: false, error: "Server is already running. Stop it first." });
413
+ if (startInFlight) return startInFlight;
414
+ startInFlight = doStartGatewayServer(req).finally(() => {
415
+ startInFlight = null;
416
+ });
417
+ return startInFlight;
418
+ }
419
+ async function doStartGatewayServer(req) {
420
+ if (req.listenMode !== "local" && req.listenMode !== "network") {
421
+ return { ok: false, error: "Invalid listen mode." };
422
+ }
423
+ const apiKey = await resolveServerUpstreamApiKey();
424
+ if (!apiKey) {
425
+ return { ok: false, error: "No providers configured. Add a provider in Providers & Keys first." };
426
+ }
427
+ let serverPassword = null;
428
+ if (req.listenMode === "network") {
429
+ if (req.passwordMode === "saved") {
430
+ const saved = await getSavedServerPassword();
431
+ if (!saved) return { ok: false, error: "No saved password found \u2014 enter a new password." };
432
+ serverPassword = saved;
433
+ } else {
434
+ const trimmed = (req.password ?? "").trim();
435
+ if (!trimmed) return { ok: false, error: "A server password is required for network mode." };
436
+ serverPassword = trimmed;
437
+ if (req.savePassword) {
438
+ await setSavedServerPassword(trimmed);
439
+ hasSavedPasswordCache = { value: true, expiresAt: Date.now() + SAVED_PASSWORD_CACHE_TTL_MS };
440
+ }
441
+ }
442
+ }
443
+ let models;
444
+ try {
445
+ models = await loadServerModels();
446
+ } catch (err) {
447
+ return { ok: false, error: `Failed to load models: ${err instanceof Error ? err.message : String(err)}` };
448
+ }
449
+ if (req.exposedProviders) models = filterServerModelsByProviders(models, req.exposedProviders);
450
+ if (req.favoritesOnly) {
451
+ const favorites = loadPreferences().favoriteModels ?? [];
452
+ if (favorites.length === 0) {
453
+ return { ok: false, error: "No favorite models configured. Add favorites in the Favorites tab first." };
454
+ }
455
+ models = filterServerModelsByFavorites(models, favorites).slice(0, MAX_MODEL_CATALOG);
456
+ if (models.length === 0) {
457
+ return { ok: false, error: "No favorite models matched the current provider filter." };
458
+ }
459
+ }
460
+ if (req.freeModelsOnly) {
461
+ models = filterServerModelsByFreeStatus(models);
462
+ if (models.length === 0) {
463
+ return { ok: false, error: "No free models matched the current server filters." };
464
+ }
465
+ }
466
+ if (models.length === 0) {
467
+ return { ok: false, error: "No models to expose. Add providers or adjust the exposed-provider filter." };
468
+ }
469
+ setServerFavoritesOnly(req.favoritesOnly);
470
+ setServerFreeModelsOnly(req.freeModelsOnly);
471
+ if (req.exposedProviders) setServerExposedProviders(req.exposedProviders);
472
+ setServerMaskGatewayIds(req.maskGatewayIds);
473
+ setServerListenMode(req.listenMode);
474
+ const host = req.listenMode === "network" ? "0.0.0.0" : "127.0.0.1";
475
+ const gateway = req.maskGatewayIds ? { maskGatewayIds: true } : void 0;
476
+ let handle;
477
+ try {
478
+ handle = await startServer({
479
+ host,
480
+ port: 17645,
481
+ apiKey,
482
+ serverPassword,
483
+ catalog: createGatewayModelCatalog(models, gateway),
484
+ backends: BACKENDS,
485
+ gateway
486
+ });
487
+ } catch (err) {
488
+ const code = err?.code;
489
+ const message = code === "EADDRINUSE" ? "Port 17645 is already in use \u2014 stop the other anygate server instance first." : `Failed to start server: ${err instanceof Error ? err.message : String(err)}`;
490
+ return { ok: false, error: message };
491
+ }
492
+ running = {
493
+ handle,
494
+ serverPassword,
495
+ config: {
496
+ favoritesOnly: req.favoritesOnly,
497
+ freeModelsOnly: req.freeModelsOnly,
498
+ exposedProviders: req.exposedProviders,
499
+ maskGatewayIds: req.maskGatewayIds,
500
+ listenMode: req.listenMode
501
+ },
502
+ providerSummary: summarizeServerProviders(models),
503
+ modelRows: buildModelRows(models, gateway)
504
+ };
505
+ return { ok: true, status: await getServerStatus() };
506
+ }
507
+ async function stopGatewayServer() {
508
+ if (running) {
509
+ await running.handle.close();
510
+ running = null;
511
+ return { ok: true, stopped: true };
512
+ }
513
+ return { ok: true, stopped: false };
514
+ }
515
+
516
+ // src/ui/api.ts
517
+ var execAsync = promisify(exec);
518
+ var MODELS_TIMEOUT_MS = 3e4;
519
+ var oauthSessions = /* @__PURE__ */ new Map();
520
+ async function fetchModelsWithTimeout(opts) {
521
+ const timeout = new Promise(
522
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), MODELS_TIMEOUT_MS)
523
+ );
524
+ return Promise.race([fetchProviderCatalog(opts), timeout]);
525
+ }
526
+ function sendCatalogFetchError(res, err, label) {
527
+ const isTimeout = String(err).includes("timeout");
528
+ sendJson(res, isTimeout ? 504 : 500, { error: isTimeout ? `${label} timed out` : String(err) });
529
+ }
530
+ function isLoopbackOrigin(origin) {
531
+ if (!origin) return false;
532
+ try {
533
+ const hostname = new URL(origin).hostname;
534
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
535
+ } catch {
536
+ return false;
537
+ }
538
+ }
539
+ function sendCors(req, res) {
540
+ const origin = req.headers.origin;
541
+ const originValue = Array.isArray(origin) ? origin[0] : origin;
542
+ if (isLoopbackOrigin(originValue)) {
543
+ res.setHeader("Access-Control-Allow-Origin", originValue);
544
+ }
545
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
546
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
547
+ }
548
+ function traceUi(opts, message) {
549
+ if (!opts?.trace || !opts.traceLogPath) return;
550
+ writeSecureLogLine(opts.traceLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
551
+ }
552
+ function notifyServerLifecycle(opts, event) {
553
+ try {
554
+ opts.onServerLifecycle?.(event);
555
+ } catch {
556
+ }
557
+ }
558
+ function handleUiApiRequest(req, res, opts = {}) {
559
+ sendCors(req, res);
560
+ if (req.method === "OPTIONS") {
561
+ res.writeHead(204);
562
+ res.end();
563
+ return;
564
+ }
565
+ const url = req.url ?? "";
566
+ traceUi(opts, `${req.method ?? "GET"} ${url}`);
567
+ if (url === "/api/config" && req.method === "GET") {
568
+ handleGetConfig(res);
569
+ } else if (url === "/api/update-status" && req.method === "GET") {
570
+ handleGetUpdateStatus(res);
571
+ } else if (url === "/api/config" && req.method === "POST") {
572
+ handlePostConfig(req, res);
573
+ } else if (url === "/api/models" && req.method === "GET") {
574
+ handleGetModels(res);
575
+ } else if (url === "/api/keys" && req.method === "POST") {
576
+ handlePostKeys(req, res);
577
+ } else if (url === "/api/providers/refresh" && req.method === "POST") {
578
+ handleProviderRefresh(req, res);
579
+ } else if (url === "/api/providers/refresh-all" && req.method === "POST") {
580
+ handleRefreshAll(res);
581
+ } else if (url === "/api/providers/templates" && req.method === "GET") {
582
+ handleGetTemplates(res);
583
+ } else if (url === "/api/providers/add" && req.method === "POST") {
584
+ handleAddProvider(req, res);
585
+ } else if (url === "/api/providers/add-custom" && req.method === "POST") {
586
+ handleAddCustomProvider(req, res);
587
+ } else if (url === "/api/providers/delete" && req.method === "POST") {
588
+ handleDeleteProvider(req, res);
589
+ } else if (url === "/api/providers/oauth/start" && req.method === "POST") {
590
+ handleOAuthStart(req, res);
591
+ } else if (url.startsWith("/api/providers/oauth/status") && req.method === "GET") {
592
+ handleOAuthStatus(req, res);
593
+ } else if (url.startsWith("/oauth/callback") && req.method === "GET") {
594
+ handleOAuthCallback(req, res);
595
+ } else if (url === "/api/apps" && req.method === "GET") {
596
+ handleGetApps(res);
597
+ } else if (url === "/api/apps/path" && req.method === "POST") {
598
+ handleSetAppPath(req, res);
599
+ } else if (url === "/api/apps/launch" && req.method === "POST") {
600
+ handleLaunchApp(req, res, opts);
601
+ } else if (url === "/api/apps/browse-folder" && req.method === "POST") {
602
+ handleBrowseFolder(res);
603
+ } else if (url === "/api/server/status" && req.method === "GET") {
604
+ handleGetServerStatus(res);
605
+ } else if (url === "/api/server/providers" && req.method === "GET") {
606
+ handleGetServerProviders(res);
607
+ } else if (url === "/api/server/start" && req.method === "POST") {
608
+ handleStartServer(req, res, opts);
609
+ } else if (url === "/api/server/stop" && req.method === "POST") {
610
+ handleStopServer(res, opts);
611
+ } else {
612
+ sendJson(res, 404, { error: "Not found" });
613
+ }
614
+ }
615
+ async function handleGetUpdateStatus(res) {
616
+ sendJson(res, 200, await checkForUpdates());
617
+ }
618
+ function handleGetConfig(res) {
619
+ const prefs = loadPreferences();
620
+ sendJson(res, 200, {
621
+ favoriteModels: prefs.favoriteModels ?? [],
622
+ antigravityCliFavoriteModels: prefs.antigravityCliFavoriteModels ?? []
623
+ });
624
+ }
625
+ async function handlePostConfig(req, res) {
626
+ try {
627
+ const body = JSON.parse(await readBody(req));
628
+ const update = {};
629
+ if (Array.isArray(body.favoriteModels)) update.favoriteModels = body.favoriteModels;
630
+ if (Array.isArray(body.antigravityCliFavoriteModels)) update.antigravityCliFavoriteModels = body.antigravityCliFavoriteModels;
631
+ if (Object.keys(update).length > 0) savePreferences(update);
632
+ sendJson(res, 200, { ok: true });
633
+ } catch (err) {
634
+ sendJson(res, 400, { error: String(err) });
635
+ }
636
+ }
637
+ async function handleGetModels(res) {
638
+ try {
639
+ const catalog = await fetchModelsWithTimeout();
640
+ const registry = loadRegistry();
641
+ const rawCountById = new Map(registry.providers.map((p2) => [p2.id, p2.modelsCache?.models.length ?? 0]));
642
+ const providers = catalog.map((p2) => ({
643
+ id: p2.id,
644
+ name: p2.name,
645
+ favoriteName: favoriteProviderDisplayName(p2),
646
+ hasKey: Boolean(p2.apiKey),
647
+ freeAccess: !p2.apiKey && (() => {
648
+ const t = registry.providers.find((rp) => rp.id === p2.id)?.templateId ?? p2.id;
649
+ return getTemplateById(t)?.anonymousFreeModels === true;
650
+ })(),
651
+ authType: p2.authType ?? "api",
652
+ modelCount: rawCountById.get(p2.id) ?? p2.models.length,
653
+ models: p2.models.map((m) => ({
654
+ id: m.id,
655
+ name: m.name,
656
+ isFree: m.isFree ?? false,
657
+ freeStatus: m.freeStatus,
658
+ freeLabel: freeStatusLabel(m.freeStatus),
659
+ contextWindow: m.contextWindow,
660
+ cost: m.cost
661
+ }))
662
+ }));
663
+ const materializedIds = new Set(catalog.map((p2) => p2.id));
664
+ for (const rp of registry.providers) {
665
+ if (rp.authType !== "oauth" || !rp.enabled || materializedIds.has(rp.id)) continue;
666
+ const credential = await resolveProviderCredential(rp.id, rp.authRef).catch(() => null);
667
+ if (!credential) continue;
668
+ providers.push({
669
+ id: rp.id,
670
+ name: rp.name,
671
+ favoriteName: favoriteProviderDisplayName({ id: rp.id, name: rp.name, authType: rp.authType }),
672
+ hasKey: true,
673
+ freeAccess: false,
674
+ authType: "oauth",
675
+ modelCount: 0,
676
+ models: []
677
+ });
678
+ }
679
+ sendJson(res, 200, { providers });
680
+ } catch (err) {
681
+ sendCatalogFetchError(res, err, "Model fetch");
682
+ }
683
+ }
684
+ async function handlePostKeys(req, res) {
685
+ try {
686
+ const body = JSON.parse(await readBody(req));
687
+ const { providerId, key } = body;
688
+ if (!providerId || typeof providerId !== "string") {
689
+ sendJson(res, 400, { error: "providerId required" });
690
+ return;
691
+ }
692
+ if (!key || typeof key !== "string" || key.trim().length === 0) {
693
+ sendJson(res, 400, { error: "key must be a non-empty string" });
694
+ return;
695
+ }
696
+ const authRef = `keyring:provider:${providerId}`;
697
+ const saved = await saveProviderCredential(authRef, key.trim());
698
+ if (saved) {
699
+ sendJson(res, 200, { ok: true });
700
+ } else {
701
+ sendJson(res, 500, { error: "Keychain unavailable \u2014 key not saved" });
702
+ }
703
+ } catch (err) {
704
+ sendJson(res, 400, { error: String(err) });
705
+ }
706
+ }
707
+ var CUSTOM_TEMPLATES = [
708
+ { id: "__custom_openai__", name: "Custom OpenAI-compatible", signupUrl: null, authType: "api", custom: true },
709
+ { id: "__custom_anthropic__", name: "Custom Anthropic-compatible", signupUrl: null, authType: "api", custom: true }
710
+ ];
711
+ function handleGetTemplates(res) {
712
+ const registry = loadRegistry();
713
+ const configured = new Set(registry.providers.map((p2) => p2.id));
714
+ const apiTemplates = listAddableTemplates(configured).map((t) => ({
715
+ id: t.id,
716
+ name: t.name,
717
+ signupUrl: t.signupUrl ?? null,
718
+ authType: t.authType,
719
+ anonymousFreeModels: t.anonymousFreeModels ?? false,
720
+ urlPrompt: t.urlPrompt ?? null,
721
+ defaultBaseUrl: t.defaultBaseUrl ?? null,
722
+ apiKeyOptional: t.apiKeyOptional ?? false,
723
+ custom: false
724
+ }));
725
+ const oauthTemplates = listVisibleOAuthTemplates(configured).map((t) => ({
726
+ id: t.id,
727
+ name: t.name,
728
+ signupUrl: t.signupUrl ?? null,
729
+ authType: t.authType,
730
+ subscriptionRisk: t.subscriptionRisk ?? false,
731
+ custom: false
732
+ }));
733
+ sendJson(res, 200, { templates: [...apiTemplates, ...oauthTemplates, ...CUSTOM_TEMPLATES] });
734
+ }
735
+ async function handleAddCustomProvider(req, res) {
736
+ try {
737
+ const body = JSON.parse(await readBody(req));
738
+ const { kind, displayName, baseUrl, apiKey = "", headers } = body;
739
+ if (kind !== "openai" && kind !== "anthropic") {
740
+ sendJson(res, 400, { error: 'kind must be "openai" or "anthropic"' });
741
+ return;
742
+ }
743
+ if (!displayName?.trim()) {
744
+ sendJson(res, 400, { error: "displayName required" });
745
+ return;
746
+ }
747
+ if (!baseUrl?.trim()) {
748
+ sendJson(res, 400, { error: "baseUrl required" });
749
+ return;
750
+ }
751
+ const result = await addCustomEndpointProvider({
752
+ kind,
753
+ displayName: displayName.trim(),
754
+ baseUrl: baseUrl.trim(),
755
+ apiKey: apiKey.trim(),
756
+ allowInsecureLocal: true,
757
+ headers: headers && Object.keys(headers).length > 0 ? headers : void 0
758
+ });
759
+ if (result.added) {
760
+ sendJson(res, 200, { ok: true, name: displayName.trim(), count: result.modelCount ?? 0 });
761
+ } else {
762
+ sendJson(res, 200, { ok: false, error: result.error, hint: result.hint });
763
+ }
764
+ } catch (err) {
765
+ sendJson(res, 500, { error: String(err) });
766
+ }
767
+ }
768
+ async function handleAddProvider(req, res) {
769
+ try {
770
+ const body = JSON.parse(await readBody(req));
771
+ const { templateId, key, baseUrl } = body;
772
+ if (!templateId || typeof templateId !== "string") {
773
+ sendJson(res, 400, { error: "templateId required" });
774
+ return;
775
+ }
776
+ const { listSupportedTemplates } = await import("./provider-templates-75KU6VA6.js");
777
+ const template = listSupportedTemplates().find((t) => t.id === templateId);
778
+ if (!template) {
779
+ sendJson(res, 404, { error: `Template '${templateId}' not found` });
780
+ return;
781
+ }
782
+ const rawKey = typeof key === "string" ? key.trim() : "";
783
+ if (!rawKey && !template.anonymousFreeModels && !template.apiKeyOptional) {
784
+ sendJson(res, 400, { error: "key must be a non-empty string" });
785
+ return;
786
+ }
787
+ const keyText = template.apiKeyOptional && !rawKey && !template.anonymousFreeModels ? template.id : rawKey;
788
+ let baseUrlOverride;
789
+ if (template.urlPrompt) {
790
+ baseUrlOverride = typeof baseUrl === "string" ? baseUrl.trim() : "";
791
+ if (!baseUrlOverride) {
792
+ sendJson(res, 400, { error: "baseUrl required" });
793
+ return;
794
+ }
795
+ const usesHttp = /^http:\/\//i.test(baseUrlOverride);
796
+ const valid = await validateCustomEndpointUrl(baseUrlOverride, { allowInsecureLocal: usesHttp });
797
+ if (!valid.ok) {
798
+ sendJson(res, 400, { error: valid.error ?? "Invalid URL", hint: valid.hint });
799
+ return;
800
+ }
801
+ }
802
+ const result = await addProviderFromTemplate(template, keyText, { baseUrl: baseUrlOverride });
803
+ if (result.added) {
804
+ sendJson(res, 200, { ok: true, name: template.name, count: result.modelCount ?? 0 });
805
+ } else {
806
+ sendJson(res, 200, { ok: false, error: result.error, hint: result.hint });
807
+ }
808
+ } catch (err) {
809
+ sendJson(res, 500, { error: String(err) });
810
+ }
811
+ }
812
+ async function handleRefreshAll(res) {
813
+ try {
814
+ const result = await refreshAllProviderModels(async (provider) => {
815
+ if (!provider.authRef) return null;
816
+ return resolveProviderCredential(provider.id, provider.authRef);
817
+ });
818
+ const summary = result.refreshed.map((r) => {
819
+ const isOAuthExpected = !r.ok && !r.skipped && r.reason?.includes("OAuth token");
820
+ return {
821
+ id: r.id,
822
+ name: r.name,
823
+ ok: r.ok || isOAuthExpected,
824
+ count: r.modelCount ?? r.previousModelCount ?? 0,
825
+ skipped: r.skipped ?? isOAuthExpected,
826
+ oauthWarning: isOAuthExpected,
827
+ reason: r.reason
828
+ };
829
+ });
830
+ sendJson(res, 200, { ok: true, providers: summary, total: summary.reduce((n, p2) => n + p2.count, 0) });
831
+ } catch (err) {
832
+ sendJson(res, 500, { ok: false, error: String(err) });
833
+ }
834
+ }
835
+ async function handleProviderRefresh(req, res) {
836
+ try {
837
+ const body = JSON.parse(await readBody(req));
838
+ const { providerId } = body;
839
+ if (!providerId || typeof providerId !== "string") {
840
+ sendJson(res, 400, { error: "providerId required" });
841
+ return;
842
+ }
843
+ const registry = loadRegistry();
844
+ const registryProvider = registry.providers.find((p2) => p2.id === providerId);
845
+ if (!registryProvider) {
846
+ sendJson(res, 200, { ok: false, error: "Provider not found in registry" });
847
+ return;
848
+ }
849
+ const apiKey = await resolveProviderCredential(providerId, registryProvider.authRef);
850
+ const result = await refreshProviderModels(providerId, apiKey, registry);
851
+ if (result.ok) {
852
+ sendJson(res, 200, { ok: true, count: result.modelCount ?? result.previousModelCount ?? 0 });
853
+ } else {
854
+ sendJson(res, 200, { ok: false, error: result.reason ?? "Refresh failed" });
855
+ }
856
+ } catch (err) {
857
+ sendJson(res, 200, { ok: false, error: String(err) });
858
+ }
859
+ }
860
+ async function handleDeleteProvider(req, res) {
861
+ try {
862
+ const body = JSON.parse(await readBody(req));
863
+ const { providerId } = body;
864
+ if (!providerId || typeof providerId !== "string") {
865
+ sendJson(res, 400, { error: "providerId required" });
866
+ return;
867
+ }
868
+ const result = await removeProviderFromRegistry(providerId);
869
+ if (result.removed) {
870
+ sendJson(res, 200, { ok: true, name: result.name });
871
+ } else {
872
+ sendJson(res, 200, { ok: false, error: result.error ?? "Provider not found" });
873
+ }
874
+ } catch (err) {
875
+ sendJson(res, 500, { error: String(err) });
876
+ }
877
+ }
878
+ var DEVICE_CODE_PROVIDER_IDS = /* @__PURE__ */ new Set(["xai-oauth", "openai-oauth", "github-copilot"]);
879
+ var PKCE_PROVIDER_IDS = /* @__PURE__ */ new Set(["claude-code", "antigravity"]);
880
+ var NATIVE_OAUTH_PROVIDER_IDS = /* @__PURE__ */ new Set([...DEVICE_CODE_PROVIDER_IDS, ...PKCE_PROVIDER_IDS]);
881
+ async function refreshOAuthProviderModels(providerId) {
882
+ const registry = loadRegistry();
883
+ const entry = registry.providers.find((p2) => p2.id === providerId);
884
+ if (!entry) return;
885
+ const apiKey = await resolveProviderCredential(providerId, entry.authRef);
886
+ await refreshProviderModels(providerId, apiKey, registry);
887
+ }
888
+ async function handleOAuthStart(req, res) {
889
+ try {
890
+ const body = JSON.parse(await readBody(req));
891
+ const { providerId } = body;
892
+ if (!providerId || !NATIVE_OAUTH_PROVIDER_IDS.has(providerId)) {
893
+ sendJson(res, 400, { error: `providerId must be one of: ${[...NATIVE_OAUTH_PROVIDER_IDS].join(", ")}` });
894
+ return;
895
+ }
896
+ const sessionId = randomUUID();
897
+ if (providerId === "xai-oauth") {
898
+ const device2 = await requestXaiDeviceCode();
899
+ const url2 = device2.verification_uri_complete ?? device2.verification_uri;
900
+ const session2 = { status: "pending", url: url2, userCode: device2.user_code, providerId };
901
+ oauthSessions.set(sessionId, session2);
902
+ pollXaiDeviceCodeToken(device2).then(async (tokens) => {
903
+ await saveNativeOAuthCredential(providerId, tokens);
904
+ await refreshOAuthProviderModels(providerId);
905
+ oauthSessions.set(sessionId, { ...session2, status: "done" });
906
+ }).catch((err) => {
907
+ oauthSessions.set(sessionId, { ...session2, status: "error", error: String(err) });
908
+ });
909
+ sendJson(res, 200, { sessionId, url: url2, userCode: device2.user_code });
910
+ return;
911
+ }
912
+ if (providerId === "github-copilot") {
913
+ const device2 = await requestGithubDeviceCode();
914
+ const url2 = device2.verification_uri;
915
+ const session2 = { status: "pending", url: url2, userCode: device2.user_code, providerId };
916
+ oauthSessions.set(sessionId, session2);
917
+ pollGithubDeviceCodeToken(device2).then(async (tokens) => {
918
+ await saveNativeOAuthCredential(providerId, tokens);
919
+ await refreshOAuthProviderModels(providerId);
920
+ oauthSessions.set(sessionId, { ...session2, status: "done" });
921
+ }).catch((err) => {
922
+ oauthSessions.set(sessionId, { ...session2, status: "error", error: String(err) });
923
+ });
924
+ sendJson(res, 200, { sessionId, url: url2, userCode: device2.user_code });
925
+ return;
926
+ }
927
+ if (PKCE_PROVIDER_IDS.has(providerId)) {
928
+ if (providerId === "claude-code") {
929
+ sendJson(res, 400, {
930
+ error: "Claude Code OAuth must be completed in the terminal: anygate providers auth claude-code"
931
+ });
932
+ return;
933
+ }
934
+ const host = req.headers.host ?? "127.0.0.1";
935
+ const redirectUri = guiCallbackRedirectUri(host);
936
+ let pkce;
937
+ if (providerId === "antigravity") {
938
+ pkce = await buildAntigravityAuthUrl(redirectUri);
939
+ } else {
940
+ sendJson(res, 400, { error: `PKCE flow for "${providerId}" not yet implemented` });
941
+ return;
942
+ }
943
+ const { authUrl, codeVerifier, oauthState } = pkce;
944
+ const session2 = {
945
+ status: "pending",
946
+ url: authUrl,
947
+ providerId,
948
+ codeVerifier,
949
+ oauthState
950
+ };
951
+ oauthSessions.set(sessionId, session2);
952
+ const codePromise = new Promise((resolve, reject) => {
953
+ session2.codeResolver = resolve;
954
+ session2.errorRejecter = (err) => reject(new Error(err));
955
+ setTimeout(() => reject(new Error("OAuth timeout \u2014 sign-in not completed")), 10 * 60 * 1e3);
956
+ });
957
+ oauthSessions.set(sessionId, session2);
958
+ codePromise.then(async (code) => {
959
+ let providerData = {};
960
+ let accountId;
961
+ let tokens;
962
+ if (providerId === "antigravity") {
963
+ const result = await completeAntigravityExchange(code, codeVerifier, redirectUri);
964
+ tokens = result.tokens;
965
+ accountId = result.userInfo.email;
966
+ if (result.projectId) providerData.projectId = result.projectId;
967
+ if (result.tierId) providerData.tier = result.tierId;
968
+ } else {
969
+ throw new Error(`Unknown PKCE provider: ${providerId}`);
970
+ }
971
+ await saveNativeOAuthCredential(providerId, tokens, accountId, providerData);
972
+ await refreshOAuthProviderModels(providerId);
973
+ oauthSessions.set(sessionId, { ...session2, status: "done" });
974
+ }).catch((err) => {
975
+ oauthSessions.set(sessionId, { ...session2, status: "error", error: String(err) });
976
+ });
977
+ sendJson(res, 200, { sessionId, authUrl, pkce: true });
978
+ return;
979
+ }
980
+ const device = await requestOpenAiDeviceCode();
981
+ const url = openAiDeviceCodeUrl();
982
+ const session = { status: "pending", url, userCode: device.user_code, providerId };
983
+ oauthSessions.set(sessionId, session);
984
+ pollOpenAiDeviceCodeToken(device).then(async ({ tokens, accountId }) => {
985
+ await saveNativeOAuthCredential(providerId, tokens, accountId);
986
+ await refreshOAuthProviderModels(providerId);
987
+ oauthSessions.set(sessionId, { ...session, status: "done" });
988
+ }).catch((err) => {
989
+ oauthSessions.set(sessionId, { ...session, status: "error", error: String(err) });
990
+ });
991
+ sendJson(res, 200, { sessionId, url, userCode: device.user_code });
992
+ } catch (err) {
993
+ sendJson(res, 500, { error: String(err) });
994
+ }
995
+ }
996
+ function handleOAuthStatus(req, res) {
997
+ const searchParams = new URL(req.url ?? "", "http://localhost").searchParams;
998
+ const sessionId = searchParams.get("sessionId") ?? "";
999
+ const session = oauthSessions.get(sessionId);
1000
+ if (!session) {
1001
+ sendJson(res, 404, { error: "Session not found or expired" });
1002
+ return;
1003
+ }
1004
+ sendJson(res, 200, { status: session.status, error: session.error });
1005
+ if (session.status !== "pending") oauthSessions.delete(sessionId);
1006
+ }
1007
+ function escapeHtml(s) {
1008
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1009
+ }
1010
+ function callbackPage(type, message) {
1011
+ const icon = type === "success" ? "&#10003;" : "&#10007;";
1012
+ const color = type === "success" ? "#22c55e" : "#ef4444";
1013
+ const title = type === "success" ? "Authentication successful" : "Authentication failed";
1014
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${title}</title></head>
1015
+ <body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
1016
+ <div style="text-align:center;padding:2rem;background:#fff;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1);max-width:400px">
1017
+ <div style="color:${color};font-size:2.5rem">${icon}</div>
1018
+ <h1 style="margin:.5rem 0">${title}</h1>
1019
+ <p style="color:#666">${escapeHtml(message)}</p>
1020
+ </div></body></html>`;
1021
+ }
1022
+ function handleOAuthCallback(req, res) {
1023
+ const sp = new URL(req.url ?? "", "http://localhost").searchParams;
1024
+ const code = sp.get("code") ?? "";
1025
+ const state = sp.get("state") ?? "";
1026
+ const error = sp.get("error") ?? "";
1027
+ let matchedSession;
1028
+ for (const session of oauthSessions.values()) {
1029
+ if (session.oauthState === state) {
1030
+ matchedSession = session;
1031
+ break;
1032
+ }
1033
+ }
1034
+ if (!matchedSession) {
1035
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
1036
+ res.end(callbackPage("error", "Unknown or expired OAuth session. Please try signing in again."));
1037
+ return;
1038
+ }
1039
+ if (error) {
1040
+ matchedSession.errorRejecter?.(error);
1041
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1042
+ res.end(callbackPage("error", `Authorization denied: ${error}`));
1043
+ return;
1044
+ }
1045
+ if (!code) {
1046
+ matchedSession.errorRejecter?.("No authorization code received");
1047
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
1048
+ res.end(callbackPage("error", "No authorization code received. Please try again."));
1049
+ return;
1050
+ }
1051
+ matchedSession.codeResolver?.(code);
1052
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1053
+ res.end(callbackPage("success", "You can close this tab and return to anygate."));
1054
+ }
1055
+ function handleGetApps(res) {
1056
+ try {
1057
+ const apps = getSupportedApps();
1058
+ sendJson(res, 200, { apps, recentLaunchFolders: loadPreferences().recentLaunchFolders ?? [] });
1059
+ } catch (err) {
1060
+ sendJson(res, 500, { error: String(err) });
1061
+ }
1062
+ }
1063
+ var AGY_APP_IDS = /* @__PURE__ */ new Set(["antigravity", "agy", "antigravity-ide"]);
1064
+ async function handleLaunchApp(req, res, opts) {
1065
+ try {
1066
+ const body = JSON.parse(await readBody(req));
1067
+ const { appId, favorites, cwd } = body;
1068
+ let { providerId, modelId } = body;
1069
+ if (!appId) {
1070
+ sendJson(res, 400, { error: "Missing appId" });
1071
+ return;
1072
+ }
1073
+ if (!getSupportedApp(appId)) {
1074
+ sendJson(res, 400, { error: `Unknown app: ${appId}` });
1075
+ return;
1076
+ }
1077
+ const { installed, path } = detectApp(appId);
1078
+ if (!installed || !path) {
1079
+ sendJson(res, 400, { error: `App ${appId} is not installed on this system.` });
1080
+ return;
1081
+ }
1082
+ if (!favorites && (providerId || modelId) && (!providerId || !modelId)) {
1083
+ sendJson(res, 400, { error: "Both providerId and modelId are required to launch a specific anygate model." });
1084
+ return;
1085
+ }
1086
+ if (favorites && !providerId && !modelId) {
1087
+ const prefs = loadPreferences();
1088
+ const favList = AGY_APP_IDS.has(appId) ? prefs.antigravityCliFavoriteModels ?? [] : prefs.favoriteModels ?? [];
1089
+ if (favList.length > 0) {
1090
+ providerId = favList[0].providerId;
1091
+ modelId = favList[0].modelId;
1092
+ }
1093
+ }
1094
+ const launchFolder = typeof cwd === "string" && cwd.trim() ? cwd.trim() : void 0;
1095
+ if (launchFolder) {
1096
+ try {
1097
+ if (!statSync(launchFolder).isDirectory()) {
1098
+ sendJson(res, 400, { error: "Launch folder must be a directory." });
1099
+ return;
1100
+ }
1101
+ } catch {
1102
+ sendJson(res, 400, { error: "Launch folder does not exist." });
1103
+ return;
1104
+ }
1105
+ recordLaunchFolder(launchFolder);
1106
+ }
1107
+ const launchCmd = getGatewayLaunchCommand(appId, {
1108
+ providerId,
1109
+ modelId,
1110
+ cwd: launchFolder,
1111
+ trace: opts.trace
1112
+ });
1113
+ traceUi(
1114
+ opts,
1115
+ `launch app=${appId} provider=${providerId ?? ""} model=${modelId ?? ""} favorites=${Boolean(favorites)} resolved-from-favorites=${Boolean(favorites && providerId)} cwd=${launchFolder ?? ""} command=${launchCmd}`
1116
+ );
1117
+ exec(launchCmd, (err) => {
1118
+ if (err) {
1119
+ traceUi(opts, `launch error app=${appId} error=${err.message}`);
1120
+ console.error("Failed to spawn native terminal window:", err);
1121
+ }
1122
+ });
1123
+ sendJson(res, 200, { ok: true, command: launchCmd });
1124
+ } catch (err) {
1125
+ sendJson(res, 500, { error: String(err) });
1126
+ }
1127
+ }
1128
+ async function handleSetAppPath(req, res) {
1129
+ try {
1130
+ const body = JSON.parse(await readBody(req));
1131
+ const { appId, path } = body;
1132
+ if (!appId || typeof appId !== "string") {
1133
+ sendJson(res, 400, { error: "Missing appId" });
1134
+ return;
1135
+ }
1136
+ if (path !== null && (typeof path !== "string" || !path.trim())) {
1137
+ sendJson(res, 400, { error: "path must be a non-empty string, or null to clear the override." });
1138
+ return;
1139
+ }
1140
+ const trimmed = typeof path === "string" ? path.trim() : null;
1141
+ if (trimmed && !existsSync2(trimmed)) {
1142
+ sendJson(res, 400, { error: "That path does not exist." });
1143
+ return;
1144
+ }
1145
+ setAppPathOverride(appId, trimmed);
1146
+ sendJson(res, 200, { ok: true, apps: getSupportedApps() });
1147
+ } catch (err) {
1148
+ sendJson(res, 500, { error: String(err) });
1149
+ }
1150
+ }
1151
+ async function handleGetServerStatus(res) {
1152
+ try {
1153
+ sendJson(res, 200, await getServerStatus());
1154
+ } catch (err) {
1155
+ sendJson(res, 500, { error: String(err) });
1156
+ }
1157
+ }
1158
+ async function handleGetServerProviders(res) {
1159
+ try {
1160
+ const catalog = await fetchModelsWithTimeout({ agent: "server" });
1161
+ sendJson(res, 200, { providers: providerOptionsFromCatalog(catalog) });
1162
+ } catch (err) {
1163
+ sendCatalogFetchError(res, err, "Provider fetch");
1164
+ }
1165
+ }
1166
+ async function handleStartServer(req, res, opts) {
1167
+ try {
1168
+ const body = JSON.parse(await readBody(req));
1169
+ if (typeof body.favoritesOnly !== "boolean") {
1170
+ sendJson(res, 400, { error: "favoritesOnly must be a boolean" });
1171
+ return;
1172
+ }
1173
+ if (typeof body.maskGatewayIds !== "boolean") {
1174
+ sendJson(res, 400, { error: "maskGatewayIds must be a boolean" });
1175
+ return;
1176
+ }
1177
+ if (body.listenMode !== "local" && body.listenMode !== "network") {
1178
+ sendJson(res, 400, { error: 'listenMode must be "local" or "network"' });
1179
+ return;
1180
+ }
1181
+ const request = {
1182
+ favoritesOnly: body.favoritesOnly,
1183
+ freeModelsOnly: Boolean(body.freeModelsOnly),
1184
+ exposedProviders: Array.isArray(body.exposedProviders) ? body.exposedProviders : null,
1185
+ maskGatewayIds: body.maskGatewayIds,
1186
+ listenMode: body.listenMode,
1187
+ passwordMode: body.passwordMode === "saved" ? "saved" : "new",
1188
+ password: typeof body.password === "string" ? body.password : void 0,
1189
+ savePassword: Boolean(body.savePassword)
1190
+ };
1191
+ const result = await startGatewayServer(request);
1192
+ if (result.ok) {
1193
+ notifyServerLifecycle(opts, {
1194
+ type: "started",
1195
+ listenMode: request.listenMode,
1196
+ modelCount: result.status.models?.length ?? 0
1197
+ });
1198
+ }
1199
+ sendJson(res, 200, result);
1200
+ } catch (err) {
1201
+ sendJson(res, 500, { ok: false, error: String(err) });
1202
+ }
1203
+ }
1204
+ async function handleStopServer(res, opts) {
1205
+ try {
1206
+ const result = await stopGatewayServer();
1207
+ if (result.stopped) notifyServerLifecycle(opts, { type: "stopped" });
1208
+ sendJson(res, 200, result);
1209
+ } catch (err) {
1210
+ sendJson(res, 500, { error: String(err) });
1211
+ }
1212
+ }
1213
+ async function handleBrowseFolder(res) {
1214
+ try {
1215
+ let resultPath = "";
1216
+ const isMac2 = process.platform === "darwin";
1217
+ const isWindows2 = process.platform === "win32";
1218
+ if (isMac2) {
1219
+ const script = 'POSIX path of (choose folder with prompt "Select launch folder:")';
1220
+ try {
1221
+ const { stdout } = await execAsync(`osascript -e '${script}'`);
1222
+ resultPath = stdout.trim();
1223
+ } catch (err) {
1224
+ if (err.code === 1 || String(err.stderr).includes("-128") || String(err.stdout).includes("-128")) {
1225
+ sendJson(res, 200, { ok: true, canceled: true });
1226
+ return;
1227
+ }
1228
+ throw err;
1229
+ }
1230
+ } else if (isWindows2) {
1231
+ const psCommand = [
1232
+ "try {",
1233
+ " Add-Type -AssemblyName System.Windows.Forms",
1234
+ " $f = New-Object System.Windows.Forms.FolderBrowserDialog",
1235
+ ' $f.Description = "Select launch folder"',
1236
+ " $owner = New-Object System.Windows.Forms.Form",
1237
+ " $owner.TopMost = $true",
1238
+ " $owner.ShowInTaskbar = $false",
1239
+ ' $owner.FormBorderStyle = "None"',
1240
+ " $owner.Opacity = 0",
1241
+ " $owner.Width = 1",
1242
+ " $owner.Height = 1",
1243
+ ' $owner.StartPosition = "CenterScreen"',
1244
+ " $owner.Show()",
1245
+ " $owner.Activate()",
1246
+ ' if ($f.ShowDialog($owner) -eq "OK") { $f.SelectedPath }',
1247
+ " $owner.Close()",
1248
+ "} catch {",
1249
+ " [Console]::Error.WriteLine($_.Exception.Message)",
1250
+ " exit 1",
1251
+ "}"
1252
+ ].join("\n");
1253
+ try {
1254
+ const encoded = Buffer.from(psCommand, "utf16le").toString("base64");
1255
+ const { stdout } = await execAsync(`powershell -NoProfile -Sta -EncodedCommand ${encoded}`);
1256
+ resultPath = stdout.trim();
1257
+ } catch (err) {
1258
+ sendJson(res, 500, { error: `Failed to open folder picker: ${err instanceof Error ? err.message : String(err)}` });
1259
+ return;
1260
+ }
1261
+ } else {
1262
+ try {
1263
+ const { stdout } = await execAsync('zenity --file-selection --directory --title="Select launch folder"');
1264
+ resultPath = stdout.trim();
1265
+ } catch {
1266
+ try {
1267
+ const { stdout } = await execAsync("kdialog --getexistingdirectory .");
1268
+ resultPath = stdout.trim();
1269
+ } catch {
1270
+ sendJson(res, 500, { error: "No GUI folder picker available on this platform" });
1271
+ return;
1272
+ }
1273
+ }
1274
+ }
1275
+ if (!resultPath) {
1276
+ sendJson(res, 200, { ok: true, canceled: true });
1277
+ return;
1278
+ }
1279
+ sendJson(res, 200, { ok: true, path: resultPath });
1280
+ } catch (err) {
1281
+ sendJson(res, 500, { error: String(err) });
1282
+ }
1283
+ }
1284
+
1285
+ // src/ui/command.ts
1286
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1287
+ var PUBLIC_DIR = join2(__dirname, "ui", "public");
1288
+ var LOCK_FILE = join2(getAppHome(), "ui.lock");
1289
+ var MIME = {
1290
+ ".html": "text/html; charset=utf-8",
1291
+ ".js": "application/javascript; charset=utf-8",
1292
+ ".css": "text/css; charset=utf-8",
1293
+ ".png": "image/png",
1294
+ ".webp": "image/webp",
1295
+ ".svg": "image/svg+xml"
1296
+ };
1297
+ function ext(path) {
1298
+ const i = path.lastIndexOf(".");
1299
+ return i >= 0 ? path.slice(i) : "";
1300
+ }
1301
+ function buildStaticCache() {
1302
+ const cache = /* @__PURE__ */ new Map();
1303
+ try {
1304
+ for (const name of readdirSync(PUBLIC_DIR)) {
1305
+ const mime = MIME[ext(name)];
1306
+ if (!mime) continue;
1307
+ const raw = readFileSync(join2(PUBLIC_DIR, name));
1308
+ const content = name === "index.html" ? Buffer.from(raw.toString("utf8").replace("{{VERSION}}", VERSION)) : raw;
1309
+ cache.set(`/${name}`, { content, mime });
1310
+ }
1311
+ } catch {
1312
+ }
1313
+ return cache;
1314
+ }
1315
+ function removeLock() {
1316
+ try {
1317
+ unlinkSync(LOCK_FILE);
1318
+ } catch {
1319
+ }
1320
+ }
1321
+ function checkExistingServer() {
1322
+ if (!existsSync3(LOCK_FILE)) return null;
1323
+ try {
1324
+ const { pid, port } = JSON.parse(readFileSync(LOCK_FILE, "utf8"));
1325
+ process.kill(pid, 0);
1326
+ return `http://127.0.0.1:${port}`;
1327
+ } catch {
1328
+ removeLock();
1329
+ return null;
1330
+ }
1331
+ }
1332
+ function isUiApiRoute(url) {
1333
+ return url.startsWith("/api/") || url.startsWith("/oauth/callback");
1334
+ }
1335
+ function formatUiServerLifecycleMessage(event) {
1336
+ if (event.type === "stopped") return "\u25C7 Server Gateway stopped";
1337
+ const mode = event.listenMode === "network" ? "Network" : "Local";
1338
+ const modelLabel = event.modelCount === 1 ? "model" : "models";
1339
+ return `\u25C6 Server Gateway started \xB7 ${mode} mode \xB7 ${event.modelCount} ${modelLabel} exposed`;
1340
+ }
1341
+ async function resolveUiShutdownDecision(signal, promptClose = () => p.confirm({
1342
+ message: "anygate UI is still running. Close it?",
1343
+ initialValue: true
1344
+ })) {
1345
+ if (signal !== "SIGINT") return "close";
1346
+ const shouldClose = await promptClose();
1347
+ if (p.isCancel(shouldClose)) return "close";
1348
+ return shouldClose ? "close" : "keep";
1349
+ }
1350
+ async function runUiCommand(opts = {}) {
1351
+ const existing = checkExistingServer();
1352
+ if (existing) {
1353
+ console.log(`
1354
+ ${pc.bold("anygate UI")} already running at ${pc.cyan(existing)}
1355
+ `);
1356
+ return 0;
1357
+ }
1358
+ if (opts.trace) {
1359
+ process.env.ANYGATE_TRACE = "1";
1360
+ }
1361
+ const staticCache = buildStaticCache();
1362
+ const traceLogPath = opts.trace ? getUiDebugLogPath() : void 0;
1363
+ const trace = traceLogPath ? makeTraceLogger(traceLogPath) : void 0;
1364
+ trace?.("ui server starting");
1365
+ const server = createServer((req, res) => {
1366
+ const url2 = req.url ?? "/";
1367
+ res.setHeader("X-Content-Type-Options", "nosniff");
1368
+ if (isUiApiRoute(url2)) {
1369
+ handleUiApiRequest(req, res, {
1370
+ trace: opts.trace,
1371
+ traceLogPath,
1372
+ onServerLifecycle: (event) => {
1373
+ console.log(`
1374
+ ${formatUiServerLifecycleMessage(event)}
1375
+ `);
1376
+ }
1377
+ });
1378
+ return;
1379
+ }
1380
+ const key = url2 === "/" ? "/index.html" : url2.split("?")[0];
1381
+ trace?.(`static ${req.method ?? "GET"} ${url2} -> ${key}`);
1382
+ const cached = staticCache.get(key);
1383
+ if (cached) {
1384
+ res.writeHead(200, { "Content-Type": cached.mime });
1385
+ res.end(cached.content);
1386
+ return;
1387
+ }
1388
+ res.writeHead(404);
1389
+ res.end("Not found");
1390
+ });
1391
+ await new Promise((resolve, reject) => {
1392
+ server.listen(0, "127.0.0.1", () => resolve());
1393
+ server.once("error", reject);
1394
+ });
1395
+ const addr = server.address();
1396
+ if (!addr || typeof addr === "string") {
1397
+ console.error("Failed to bind server");
1398
+ return 1;
1399
+ }
1400
+ const port = addr.port;
1401
+ const url = `http://127.0.0.1:${port}`;
1402
+ mkdirSync(getAppHome(), { recursive: true });
1403
+ writeFileSync2(LOCK_FILE, JSON.stringify({ pid: process.pid, port }));
1404
+ const cleanup = () => {
1405
+ removeLock();
1406
+ server.close();
1407
+ process.exit(0);
1408
+ };
1409
+ let handlingSignal = false;
1410
+ const handleSignal = async (signal) => {
1411
+ if (handlingSignal) return;
1412
+ handlingSignal = true;
1413
+ const decision = await resolveUiShutdownDecision(signal);
1414
+ if (decision === "keep") {
1415
+ handlingSignal = false;
1416
+ return;
1417
+ }
1418
+ cleanup();
1419
+ };
1420
+ process.on("SIGINT", () => {
1421
+ void handleSignal("SIGINT");
1422
+ });
1423
+ process.on("SIGTERM", () => {
1424
+ void handleSignal("SIGTERM");
1425
+ });
1426
+ console.log(`
1427
+ ${pc.bold("anygate UI")} ${pc.cyan(url)}
1428
+ ${pc.dim("Press Ctrl+C to stop")}
1429
+ `);
1430
+ if (traceLogPath) {
1431
+ console.log(` ${pc.dim(`Trace log: ${traceLogPath}`)}
1432
+ `);
1433
+ trace?.(`ui server listening ${url}`);
1434
+ }
1435
+ try {
1436
+ const { default: open } = await import("open");
1437
+ await open(url);
1438
+ trace?.(`browser open ${url}`);
1439
+ } catch {
1440
+ trace?.(`browser open failed ${url}`);
1441
+ }
1442
+ await new Promise(() => {
1443
+ });
1444
+ return 0;
1445
+ }
1446
+ export {
1447
+ formatUiServerLifecycleMessage,
1448
+ isUiApiRoute,
1449
+ resolveUiShutdownDecision,
1450
+ runUiCommand
1451
+ };
1452
+ //# sourceMappingURL=command-SVHKYXNL.js.map