bosun 0.41.7 → 0.41.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +23 -1
  2. package/agent/agent-event-bus.mjs +31 -2
  3. package/agent/agent-pool.mjs +251 -11
  4. package/agent/agent-prompts.mjs +5 -1
  5. package/agent/agent-supervisor.mjs +22 -0
  6. package/agent/primary-agent.mjs +115 -5
  7. package/cli.mjs +3 -2
  8. package/config/config.mjs +4 -1
  9. package/desktop/main.mjs +350 -25
  10. package/desktop/preload.cjs +8 -0
  11. package/desktop/preload.mjs +19 -0
  12. package/entrypoint.mjs +332 -0
  13. package/infra/health-status.mjs +72 -0
  14. package/infra/library-manager.mjs +58 -1
  15. package/infra/maintenance.mjs +1 -2
  16. package/infra/monitor.mjs +25 -7
  17. package/infra/session-tracker.mjs +30 -3
  18. package/package.json +10 -4
  19. package/server/bosun-mcp-server.mjs +1004 -0
  20. package/server/setup-web-server.mjs +287 -258
  21. package/server/ui-server.mjs +218 -23
  22. package/shell/claude-shell.mjs +14 -1
  23. package/shell/codex-model-profiles.mjs +166 -29
  24. package/shell/codex-shell.mjs +56 -18
  25. package/shell/opencode-providers.mjs +20 -8
  26. package/task/task-executor.mjs +28 -0
  27. package/task/task-store.mjs +13 -4
  28. package/tools/list-todos.mjs +7 -1
  29. package/ui/app.js +3 -2
  30. package/ui/components/agent-selector.js +127 -0
  31. package/ui/components/session-list.js +2 -0
  32. package/ui/demo-defaults.js +6 -6
  33. package/ui/modules/router.js +2 -0
  34. package/ui/modules/state.js +13 -5
  35. package/ui/tabs/chat.js +3 -0
  36. package/ui/tabs/library.js +284 -52
  37. package/ui/tabs/tasks.js +5 -13
  38. package/workflow/workflow-engine.mjs +16 -4
  39. package/workflow/workflow-nodes/definitions.mjs +37 -0
  40. package/workflow/workflow-nodes.mjs +489 -153
  41. package/workflow/workflow-templates.mjs +0 -5
  42. package/workflow-templates/github.mjs +106 -16
  43. package/workspace/worktree-manager.mjs +1 -1
@@ -69,11 +69,16 @@ function buildModelsProbeRequest({ apiKey = "", baseUrl = "" } = {}) {
69
69
  return { endpoint: parsed.toString(), headers };
70
70
  }
71
71
 
72
+ if (isAzure && (lowerPath === "/openai/v1" || lowerPath.startsWith("/openai/v1/"))) {
73
+ parsed.pathname = "/openai/v1/models";
74
+ parsed.search = "";
75
+ return { endpoint: parsed.toString(), headers };
76
+ }
77
+
72
78
  if (isAzure || lowerPath === "/openai" || lowerPath.startsWith("/openai/")) {
73
79
  parsed.pathname = "/openai/models";
74
- if (!parsed.searchParams.has("api-version")) {
75
- parsed.searchParams.set("api-version", "2024-10-21");
76
- }
80
+ parsed.search = "";
81
+ parsed.searchParams.set("api-version", "2024-10-21");
77
82
  return { endpoint: parsed.toString(), headers };
78
83
  }
79
84
 
@@ -2438,6 +2443,281 @@ function handleApply(body) {
2438
2443
 
2439
2444
  // ── Server ───────────────────────────────────────────────────────────────────
2440
2445
 
2446
+ /**
2447
+ * Handle /api/setup/* routes.
2448
+ * Reusable from both the standalone setup server and the unified ui-server.
2449
+ *
2450
+ * @param {import("node:http").IncomingMessage} req
2451
+ * @param {import("node:http").ServerResponse} res
2452
+ * @param {URL} url
2453
+ * @param {object} [options]
2454
+ * @param {() => void} [options.onComplete] - callback for unified mode (instead of process.exit)
2455
+ * @returns {Promise<boolean>} true if the route was handled
2456
+ */
2457
+ async function handleSetupApi(req, res, url, options = {}) {
2458
+ if (!url.pathname.startsWith("/api/setup/")) return false;
2459
+
2460
+ const route = url.pathname.replace("/api/setup/", "");
2461
+
2462
+ try {
2463
+ switch (route) {
2464
+ case "status":
2465
+ jsonResponse(res, 200, await handleStatus());
2466
+ return true;
2467
+ case "vendor-status":
2468
+ jsonResponse(res, 200, checkVendorFiles());
2469
+ return true;
2470
+ case "prerequisites":
2471
+ jsonResponse(res, 200, handlePrerequisites());
2472
+ return true;
2473
+ case "defaults":
2474
+ jsonResponse(res, 200, handleDefaults());
2475
+ return true;
2476
+ case "models":
2477
+ jsonResponse(res, 200, handleModels());
2478
+ return true;
2479
+ case "models/probe":
2480
+ if (req.method !== "POST") {
2481
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2482
+ return true;
2483
+ }
2484
+ jsonResponse(res, 200, await handleModelsProbe(await readBody(req)));
2485
+ return true;
2486
+ case "executors":
2487
+ jsonResponse(res, 200, handleExecutors());
2488
+ return true;
2489
+ case "workflows":
2490
+ jsonResponse(res, 200, handleWorkflowTemplates());
2491
+ return true;
2492
+ case "voice/endpoints/test":
2493
+ if (req.method !== "POST") {
2494
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2495
+ return true;
2496
+ }
2497
+ jsonResponse(res, 200, await handleVoiceEndpointTest(await readBody(req)));
2498
+ return true;
2499
+
2500
+ // ── Voice OAuth auth routes ─────────────────────────────────────────
2501
+ case "voice/auth/openai/status":
2502
+ case "voice/auth/claude/status":
2503
+ case "voice/auth/gemini/status": {
2504
+ const provider = route.split("/")[2]; // openai | claude | gemini
2505
+ try {
2506
+ const statusFns = {
2507
+ openai: "getOpenAILoginStatus",
2508
+ claude: "getClaudeLoginStatus",
2509
+ gemini: "getGeminiLoginStatus",
2510
+ };
2511
+ const mod = await import("../voice/voice-auth-manager.mjs");
2512
+ const fn = mod[statusFns[provider]];
2513
+ if (!fn) throw new Error(`No status function for ${provider}`);
2514
+ jsonResponse(res, 200, { ok: true, ...fn() });
2515
+ } catch (err) {
2516
+ jsonResponse(res, 200, { ok: true, status: "idle", hasToken: false, error: err.message });
2517
+ }
2518
+ return true;
2519
+ }
2520
+ case "voice/auth/openai/login":
2521
+ case "voice/auth/claude/login":
2522
+ case "voice/auth/gemini/login": {
2523
+ if (req.method !== "POST") {
2524
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2525
+ return true;
2526
+ }
2527
+ const provider = route.split("/")[2];
2528
+ try {
2529
+ const loginFns = {
2530
+ openai: "startOpenAICodexLogin",
2531
+ claude: "startClaudeLogin",
2532
+ gemini: "startGeminiLogin",
2533
+ };
2534
+ const mod = await import("../voice/voice-auth-manager.mjs");
2535
+ const fn = mod[loginFns[provider]];
2536
+ if (!fn) throw new Error(`No login function for ${provider}`);
2537
+ const result = fn({ openBrowser: false });
2538
+ jsonResponse(res, 200, { ok: true, ...(result || {}) });
2539
+ } catch (err) {
2540
+ jsonResponse(res, 500, { ok: false, error: err.message });
2541
+ }
2542
+ return true;
2543
+ }
2544
+ case "voice/auth/openai/logout":
2545
+ case "voice/auth/claude/logout":
2546
+ case "voice/auth/gemini/logout": {
2547
+ if (req.method !== "POST") {
2548
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2549
+ return true;
2550
+ }
2551
+ const provider = route.split("/")[2];
2552
+ try {
2553
+ const logoutFns = {
2554
+ openai: "logoutOpenAI",
2555
+ claude: "logoutClaude",
2556
+ gemini: "logoutGemini",
2557
+ };
2558
+ const mod = await import("../voice/voice-auth-manager.mjs");
2559
+ const fn = mod[logoutFns[provider]];
2560
+ if (!fn) throw new Error(`No logout function for ${provider}`);
2561
+ const result = fn();
2562
+ jsonResponse(res, 200, { ok: true, ...(result || {}) });
2563
+ } catch (err) {
2564
+ jsonResponse(res, 500, { ok: false, error: err.message });
2565
+ }
2566
+ return true;
2567
+ }
2568
+ case "voice/auth/openai/cancel":
2569
+ case "voice/auth/claude/cancel":
2570
+ case "voice/auth/gemini/cancel": {
2571
+ if (req.method !== "POST") {
2572
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2573
+ return true;
2574
+ }
2575
+ const provider = route.split("/")[2];
2576
+ try {
2577
+ const cancelFns = {
2578
+ openai: "cancelOpenAILogin",
2579
+ claude: "cancelClaudeLogin",
2580
+ gemini: "cancelGeminiLogin",
2581
+ };
2582
+ const mod = await import("../voice/voice-auth-manager.mjs");
2583
+ const fn = mod[cancelFns[provider]];
2584
+ if (!fn) throw new Error(`No cancel function for ${provider}`);
2585
+ fn();
2586
+ jsonResponse(res, 200, { ok: true });
2587
+ } catch (err) {
2588
+ jsonResponse(res, 500, { ok: false, error: err.message });
2589
+ }
2590
+ return true;
2591
+ }
2592
+
2593
+ case "validate":
2594
+ if (req.method !== "POST") {
2595
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2596
+ return true;
2597
+ }
2598
+ jsonResponse(res, 200, handleValidate(await readBody(req)));
2599
+ return true;
2600
+ case "telegram-chat-id": {
2601
+ if (req.method !== "POST") {
2602
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2603
+ return true;
2604
+ }
2605
+ const result = await handleTelegramChatIdLookup(await readBody(req));
2606
+ jsonResponse(res, result.status, result);
2607
+ return true;
2608
+ }
2609
+ case "apply":
2610
+ if (req.method !== "POST") {
2611
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2612
+ return true;
2613
+ }
2614
+ jsonResponse(res, 200, handleApply(await readBody(req)));
2615
+ return true;
2616
+ case "install-startup": {
2617
+ if (req.method !== "POST") {
2618
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2619
+ return true;
2620
+ }
2621
+ try {
2622
+ const body_ = await readBody(req);
2623
+ const { installStartupService } = await import("../infra/startup-service.mjs");
2624
+ const result = await installStartupService({ daemon: body_?.daemon !== false });
2625
+ jsonResponse(res, 200, { ok: true, ...result });
2626
+ } catch (err) {
2627
+ jsonResponse(res, 500, { ok: false, error: err.message });
2628
+ }
2629
+ return true;
2630
+ }
2631
+ case "remove-startup": {
2632
+ if (req.method !== "POST") {
2633
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2634
+ return true;
2635
+ }
2636
+ try {
2637
+ const { removeStartupService } = await import("../infra/startup-service.mjs");
2638
+ const result = await removeStartupService();
2639
+ jsonResponse(res, 200, { ok: true, ...result });
2640
+ } catch (err) {
2641
+ jsonResponse(res, 500, { ok: false, error: err.message });
2642
+ }
2643
+ return true;
2644
+ }
2645
+ case "install-desktop-shortcut": {
2646
+ if (req.method !== "POST") {
2647
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2648
+ return true;
2649
+ }
2650
+ try {
2651
+ const { installDesktopShortcut } = await import("../infra/desktop-shortcut.mjs");
2652
+ const result = installDesktopShortcut();
2653
+ jsonResponse(res, 200, { ok: true, ...result });
2654
+ } catch (err) {
2655
+ jsonResponse(res, 500, { ok: false, error: err.message });
2656
+ }
2657
+ return true;
2658
+ }
2659
+ case "remove-desktop-shortcut": {
2660
+ if (req.method !== "POST") {
2661
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2662
+ return true;
2663
+ }
2664
+ try {
2665
+ const { removeDesktopShortcut } = await import("../infra/desktop-shortcut.mjs");
2666
+ const result = removeDesktopShortcut();
2667
+ jsonResponse(res, 200, { ok: true, ...result });
2668
+ } catch (err) {
2669
+ jsonResponse(res, 500, { ok: false, error: err.message });
2670
+ }
2671
+ return true;
2672
+ }
2673
+ case "complete":
2674
+ if (req.method !== "POST") {
2675
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2676
+ return true;
2677
+ }
2678
+ jsonResponse(res, 200, { ok: true, message: "Setup complete" });
2679
+ if (options.onComplete) {
2680
+ // Unified mode: signal completion instead of exiting
2681
+ setTimeout(() => options.onComplete(), 500);
2682
+ } else {
2683
+ // Standalone mode: shut down setup server
2684
+ setTimeout(() => {
2685
+ console.log("\n :check: Setup complete — shutting down wizard server.\n");
2686
+ if (callbackServer) callbackServer.close();
2687
+ server.close();
2688
+ process.exit(0);
2689
+ }, 500);
2690
+ }
2691
+ return true;
2692
+ case "oauth-state": {
2693
+ // The setup wizard polls this to detect when the GitHub OAuth callback
2694
+ // has been received (possibly on a different port).
2695
+ const pendingPath = oauthPendingPath();
2696
+ if (existsSync(pendingPath)) {
2697
+ try {
2698
+ const raw = readFileSync(pendingPath, "utf8");
2699
+ const data = JSON.parse(raw);
2700
+ // Delete the file so it's only claimed once
2701
+ try { unlinkSync(pendingPath); } catch { /* ignore */ }
2702
+ jsonResponse(res, 200, { ok: true, pending: true, ...data });
2703
+ } catch {
2704
+ jsonResponse(res, 200, { ok: true, pending: false });
2705
+ }
2706
+ } else {
2707
+ jsonResponse(res, 200, { ok: true, pending: false });
2708
+ }
2709
+ return true;
2710
+ }
2711
+ default:
2712
+ jsonResponse(res, 404, { ok: false, error: `Unknown route: ${route}` });
2713
+ return true;
2714
+ }
2715
+ } catch (err) {
2716
+ jsonResponse(res, 500, { ok: false, error: err.message });
2717
+ return true;
2718
+ }
2719
+ }
2720
+
2441
2721
  async function handleRequest(req, res) {
2442
2722
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
2443
2723
 
@@ -2503,262 +2783,10 @@ async function handleRequest(req, res) {
2503
2783
  return;
2504
2784
  }
2505
2785
 
2506
- // API routes
2786
+ // API routes — delegate to the shared handler (standalone mode, no onComplete callback)
2507
2787
  if (url.pathname.startsWith("/api/setup/")) {
2508
- const route = url.pathname.replace("/api/setup/", "");
2509
-
2510
- try {
2511
- switch (route) {
2512
- case "status":
2513
- jsonResponse(res, 200, await handleStatus());
2514
- return;
2515
- case "vendor-status":
2516
- jsonResponse(res, 200, checkVendorFiles());
2517
- return;
2518
- case "prerequisites":
2519
- jsonResponse(res, 200, handlePrerequisites());
2520
- return;
2521
- case "defaults":
2522
- jsonResponse(res, 200, handleDefaults());
2523
- return;
2524
- case "models":
2525
- jsonResponse(res, 200, handleModels());
2526
- return;
2527
- case "models/probe":
2528
- if (req.method !== "POST") {
2529
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2530
- return;
2531
- }
2532
- jsonResponse(res, 200, await handleModelsProbe(await readBody(req)));
2533
- return;
2534
- case "executors":
2535
- jsonResponse(res, 200, handleExecutors());
2536
- return;
2537
- case "workflows":
2538
- jsonResponse(res, 200, handleWorkflowTemplates());
2539
- return;
2540
- case "voice/endpoints/test":
2541
- if (req.method !== "POST") {
2542
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2543
- return;
2544
- }
2545
- jsonResponse(res, 200, await handleVoiceEndpointTest(await readBody(req)));
2546
- return;
2547
-
2548
- // ── Voice OAuth auth routes ─────────────────────────────────────────
2549
- case "voice/auth/openai/status":
2550
- case "voice/auth/claude/status":
2551
- case "voice/auth/gemini/status": {
2552
- const provider = route.split("/")[2]; // openai | claude | gemini
2553
- try {
2554
- const statusFns = {
2555
- openai: "getOpenAILoginStatus",
2556
- claude: "getClaudeLoginStatus",
2557
- gemini: "getGeminiLoginStatus",
2558
- };
2559
- const mod = await import("../voice/voice-auth-manager.mjs");
2560
- const fn = mod[statusFns[provider]];
2561
- if (!fn) throw new Error(`No status function for ${provider}`);
2562
- jsonResponse(res, 200, { ok: true, ...fn() });
2563
- } catch (err) {
2564
- jsonResponse(res, 200, { ok: true, status: "idle", hasToken: false, error: err.message });
2565
- }
2566
- return;
2567
- }
2568
- case "voice/auth/openai/login":
2569
- case "voice/auth/claude/login":
2570
- case "voice/auth/gemini/login": {
2571
- if (req.method !== "POST") {
2572
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2573
- return;
2574
- }
2575
- const provider = route.split("/")[2];
2576
- try {
2577
- const loginFns = {
2578
- openai: "startOpenAICodexLogin",
2579
- claude: "startClaudeLogin",
2580
- gemini: "startGeminiLogin",
2581
- };
2582
- const mod = await import("../voice/voice-auth-manager.mjs");
2583
- const fn = mod[loginFns[provider]];
2584
- if (!fn) throw new Error(`No login function for ${provider}`);
2585
- const result = fn({ openBrowser: false });
2586
- jsonResponse(res, 200, { ok: true, ...(result || {}) });
2587
- } catch (err) {
2588
- jsonResponse(res, 500, { ok: false, error: err.message });
2589
- }
2590
- return;
2591
- }
2592
- case "voice/auth/openai/logout":
2593
- case "voice/auth/claude/logout":
2594
- case "voice/auth/gemini/logout": {
2595
- if (req.method !== "POST") {
2596
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2597
- return;
2598
- }
2599
- const provider = route.split("/")[2];
2600
- try {
2601
- const logoutFns = {
2602
- openai: "logoutOpenAI",
2603
- claude: "logoutClaude",
2604
- gemini: "logoutGemini",
2605
- };
2606
- const mod = await import("../voice/voice-auth-manager.mjs");
2607
- const fn = mod[logoutFns[provider]];
2608
- if (!fn) throw new Error(`No logout function for ${provider}`);
2609
- const result = fn();
2610
- jsonResponse(res, 200, { ok: true, ...(result || {}) });
2611
- } catch (err) {
2612
- jsonResponse(res, 500, { ok: false, error: err.message });
2613
- }
2614
- return;
2615
- }
2616
- case "voice/auth/openai/cancel":
2617
- case "voice/auth/claude/cancel":
2618
- case "voice/auth/gemini/cancel": {
2619
- if (req.method !== "POST") {
2620
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2621
- return;
2622
- }
2623
- const provider = route.split("/")[2];
2624
- try {
2625
- const cancelFns = {
2626
- openai: "cancelOpenAILogin",
2627
- claude: "cancelClaudeLogin",
2628
- gemini: "cancelGeminiLogin",
2629
- };
2630
- const mod = await import("../voice/voice-auth-manager.mjs");
2631
- const fn = mod[cancelFns[provider]];
2632
- if (!fn) throw new Error(`No cancel function for ${provider}`);
2633
- fn();
2634
- jsonResponse(res, 200, { ok: true });
2635
- } catch (err) {
2636
- jsonResponse(res, 500, { ok: false, error: err.message });
2637
- }
2638
- return;
2639
- }
2640
-
2641
- case "validate":
2642
- if (req.method !== "POST") {
2643
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2644
- return;
2645
- }
2646
- jsonResponse(res, 200, handleValidate(await readBody(req)));
2647
- return;
2648
- case "telegram-chat-id": {
2649
- if (req.method !== "POST") {
2650
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2651
- return;
2652
- }
2653
- const result = await handleTelegramChatIdLookup(await readBody(req));
2654
- jsonResponse(res, result.status, result);
2655
- return;
2656
- }
2657
- case "apply":
2658
- if (req.method !== "POST") {
2659
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2660
- return;
2661
- }
2662
- jsonResponse(res, 200, handleApply(await readBody(req)));
2663
- return;
2664
- case "install-startup": {
2665
- if (req.method !== "POST") {
2666
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2667
- return;
2668
- }
2669
- try {
2670
- const body_ = await readBody(req);
2671
- const { installStartupService } = await import("../infra/startup-service.mjs");
2672
- const result = await installStartupService({ daemon: body_?.daemon !== false });
2673
- jsonResponse(res, 200, { ok: true, ...result });
2674
- } catch (err) {
2675
- jsonResponse(res, 500, { ok: false, error: err.message });
2676
- }
2677
- return;
2678
- }
2679
- case "remove-startup": {
2680
- if (req.method !== "POST") {
2681
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2682
- return;
2683
- }
2684
- try {
2685
- const { removeStartupService } = await import("../infra/startup-service.mjs");
2686
- const result = await removeStartupService();
2687
- jsonResponse(res, 200, { ok: true, ...result });
2688
- } catch (err) {
2689
- jsonResponse(res, 500, { ok: false, error: err.message });
2690
- }
2691
- return;
2692
- }
2693
- case "install-desktop-shortcut": {
2694
- if (req.method !== "POST") {
2695
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2696
- return;
2697
- }
2698
- try {
2699
- const { installDesktopShortcut } = await import("../infra/desktop-shortcut.mjs");
2700
- const result = installDesktopShortcut();
2701
- jsonResponse(res, 200, { ok: true, ...result });
2702
- } catch (err) {
2703
- jsonResponse(res, 500, { ok: false, error: err.message });
2704
- }
2705
- return;
2706
- }
2707
- case "remove-desktop-shortcut": {
2708
- if (req.method !== "POST") {
2709
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2710
- return;
2711
- }
2712
- try {
2713
- const { removeDesktopShortcut } = await import("../infra/desktop-shortcut.mjs");
2714
- const result = removeDesktopShortcut();
2715
- jsonResponse(res, 200, { ok: true, ...result });
2716
- } catch (err) {
2717
- jsonResponse(res, 500, { ok: false, error: err.message });
2718
- }
2719
- return;
2720
- }
2721
- case "complete":
2722
- if (req.method !== "POST") {
2723
- jsonResponse(res, 405, { ok: false, error: "POST required" });
2724
- return;
2725
- }
2726
- jsonResponse(res, 200, { ok: true, message: "Setup complete" });
2727
- // Shut down server after response is sent
2728
- setTimeout(() => {
2729
- console.log("\n :check: Setup complete — shutting down wizard server.\n");
2730
- if (callbackServer) callbackServer.close();
2731
- server.close();
2732
- process.exit(0);
2733
- }, 500);
2734
- return;
2735
- case "oauth-state": {
2736
- // The setup wizard polls this to detect when the GitHub OAuth callback
2737
- // has been received (possibly on a different port).
2738
- const pendingPath = oauthPendingPath();
2739
- if (existsSync(pendingPath)) {
2740
- try {
2741
- const raw = readFileSync(pendingPath, "utf8");
2742
- const data = JSON.parse(raw);
2743
- // Delete the file so it's only claimed once
2744
- try { unlinkSync(pendingPath); } catch { /* ignore */ }
2745
- jsonResponse(res, 200, { ok: true, pending: true, ...data });
2746
- } catch {
2747
- jsonResponse(res, 200, { ok: true, pending: false });
2748
- }
2749
- } else {
2750
- jsonResponse(res, 200, { ok: true, pending: false });
2751
- }
2752
- return;
2753
- }
2754
- default:
2755
- jsonResponse(res, 404, { ok: false, error: `Unknown route: ${route}` });
2756
- return;
2757
- }
2758
- } catch (err) {
2759
- jsonResponse(res, 500, { ok: false, error: err.message });
2760
- return;
2761
- }
2788
+ await handleSetupApi(req, res, url);
2789
+ return;
2762
2790
  }
2763
2791
 
2764
2792
  // Static file serving from ui/
@@ -3060,6 +3088,7 @@ export {
3060
3088
  applyTelegramMiniAppSetupEnv,
3061
3089
  applyNonBlockingSetupEnvDefaults,
3062
3090
  buildModelsProbeRequest,
3091
+ handleSetupApi,
3063
3092
  handleTelegramChatIdLookup,
3064
3093
  isAzureOpenAIHost,
3065
3094
  normalizeWorkflowTemplateOverrides,