@wrongstack/cli 0.267.0 → 0.268.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.
package/dist/index.js CHANGED
@@ -1,24 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, DefaultSecretScrubber, projectHash, wstackGlobalRoot, resolveProjectDir, GlobalMailbox, TOKENS, ToolRegistry, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, normalizeTokenSavingTier, SlashCommandRegistry, attachDepWatcherBridge, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, DEFAULT_CONTEXT_WINDOW_MODE_ID, recentTextTurns, enhanceUserPrompt, projectSlug, DefaultSystemPromptBuilder, mutateTasks, loadTasks, resolveContextWindowPolicy, repairToolUseAdjacency, mutatePlan, setPlanItemStatus, getPlanTemplate, loadPlan, emptyPlan, addPlanItem, savePlan, resolveProviderModelList, DefaultLogger, DefaultModelsRegistry, isStdinTTY, atomicWrite, DefaultPathResolver, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, makeAutonomyPromptContributor, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, createDefaultPipelines, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, WIDE_SUBAGENT_CAPABILITIES, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, AutonomousCoordinator, noOpVault, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, setQueuedMessagesSnapshot, DefaultSessionRewinder, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionStore as DefaultSessionStore$1, ProviderRegistry, StreamHangError, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, getContextWindowMode, AGENT_CATALOG, dispatchAgent, formatTodosList, formatTaskList, formatTaskProgress, formatPlan, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, FsError, ConfigError, InputBuilder, truncate, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, resolveMailboxIdentity, isSecretField as isSecretField$1 } from '@wrongstack/core';
2
+ import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, resolveHqDataDir, ensureHqFirstRunAuthFile, DEFAULT_HQ_REDACTION_POLICY, watchHqAuthFile, parseHqFrame, HQ_PROTOCOL_VERSION, parseHqEventPayload, scrubAndTruncateHqPreview, DefaultSecretScrubber, projectHash, wstackGlobalRoot, TOKENS, ToolRegistry, createHqPublisherFromEnv, GlobalMailbox, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, normalizeTokenSavingTier, SlashCommandRegistry, attachDepWatcherBridge, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveProjectDir, recentTextTurns, enhanceUserPrompt, projectSlug, DefaultSystemPromptBuilder, mutateTasks, loadTasks, resolveContextWindowPolicy, repairToolUseAdjacency, mutatePlan, setPlanItemStatus, getPlanTemplate, loadPlan, emptyPlan, addPlanItem, savePlan, resolveProviderModelList, DefaultLogger, DefaultModelsRegistry, isStdinTTY, atomicWrite, DefaultPathResolver, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, makeAutonomyPromptContributor, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, createDefaultPipelines, applyModelRuntime, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, WIDE_SUBAGENT_CAPABILITIES, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, AutonomousCoordinator, resolveWstackPaths, setQueuedMessagesSnapshot, noOpVault, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, DefaultSessionRewinder, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, ToolAuditLog, DefaultSessionStore as DefaultSessionStore$1, ProviderRegistry, StreamHangError, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, getContextWindowMode, AGENT_CATALOG, dispatchAgent, formatTodosList, formatTaskList, formatTaskProgress, formatPlan, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, FsError, ConfigError, InputBuilder, HQ_AUTH_FILE_VERSION, truncate, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, resolveMailboxIdentity, mutateHqAuthFile, mintHqToken, readHqAuthFile, isSecretField as isSecretField$1 } from '@wrongstack/core';
3
3
  import * as fsp5 from 'fs/promises';
4
4
  import { decryptConfigSecrets, encryptConfigSecrets, DefaultSecretVault, isSecretField } from '@wrongstack/core/security';
5
5
  import * as path4 from 'path';
6
6
  import { join } from 'path';
7
7
  import * as crypto3 from 'crypto';
8
- import { createHash, randomBytes, randomUUID } from 'crypto';
8
+ import { createHash, randomUUID, randomBytes } from 'crypto';
9
+ import * as http from 'http';
10
+ import { createServer } from 'http';
11
+ import { WebSocketServer, WebSocket } from 'ws';
9
12
  import { createRequire } from 'module';
10
13
  import * as os from 'os';
11
14
  import os__default from 'os';
12
15
  import { findFreePort, AutoPhaseWebSocketHandler, generateAuthToken, verifyClient, handleGitInfo, handleShellOpen, handleGitDiff, handleGitChanges, handleSkillsExport, handleSkillsEdit, handleSkillsCreate, handleSkillsUpdate, handleSkillsUninstall, handleSkillsInstall, handleSkillsContent, handleMcpRestart, handleMcpDisable, handleMcpEnable, handleMcpDiscover, handleMcpSleep, handleMcpWake, handleMcpUpdate, handleMcpRemove, handleMcpAdd, handleMcpList, handleMemoryForget, handleMemoryRemember, handleMemoryList, handleFilesWrite, handleFilesRead, handleFilesTree, handleFilesList, createEternalSubscription, createHttpServer, registerInstance, openBrowser as openBrowser$1, unregisterInstance, estimateTokens as estimateTokens$1, stringifyContent, messagePreview, messageTokens, createCustomModeStore } from '@wrongstack/webui/server';
13
16
  import { setOAuthTokenPersister, makeProviderFromConfig, capabilitiesFor, buildProviderFactoriesFromRegistry, refreshCopilotToken, copilotBaseUrlFromToken } from '@wrongstack/providers';
14
- import { toErrorMessage } from '@wrongstack/core/utils';
17
+ import { toErrorMessage, readJsonObjectFile, updateJsonObjectFile, setJsonPath, jsonObjectFileExists, removeJsonPath } from '@wrongstack/core/utils';
15
18
  import { getProcessRegistry, builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes, shutdownCodebaseIndexHost, TIER2_TOOLS, TIER3_TOOLS, TIER1_TOOLS, resetIndexCircuitBreaker } from '@wrongstack/tools';
16
19
  import { DefaultSessionStore } from '@wrongstack/core/storage';
17
20
  import { probeLocalLlm } from '@wrongstack/runtime/probe';
18
21
  import * as fs3 from 'fs';
19
22
  import { watch, writeFileSync, existsSync, readFileSync } from 'fs';
20
23
  import { SkillInstaller } from '@wrongstack/core/skills';
21
- import { WebSocketServer, WebSocket } from 'ws';
22
24
  import { spawn, execFile, execFileSync } from 'child_process';
23
25
  import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
24
26
  import { fileURLToPath } from 'url';
@@ -28,7 +30,6 @@ import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, runEnsemble, renderEnsembleT
28
30
  import { parseNextSteps } from '@wrongstack/tui';
29
31
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
30
32
  import { SubagentBudget } from '@wrongstack/core/coordination';
31
- import { createServer } from 'http';
32
33
  import { loadBenchConfig, reportHeaderLine, readSummary, renderMarkdownReport, createPolyglotSuite, createSwebenchSuite, runBenchmark, writeJsonArtifacts, collectCellPredictions, writePredictionsJsonl, gradePolyglot, gradeSwebench } from '@wrongstack/bench';
33
34
  import { allServers } from '@wrongstack/core/infrastructure';
34
35
  import { ToolExecutor } from '@wrongstack/core/execution';
@@ -655,26 +656,26 @@ function fmtDuration(ms) {
655
656
  const remMin = m - h * 60;
656
657
  return `${h}h${remMin}m`;
657
658
  }
658
- function fmtTaskResultLine(r, color77) {
659
+ function fmtTaskResultLine(r, color78) {
659
660
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
660
661
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
661
662
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
662
663
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
663
- const errKindChip = errKind ? color77.dim(` [${errKind}]`) : "";
664
- const errSnip = errMsg || errKind ? `${errKindChip}${color77.dim(errTail)}` : "";
664
+ const errKindChip = errKind ? color78.dim(` [${errKind}]`) : "";
665
+ const errSnip = errMsg || errKind ? `${errKindChip}${color78.dim(errTail)}` : "";
665
666
  switch (r.status) {
666
667
  case "success":
667
- return { mark: color77.green("\u2713"), stats, tail: "" };
668
+ return { mark: color78.green("\u2713"), stats, tail: "" };
668
669
  case "timeout":
669
670
  return {
670
- mark: color77.yellow("\u23F1"),
671
- stats: `${color77.yellow("timeout")} ${stats}`,
671
+ mark: color78.yellow("\u23F1"),
672
+ stats: `${color78.yellow("timeout")} ${stats}`,
672
673
  tail: errSnip
673
674
  };
674
675
  case "stopped":
675
- return { mark: color77.dim("\u2298"), stats: `${color77.dim("stopped")} ${stats}`, tail: errSnip };
676
+ return { mark: color78.dim("\u2298"), stats: `${color78.dim("stopped")} ${stats}`, tail: errSnip };
676
677
  case "failed":
677
- return { mark: color77.red("\u2717"), stats: `${color77.red("failed")} ${stats}`, tail: errSnip };
678
+ return { mark: color78.red("\u2717"), stats: `${color78.red("failed")} ${stats}`, tail: errSnip };
678
679
  }
679
680
  }
680
681
  var init_utils = __esm({
@@ -2663,7 +2664,7 @@ async function runProjectPicker(opts) {
2663
2664
  const reservedBottom = 3;
2664
2665
  const headerHeight = reservedTop + reservedBottom;
2665
2666
  const baseVisibleHeight = Math.max(5, terminalHeight() - headerHeight);
2666
- return new Promise((resolve11) => {
2667
+ return new Promise((resolve12) => {
2667
2668
  const wasRaw = stdin.isRaw;
2668
2669
  const wasPaused = stdin.isPaused();
2669
2670
  let filter = "";
@@ -2787,7 +2788,7 @@ async function runProjectPicker(opts) {
2787
2788
  cleanup();
2788
2789
  out.write(CURSOR_SHOW);
2789
2790
  out.write("\n");
2790
- resolve11(void 0);
2791
+ resolve12(void 0);
2791
2792
  return;
2792
2793
  }
2793
2794
  if (ch === ESC) {
@@ -2800,7 +2801,7 @@ async function runProjectPicker(opts) {
2800
2801
  cleanup();
2801
2802
  out.write(CURSOR_SHOW);
2802
2803
  out.write("\n");
2803
- resolve11(void 0);
2804
+ resolve12(void 0);
2804
2805
  return;
2805
2806
  }
2806
2807
  if (ch === BS || ch === "\b") {
@@ -2817,22 +2818,22 @@ async function runProjectPicker(opts) {
2817
2818
  out.write(CURSOR_SHOW);
2818
2819
  out.write("\n");
2819
2820
  if (!item || item.key === "__divider__") {
2820
- resolve11(void 0);
2821
+ resolve12(void 0);
2821
2822
  return;
2822
2823
  }
2823
2824
  if (item.key === "quit") {
2824
- resolve11(void 0);
2825
+ resolve12(void 0);
2825
2826
  return;
2826
2827
  }
2827
2828
  if (item.key === "new-session") {
2828
- resolve11({ kind: "action", key: "new-session", action: "new-session" });
2829
+ resolve12({ kind: "action", key: "new-session", action: "new-session" });
2829
2830
  return;
2830
2831
  }
2831
2832
  if (item.key === "prev-sessions") {
2832
- resolve11({ kind: "action", key: "prev-sessions", action: "prev-sessions" });
2833
+ resolve12({ kind: "action", key: "prev-sessions", action: "prev-sessions" });
2833
2834
  return;
2834
2835
  }
2835
- resolve11({ kind: "project", key: item.key });
2836
+ resolve12({ kind: "project", key: item.key });
2836
2837
  return;
2837
2838
  }
2838
2839
  if (filter.length === 0) {
@@ -2840,7 +2841,7 @@ async function runProjectPicker(opts) {
2840
2841
  cleanup();
2841
2842
  out.write(CURSOR_SHOW);
2842
2843
  out.write("\n");
2843
- resolve11(void 0);
2844
+ resolve12(void 0);
2844
2845
  return;
2845
2846
  }
2846
2847
  if (ch === "j") {
@@ -2869,7 +2870,7 @@ async function runProjectPicker(opts) {
2869
2870
  try {
2870
2871
  stdin.setRawMode(true);
2871
2872
  } catch {
2872
- resolve11(void 0);
2873
+ resolve12(void 0);
2873
2874
  return;
2874
2875
  }
2875
2876
  stdin.resume();
@@ -2880,7 +2881,7 @@ async function runProjectPicker(opts) {
2880
2881
  stdin.once("close", () => {
2881
2882
  cleanup();
2882
2883
  out.write(CURSOR_SHOW);
2883
- resolve11(void 0);
2884
+ resolve12(void 0);
2884
2885
  });
2885
2886
  });
2886
2887
  }
@@ -2906,6 +2907,1108 @@ var init_project_picker = __esm({
2906
2907
  }
2907
2908
  });
2908
2909
 
2910
+ // src/hq-server.ts
2911
+ var hq_server_exports = {};
2912
+ __export(hq_server_exports, {
2913
+ startHqServer: () => startHqServer
2914
+ });
2915
+ function displayHost(host) {
2916
+ return host === "0.0.0.0" ? "127.0.0.1" : host;
2917
+ }
2918
+ function buildHttpUrl(host, port, token) {
2919
+ const url = new URL(`http://${displayHost(host)}:${port}/`);
2920
+ if (token) url.searchParams.set("token", token);
2921
+ return url.toString();
2922
+ }
2923
+ function buildClientWsUrl(host, port, token) {
2924
+ const url = new URL(`ws://${displayHost(host)}:${port}/ws/client`);
2925
+ if (token) url.searchParams.set("token", token);
2926
+ return url.toString();
2927
+ }
2928
+ function writeHqStartupInfo(write, handle) {
2929
+ const firstRun = handle.firstRunSetup;
2930
+ write(`WrongStack HQ listening on http://${handle.host}:${handle.port}
2931
+ `);
2932
+ if (firstRun) {
2933
+ write(`Browser endpoint: ${firstRun.browserUrl}
2934
+ `);
2935
+ write(`Client endpoint: ${buildClientWsUrl(handle.host, handle.port, firstRun.clientEnv.WRONGSTACK_HQ_TOKEN)}
2936
+ `);
2937
+ write(`
2938
+ First-run HQ auth created in ${firstRun.dataDir}
2939
+ `);
2940
+ write(`Start clients with:
2941
+ `);
2942
+ write(` WRONGSTACK_HQ_URL=${firstRun.clientEnv.WRONGSTACK_HQ_URL}
2943
+ `);
2944
+ write(` WRONGSTACK_HQ_TOKEN=${firstRun.clientEnv.WRONGSTACK_HQ_TOKEN}
2945
+ `);
2946
+ } else {
2947
+ write(`Client endpoint: ws://${handle.host}:${handle.port}/ws/client
2948
+ `);
2949
+ write(`Browser endpoint: http://${handle.host}:${handle.port}
2950
+ `);
2951
+ }
2952
+ }
2953
+ async function startHqServer(options = {}) {
2954
+ const host = options.host ?? DEFAULT_HOST;
2955
+ const port = options.port ?? DEFAULT_PORT;
2956
+ const dataDir = resolveHqDataDir(options.dataDir);
2957
+ const firstRunAuth = await ensureHqFirstRunAuthFile(dataDir, {
2958
+ warn: (msg) => console.warn(JSON.stringify({ level: "warn", event: "hq.auth_load_failed", message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))
2959
+ });
2960
+ return startHqServerWithAuth(options, host, port, dataDir, firstRunAuth);
2961
+ }
2962
+ function extractBrowserToken(req2, url) {
2963
+ const queryToken = url.searchParams.get("token");
2964
+ if (queryToken) return queryToken;
2965
+ const auth = req2.headers.authorization;
2966
+ if (typeof auth === "string" && auth.toLowerCase().startsWith("bearer ")) {
2967
+ return auth.slice(7).trim();
2968
+ }
2969
+ return void 0;
2970
+ }
2971
+ function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth) {
2972
+ const authFile = firstRunAuth.authFile;
2973
+ const mutableAuth = {
2974
+ operatorPolicy: {
2975
+ ...DEFAULT_HQ_REDACTION_POLICY,
2976
+ ...authFile.redactionPolicy ?? {}
2977
+ },
2978
+ browserTokens: new Set((authFile.browserTokens ?? []).map((t) => t.token)),
2979
+ clientTokens: new Set((authFile.clientTokens ?? []).map((t) => t.token))
2980
+ };
2981
+ console.warn(JSON.stringify({
2982
+ level: "info",
2983
+ event: "hq.startup",
2984
+ message: "WrongStack HQ starting",
2985
+ dataDir,
2986
+ host,
2987
+ port,
2988
+ operatorPolicyActive: authFile.redactionPolicy !== void 0,
2989
+ browserTokenMode: mutableAuth.browserTokens.size > 0,
2990
+ clientTokenMode: mutableAuth.clientTokens.size > 0,
2991
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2992
+ }));
2993
+ return new Promise((resolve12, reject) => {
2994
+ const clients = /* @__PURE__ */ new Map();
2995
+ const browsers = /* @__PURE__ */ new Set();
2996
+ const eventLog = [];
2997
+ const httpServer = http.createServer((req2, res) => {
2998
+ const url = new URL(req2.url ?? "/", `http://${host}:${port}`);
2999
+ if (mutableAuth.browserTokens.size > 0) {
3000
+ const supplied = extractBrowserToken(req2, url);
3001
+ if (!supplied || !mutableAuth.browserTokens.has(supplied)) {
3002
+ res.writeHead(401, { "Content-Type": "application/json" });
3003
+ res.end(
3004
+ JSON.stringify({
3005
+ error: {
3006
+ code: "INVALID_TOKEN",
3007
+ message: "A valid ?token= or Authorization: Bearer is required for HTTP access in browser token mode."
3008
+ }
3009
+ })
3010
+ );
3011
+ return;
3012
+ }
3013
+ }
3014
+ if (url.pathname === "/" || url.pathname === "/index.html") {
3015
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3016
+ res.end(HQ_HTML);
3017
+ return;
3018
+ }
3019
+ if (url.pathname === "/api/snapshot") {
3020
+ res.writeHead(200, { "Content-Type": "application/json" });
3021
+ res.end(JSON.stringify(buildSnapshot(clients)));
3022
+ return;
3023
+ }
3024
+ if (url.pathname.startsWith("/api/projects/")) {
3025
+ const projectId = decodeURIComponent(url.pathname.slice("/api/projects/".length));
3026
+ if (!projectId) {
3027
+ res.writeHead(400, { "Content-Type": "application/json" });
3028
+ res.end(
3029
+ JSON.stringify({ error: { code: "BAD_REQUEST", message: "projectId is required" } })
3030
+ );
3031
+ return;
3032
+ }
3033
+ const detail = buildProjectDetail(clients, projectId);
3034
+ if (!detail) {
3035
+ res.writeHead(404, { "Content-Type": "application/json" });
3036
+ res.end(
3037
+ JSON.stringify({
3038
+ error: { code: "NOT_FOUND", message: `Unknown project: ${projectId}` }
3039
+ })
3040
+ );
3041
+ return;
3042
+ }
3043
+ res.writeHead(200, { "Content-Type": "application/json" });
3044
+ res.end(JSON.stringify(detail));
3045
+ return;
3046
+ }
3047
+ res.writeHead(404, { "Content-Type": "text/plain" });
3048
+ res.end("Not found");
3049
+ });
3050
+ const wss = new WebSocketServer({ noServer: true, maxPayload: 1 * 1024 * 1024 });
3051
+ httpServer.on("upgrade", (req2, socket, head) => {
3052
+ const url = new URL(req2.url ?? "/", `http://${host}:${port}`);
3053
+ const pathname = url.pathname;
3054
+ if (pathname !== "/ws/client" && pathname !== "/ws/browser") {
3055
+ socket.destroy();
3056
+ return;
3057
+ }
3058
+ const tokenSet = pathname === "/ws/browser" ? mutableAuth.browserTokens : mutableAuth.clientTokens;
3059
+ if (tokenSet.size > 0) {
3060
+ const supplied = url.searchParams.get("token") ?? "";
3061
+ if (!supplied || !tokenSet.has(supplied)) {
3062
+ socket.write(
3063
+ "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
3064
+ error: {
3065
+ code: "INVALID_TOKEN",
3066
+ message: `A valid ?token= is required for ${pathname} connections in token mode.`
3067
+ }
3068
+ })
3069
+ );
3070
+ socket.destroy();
3071
+ return;
3072
+ }
3073
+ }
3074
+ wss.handleUpgrade(req2, socket, head, (ws) => {
3075
+ wss.emit("connection", ws, req2, pathname);
3076
+ });
3077
+ });
3078
+ wss.on("connection", (ws, _req, pathname) => {
3079
+ if (pathname === "/ws/browser") {
3080
+ handleBrowser(ws, clients, browsers);
3081
+ } else {
3082
+ handleClient(ws, clients, browsers, eventLog, mutableAuth.operatorPolicy);
3083
+ }
3084
+ });
3085
+ const authWatcher = watchHqAuthFile(
3086
+ dataDir,
3087
+ (next) => {
3088
+ mutableAuth.operatorPolicy = {
3089
+ ...DEFAULT_HQ_REDACTION_POLICY,
3090
+ ...next.redactionPolicy ?? {}
3091
+ };
3092
+ mutableAuth.browserTokens = new Set((next.browserTokens ?? []).map((t) => t.token));
3093
+ mutableAuth.clientTokens = new Set((next.clientTokens ?? []).map((t) => t.token));
3094
+ console.warn(JSON.stringify({
3095
+ level: "info",
3096
+ event: "hq.auth.reloaded",
3097
+ message: "HQ auth.json reloaded",
3098
+ browserTokenCount: mutableAuth.browserTokens.size,
3099
+ clientTokenCount: mutableAuth.clientTokens.size,
3100
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3101
+ }));
3102
+ },
3103
+ {
3104
+ warn: (msg) => console.warn(JSON.stringify({
3105
+ level: "warn",
3106
+ event: "hq.auth.reload_failed",
3107
+ message: msg,
3108
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3109
+ }))
3110
+ }
3111
+ );
3112
+ const onError = (err) => {
3113
+ if (err.code === "EADDRINUSE" && !options.strictPort) {
3114
+ httpServer.listen(port + 1, host);
3115
+ } else {
3116
+ reject(err);
3117
+ }
3118
+ };
3119
+ httpServer.once("error", onError);
3120
+ httpServer.listen(port, host, () => {
3121
+ httpServer.removeListener("error", onError);
3122
+ const addr = httpServer.address();
3123
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
3124
+ const firstRunSetup = firstRunAuth.created && firstRunAuth.browserToken && firstRunAuth.clientToken ? {
3125
+ dataDir,
3126
+ browserUrl: buildHttpUrl(host, actualPort, firstRunAuth.browserToken.token),
3127
+ clientEnv: {
3128
+ WRONGSTACK_HQ_URL: `http://${displayHost(host)}:${actualPort}`,
3129
+ WRONGSTACK_HQ_TOKEN: firstRunAuth.clientToken.token
3130
+ }
3131
+ } : void 0;
3132
+ const handle = {
3133
+ host,
3134
+ port: actualPort,
3135
+ ...firstRunSetup ? { firstRunSetup } : {},
3136
+ close: () => new Promise((res) => {
3137
+ authWatcher.close();
3138
+ for (const ws of browsers) ws.close(1001, "HQ shutting down");
3139
+ for (const ws of clients.keys()) ws.close(1001, "HQ shutting down");
3140
+ wss.close();
3141
+ httpServer.close(() => res());
3142
+ })
3143
+ };
3144
+ writeHqStartupInfo((line) => console.log(line.trimEnd()), handle);
3145
+ resolve12(handle);
3146
+ });
3147
+ });
3148
+ }
3149
+ function handleBrowser(ws, clients, browsers) {
3150
+ browsers.add(ws);
3151
+ const snapshotMsg = { type: "hq.snapshot", snapshot: buildSnapshot(clients) };
3152
+ ws.send(JSON.stringify(snapshotMsg));
3153
+ ws.on("close", () => {
3154
+ browsers.delete(ws);
3155
+ });
3156
+ }
3157
+ function handleClient(ws, clients, browsers, eventLog, operatorPolicy) {
3158
+ let registered = false;
3159
+ ws.on("message", (data) => {
3160
+ const raw = typeof data === "string" ? data : Buffer.isBuffer(data) ? data : new TextDecoder().decode(data);
3161
+ const parsed = parseHqFrame(raw);
3162
+ if (!parsed.ok) {
3163
+ const code = parsed.reason === "invalid-json" ? 1003 : 1008;
3164
+ ws.close(code, `invalid frame: ${parsed.reason}`);
3165
+ return;
3166
+ }
3167
+ const frame = parsed.frame;
3168
+ if (frame.type === "client.hello") {
3169
+ const payload = frame.payload;
3170
+ if (payload.protocolVersion !== HQ_PROTOCOL_VERSION) {
3171
+ ws.close(1008, "protocol version mismatch");
3172
+ return;
3173
+ }
3174
+ const client = {
3175
+ ws,
3176
+ clientId: payload.client.clientId,
3177
+ projectId: payload.project.projectId,
3178
+ kind: payload.client.kind,
3179
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3180
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
3181
+ ...payload.client.hostname ? { hostname: payload.client.hostname } : {},
3182
+ ...payload.client.pid ? { pid: payload.client.pid } : {},
3183
+ ...payload.client.version ? { version: payload.client.version } : {},
3184
+ capabilities: payload.capabilities,
3185
+ mailboxes: /* @__PURE__ */ new Map()
3186
+ };
3187
+ clients.set(ws, client);
3188
+ registered = true;
3189
+ const welcome = {
3190
+ type: "hq.welcome",
3191
+ protocolVersion: HQ_PROTOCOL_VERSION,
3192
+ serverTime: (/* @__PURE__ */ new Date()).toISOString(),
3193
+ acceptedCapabilities: payload.capabilities,
3194
+ // The operator-configured override (from <dataDir>/auth.json) wins
3195
+ // over the default. The client learns the *effective* policy.
3196
+ redactionPolicy: operatorPolicy
3197
+ };
3198
+ ws.send(JSON.stringify(welcome));
3199
+ const event = {
3200
+ id: randomUUID(),
3201
+ type: "client.hello",
3202
+ schemaVersion: HQ_PROTOCOL_VERSION,
3203
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3204
+ clientId: payload.client.clientId,
3205
+ projectId: payload.project.projectId,
3206
+ seq: 0,
3207
+ payload: { client: payload.client, project: payload.project }
3208
+ };
3209
+ eventLog.push(event);
3210
+ if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
3211
+ broadcastSnapshot(clients, browsers);
3212
+ broadcastEvent(event, browsers);
3213
+ return;
3214
+ }
3215
+ if (!registered) return;
3216
+ if (frame.type === "client.event") {
3217
+ const event = frame.event;
3218
+ const client = clients.get(ws);
3219
+ if (client) client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
3220
+ if (event.type === "mailbox.snapshot" && client !== void 0) {
3221
+ const payloadResult = parseHqEventPayload(event.type, event.payload);
3222
+ if (payloadResult.ok) {
3223
+ const payload = payloadResult.payload;
3224
+ client.mailboxes.set(payload.mailboxId, payload);
3225
+ eventLog.push(event);
3226
+ if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
3227
+ broadcastSnapshot(clients, browsers);
3228
+ broadcastEvent(event, browsers);
3229
+ return;
3230
+ }
3231
+ return;
3232
+ }
3233
+ if (event.type === "mailbox.event") {
3234
+ const payloadResult = parseHqEventPayload(event.type, event.payload);
3235
+ if (!payloadResult.ok) {
3236
+ return;
3237
+ }
3238
+ const payload = payloadResult.payload;
3239
+ const sanitizedSummary = scrubAndTruncateHqPreview(payload.summary, 280);
3240
+ const sanitizedEvent = sanitizedSummary === void 0 ? event : { ...event, payload: { ...payload, summary: sanitizedSummary } };
3241
+ eventLog.push(sanitizedEvent);
3242
+ if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
3243
+ broadcastEvent(sanitizedEvent, browsers);
3244
+ return;
3245
+ }
3246
+ eventLog.push(event);
3247
+ if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
3248
+ broadcastEvent(event, browsers);
3249
+ }
3250
+ });
3251
+ ws.on("close", () => {
3252
+ clients.delete(ws);
3253
+ broadcastSnapshot(clients, browsers);
3254
+ });
3255
+ }
3256
+ function buildSnapshot(clients) {
3257
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3258
+ const clientRecords = [];
3259
+ const projectMap = /* @__PURE__ */ new Map();
3260
+ const mailboxSummaries = [];
3261
+ for (const client of clients.values()) {
3262
+ clientRecords.push({
3263
+ clientId: client.clientId,
3264
+ kind: client.kind,
3265
+ machineId: "",
3266
+ ...client.hostname ? { hostname: client.hostname } : {},
3267
+ ...client.pid ? { pid: client.pid } : {},
3268
+ ...client.version ? { version: client.version } : {},
3269
+ connected: true,
3270
+ connectedAt: client.connectedAt,
3271
+ lastSeenAt: client.lastSeenAt,
3272
+ projectId: client.projectId,
3273
+ capabilities: client.capabilities
3274
+ });
3275
+ let project = projectMap.get(client.projectId);
3276
+ if (!project) {
3277
+ project = {
3278
+ projectId: client.projectId,
3279
+ projectName: client.projectId,
3280
+ projectRootDisplay: "",
3281
+ machineIds: [],
3282
+ activeClients: 0,
3283
+ activeSessions: 0,
3284
+ activeSubagents: 0,
3285
+ totalCostUsd: 0,
3286
+ lastActivityAt: now,
3287
+ status: "active"
3288
+ };
3289
+ projectMap.set(client.projectId, project);
3290
+ }
3291
+ project.activeClients++;
3292
+ for (const snapshot of client.mailboxes.values()) {
3293
+ mailboxSummaries.push({
3294
+ mailboxId: snapshot.mailboxId,
3295
+ projectId: client.projectId,
3296
+ scope: snapshot.scope,
3297
+ messageCount: snapshot.totals.messages,
3298
+ unreadCount: snapshot.totals.unread,
3299
+ incompleteCount: snapshot.totals.incomplete,
3300
+ highPriorityCount: snapshot.totals.highPriority,
3301
+ onlineAgentCount: snapshot.totals.onlineAgents,
3302
+ lastActivityAt: now
3303
+ });
3304
+ }
3305
+ }
3306
+ const projects = Array.from(projectMap.values());
3307
+ const totals = computeTotals({
3308
+ projects: projects.length,
3309
+ clients: clientRecords.length,
3310
+ mailboxes: mailboxSummaries
3311
+ });
3312
+ return {
3313
+ generatedAt: now,
3314
+ clients: clientRecords,
3315
+ projects,
3316
+ sessions: [],
3317
+ fleets: [],
3318
+ mailboxes: mailboxSummaries,
3319
+ totals
3320
+ };
3321
+ }
3322
+ function computeTotals(input) {
3323
+ let unread = 0;
3324
+ let incomplete = 0;
3325
+ for (const m of input.mailboxes) {
3326
+ unread += m.unreadCount;
3327
+ incomplete += m.incompleteCount;
3328
+ }
3329
+ return {
3330
+ activeProjects: input.projects,
3331
+ activeClients: input.clients,
3332
+ activeSessions: 0,
3333
+ activeSubagents: 0,
3334
+ unreadMailboxMessages: unread,
3335
+ incompleteMailboxMessages: incomplete,
3336
+ totalCostUsd: 0
3337
+ };
3338
+ }
3339
+ function buildProjectDetail(clients, projectId) {
3340
+ const projectClients = [];
3341
+ for (const c of clients.values()) {
3342
+ if (c.projectId === projectId) projectClients.push(c);
3343
+ }
3344
+ if (projectClients.length === 0) return null;
3345
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3346
+ const clientRecords = projectClients.map((c) => ({
3347
+ clientId: c.clientId,
3348
+ kind: c.kind,
3349
+ machineId: "",
3350
+ ...c.hostname ? { hostname: c.hostname } : {},
3351
+ ...c.pid ? { pid: c.pid } : {},
3352
+ ...c.version ? { version: c.version } : {},
3353
+ connected: true,
3354
+ connectedAt: c.connectedAt,
3355
+ lastSeenAt: c.lastSeenAt,
3356
+ projectId: c.projectId,
3357
+ capabilities: c.capabilities
3358
+ }));
3359
+ const mailboxPayloads = [];
3360
+ let latestActivity = now;
3361
+ for (const c of projectClients) {
3362
+ for (const snap of c.mailboxes.values()) {
3363
+ mailboxPayloads.push(snap);
3364
+ if (snap.totals.messages > 0) latestActivity = now;
3365
+ }
3366
+ }
3367
+ const project = {
3368
+ projectId,
3369
+ projectName: projectId,
3370
+ projectRootDisplay: "",
3371
+ machineIds: [],
3372
+ activeClients: projectClients.length,
3373
+ activeSessions: 0,
3374
+ activeSubagents: 0,
3375
+ totalCostUsd: 0,
3376
+ lastActivityAt: latestActivity,
3377
+ status: "active"
3378
+ };
3379
+ return {
3380
+ generatedAt: now,
3381
+ project,
3382
+ clients: clientRecords,
3383
+ mailboxes: mailboxPayloads
3384
+ };
3385
+ }
3386
+ function broadcastSnapshot(clients, browsers) {
3387
+ const snapshot = buildSnapshot(clients);
3388
+ const msg = { type: "hq.snapshot", snapshot };
3389
+ const data = JSON.stringify(msg);
3390
+ for (const ws of browsers) {
3391
+ if (ws.readyState === WebSocket.OPEN) ws.send(data);
3392
+ }
3393
+ }
3394
+ function broadcastEvent(event, browsers) {
3395
+ const msg = { type: "hq.event", event };
3396
+ const data = JSON.stringify(msg);
3397
+ for (const ws of browsers) {
3398
+ if (ws.readyState === WebSocket.OPEN) ws.send(data);
3399
+ }
3400
+ }
3401
+ var DEFAULT_HOST, DEFAULT_PORT, MAX_EVENT_LOG, HQ_HTML;
3402
+ var init_hq_server = __esm({
3403
+ "src/hq-server.ts"() {
3404
+ DEFAULT_HOST = "127.0.0.1";
3405
+ DEFAULT_PORT = 3499;
3406
+ MAX_EVENT_LOG = 500;
3407
+ HQ_HTML = `<!DOCTYPE html>
3408
+ <html lang="en">
3409
+ <head>
3410
+ <meta charset="UTF-8" />
3411
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3412
+ <title>WrongStack HQ</title>
3413
+ <style>
3414
+ :root {
3415
+ --bg: #0d1117;
3416
+ --panel: #161b22;
3417
+ --border: #30363d;
3418
+ --text: #c9d1d9;
3419
+ --muted: #8b949e;
3420
+ --dim: #6e7681;
3421
+ --accent: #58a6ff;
3422
+ --live: #3fb950;
3423
+ --warn: #d29922;
3424
+ --high: #f85149;
3425
+ }
3426
+ * { box-sizing: border-box; }
3427
+ body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 24px; }
3428
+ h1 { margin: 0 0 4px; color: var(--accent); font-size: 22px; }
3429
+ .hq-sub { color: var(--muted); font-size: 13px; margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
3430
+ .hq-led { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
3431
+ .hq-led.live { background: var(--live); box-shadow: 0 0 6px var(--live); }
3432
+ .hq-led.dead { background: var(--dim); }
3433
+ .hq-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
3434
+ .hq-stat { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; }
3435
+ .hq-stat .num { font-size: 26px; font-weight: 700; color: #f0f6fc; line-height: 1.1; }
3436
+ .hq-stat .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); margin-top: 4px; }
3437
+ .hq-stat.warn .num { color: var(--warn); }
3438
+ .hq-stat.high .num { color: var(--high); }
3439
+ section { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
3440
+ section h2 { margin: 0 0 12px; font-size: 14px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--muted); font-weight: 600; }
3441
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
3442
+ th { text-align: left; font-weight: 600; color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 10px; border-bottom: 1px solid var(--border); }
3443
+ td { padding: 10px; border-bottom: 1px solid #21262d; }
3444
+ tr:last-child td { border-bottom: none; }
3445
+ td.num { text-align: right; font-variant-numeric: tabular-nums; }
3446
+ td .pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: #21262d; color: var(--muted); }
3447
+ td .pill.project { background: rgba(88,166,255,0.15); color: var(--accent); }
3448
+ td .pill.global { background: rgba(63,185,80,0.15); color: var(--live); }
3449
+ .empty { color: var(--dim); font-style: italic; padding: 12px 0; font-size: 13px; }
3450
+ .badge { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 11px; background: #21262d; color: var(--muted); margin-right: 4px; }
3451
+ .project-link { color: var(--accent); cursor: pointer; text-decoration: none; }
3452
+ .project-link:hover { text-decoration: underline; }
3453
+ .drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: none; z-index: 50; }
3454
+ .drawer-backdrop.open { display: block; }
3455
+ .drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(720px, 90vw); background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,0.5); transform: translateX(100%); transition: transform 0.18s ease; overflow-y: auto; z-index: 51; padding: 24px; }
3456
+ .drawer.open { transform: translateX(0); }
3457
+ .drawer h2 { margin: 0 0 4px; color: var(--accent); font-size: 18px; }
3458
+ .drawer .drawer-meta { color: var(--muted); font-size: 12px; margin-bottom: 20px; }
3459
+ .drawer .drawer-close { float: right; background: transparent; border: 1px solid var(--border); color: var(--text); padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 12px; }
3460
+ .drawer .drawer-close:hover { background: #21262d; }
3461
+ .msg-row { padding: 10px; border-bottom: 1px solid #21262d; font-size: 13px; }
3462
+ .msg-row:last-child { border-bottom: none; }
3463
+ .msg-row .msg-subject { font-weight: 600; color: #f0f6fc; }
3464
+ .msg-row .msg-meta { color: var(--dim); font-size: 11px; margin-top: 2px; }
3465
+ .msg-row .msg-preview { color: var(--muted); font-size: 12px; margin-top: 4px; font-style: italic; }
3466
+ .pill.priority-high { background: rgba(248,81,73,0.18); color: var(--high); }
3467
+ .pill.priority-normal { background: rgba(88,166,255,0.15); color: var(--accent); }
3468
+ .pill.priority-low { background: #21262d; color: var(--dim); }
3469
+ .hq-toolbar { display: flex; align-items: center; gap: 8px; margin: 12px 0 16px; padding: 8px 12px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; }
3470
+ .hq-toolbar label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); }
3471
+ .hq-toolbar select { background: #0d1117; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; font-size: 13px; min-width: 280px; }
3472
+ .hq-toolbar select:focus { outline: none; border-color: var(--accent); }
3473
+ .feed-status { float: right; font-size: 10px; text-transform: none; letter-spacing: 0; color: var(--dim); }
3474
+ .feed-status.live { color: var(--live); }
3475
+ .feed-row { padding: 8px 10px; border-bottom: 1px solid #21262d; font-size: 12px; animation: feed-flash 0.6s ease-out; }
3476
+ .feed-row:last-child { border-bottom: none; }
3477
+ .feed-row .feed-action { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; margin-right: 6px; background: #21262d; color: var(--muted); font-family: ui-monospace, monospace; }
3478
+ .feed-row .feed-action.message-sent { background: rgba(88,166,255,0.18); color: var(--accent); }
3479
+ .feed-row .feed-action.message-completed { background: rgba(63,185,80,0.18); color: var(--live); }
3480
+ .feed-row .feed-action.message-read { background: rgba(139,148,158,0.18); color: var(--muted); }
3481
+ .feed-row .feed-action.agent-offline { background: rgba(248,81,73,0.18); color: var(--high); }
3482
+ .feed-row .feed-action.agent-registered { background: rgba(63,185,80,0.18); color: var(--live); }
3483
+ .feed-row .feed-meta { color: var(--dim); font-size: 10px; margin-top: 2px; }
3484
+ @keyframes feed-flash { 0% { background: rgba(88,166,255,0.25); } 100% { background: transparent; } }
3485
+ </style>
3486
+ </head>
3487
+ <body>
3488
+ <h1>\u{1F4CB} WrongStack HQ</h1>
3489
+ <p class="hq-sub" id="hq-conn"><span class="hq-led dead" id="hq-led"></span>Connecting\u2026</p>
3490
+ <div class="hq-toolbar">
3491
+ <label for="project-picker">Project:</label>
3492
+ <select id="project-picker" aria-label="Select project">
3493
+ <option value="">\u2014 Select project \u2014</option>
3494
+ </select>
3495
+ </div>
3496
+
3497
+ <div class="hq-grid">
3498
+ <div class="hq-stat"><span class="num" id="stat-clients">0</span><div class="label">Active clients</div></div>
3499
+ <div class="hq-stat"><span class="num" id="stat-projects">0</span><div class="label">Projects</div></div>
3500
+ <div class="hq-stat warn"><span class="num" id="stat-mailboxes">0</span><div class="label">Mailboxes</div></div>
3501
+ <div class="hq-stat warn"><span class="num" id="stat-unread">0</span><div class="label">Unread messages</div></div>
3502
+ <div class="hq-stat warn"><span class="num" id="stat-incomplete">0</span><div class="label">Open messages</div></div>
3503
+ <div class="hq-stat high"><span class="num" id="stat-high">0</span><div class="label">High priority</div></div>
3504
+ <div class="hq-stat"><span class="num" id="stat-agents">0</span><div class="label">Online agents</div></div>
3505
+ </div>
3506
+
3507
+ <section>
3508
+ <h2>\u{1F4EC} Mailboxes</h2>
3509
+ <table>
3510
+ <thead>
3511
+ <tr>
3512
+ <th>Mailbox</th>
3513
+ <th>Scope</th>
3514
+ <th>Project</th>
3515
+ <th class="num">Messages</th>
3516
+ <th class="num">Unread</th>
3517
+ <th class="num">Open</th>
3518
+ <th class="num">High</th>
3519
+ <th class="num">Agents</th>
3520
+ </tr>
3521
+ </thead>
3522
+ <tbody id="tbody-mailboxes">
3523
+ <tr><td colspan="8" class="empty">No mailboxes yet. Connect a TUI/REPL/WebUI client with WRONGSTACK_HQ_URL set.</td></tr>
3524
+ </tbody>
3525
+ </table>
3526
+ </section>
3527
+
3528
+ <section>
3529
+ <h2>\u{1F465} Clients</h2>
3530
+ <table>
3531
+ <thead>
3532
+ <tr>
3533
+ <th>Client ID</th>
3534
+ <th>Kind</th>
3535
+ <th>Project</th>
3536
+ <th>Capabilities</th>
3537
+ <th>Last seen</th>
3538
+ </tr>
3539
+ </thead>
3540
+ <tbody id="tbody-clients">
3541
+ <tr><td colspan="5" class="empty">No clients connected yet.</td></tr>
3542
+ </tbody>
3543
+ </table>
3544
+ </section>
3545
+
3546
+ <div class="drawer-backdrop" id="drawer-backdrop"></div>
3547
+ <aside class="drawer" id="drawer" aria-hidden="true">
3548
+ <button class="drawer-close" id="drawer-close">Close</button>
3549
+ <h2 id="drawer-title">Project</h2>
3550
+ <p class="drawer-meta" id="drawer-meta"></p>
3551
+ <section>
3552
+ <h2>\u{1F4EC} Mailboxes</h2>
3553
+ <table>
3554
+ <thead>
3555
+ <tr>
3556
+ <th>Mailbox</th>
3557
+ <th>Scope</th>
3558
+ <th class="num">Messages</th>
3559
+ <th class="num">Unread</th>
3560
+ <th class="num">Agents</th>
3561
+ </tr>
3562
+ </thead>
3563
+ <tbody id="drawer-mailboxes">
3564
+ <tr><td colspan="5" class="empty">Loading\u2026</td></tr>
3565
+ </tbody>
3566
+ </table>
3567
+ </section>
3568
+ <section>
3569
+ <h2>\u{1F4E8} Recent messages</h2>
3570
+ <div id="drawer-messages">
3571
+ <p class="empty">Loading\u2026</p>
3572
+ </div>
3573
+ </section>
3574
+ <section>
3575
+ <h2>\u{1F4E1} Live mailbox events
3576
+ <span class="feed-status" id="feed-status">(idle)</span>
3577
+ </h2>
3578
+ <div id="drawer-event-feed">
3579
+ <p class="empty">No mailbox events yet for this project.</p>
3580
+ </div>
3581
+ </section>
3582
+ <section>
3583
+ <h2>\u{1F465} Clients</h2>
3584
+ <table>
3585
+ <thead>
3586
+ <tr>
3587
+ <th>Client ID</th>
3588
+ <th>Kind</th>
3589
+ <th>Last seen</th>
3590
+ </tr>
3591
+ </thead>
3592
+ <tbody id="drawer-clients">
3593
+ <tr><td colspan="3" class="empty">Loading\u2026</td></tr>
3594
+ </tbody>
3595
+ </table>
3596
+ </section>
3597
+ </aside>
3598
+
3599
+ <script>
3600
+ const led = document.getElementById('hq-led');
3601
+ const connText = document.getElementById('hq-conn');
3602
+
3603
+ function el(id) { return document.getElementById(id); }
3604
+
3605
+ function fmtTime(iso) {
3606
+ if (!iso) return '\u2014';
3607
+ const d = new Date(iso);
3608
+ if (isNaN(d.getTime())) return '\u2014';
3609
+ return d.toLocaleTimeString();
3610
+ }
3611
+
3612
+ function shortId(s) {
3613
+ if (!s) return '\u2014';
3614
+ return s.length > 12 ? s.slice(0, 6) + '\u2026' + s.slice(-4) : s;
3615
+ }
3616
+
3617
+ function renderMailboxes(mailboxes) {
3618
+ const tbody = el('tbody-mailboxes');
3619
+ if (!mailboxes || mailboxes.length === 0) {
3620
+ tbody.innerHTML = '<tr><td colspan="8" class="empty">No mailboxes yet. Connect a TUI/REPL/WebUI client with WRONGSTACK_HQ_URL set.</td></tr>';
3621
+ return;
3622
+ }
3623
+ tbody.innerHTML = mailboxes.map((m) => {
3624
+ const scopeClass = m.scope === 'global' ? 'global' : 'project';
3625
+ const projectCell = '<a href="#' + encodeURIComponent(m.projectId) + '" class="project-link" data-project="' + escapeHtml(m.projectId) + '">' + escapeHtml(shortId(m.projectId)) + '</a>';
3626
+ return '<tr>' +
3627
+ '<td><code>' + escapeHtml(shortId(m.mailboxId)) + '</code></td>' +
3628
+ '<td><span class="pill ' + scopeClass + '">' + escapeHtml(m.scope) + '</span></td>' +
3629
+ '<td>' + projectCell + '</td>' +
3630
+ '<td class="num">' + m.messageCount + '</td>' +
3631
+ '<td class="num">' + (m.unreadCount > 0 ? '<strong>' + m.unreadCount + '</strong>' : '0') + '</td>' +
3632
+ '<td class="num">' + m.incompleteCount + '</td>' +
3633
+ '<td class="num">' + (m.highPriorityCount > 0 ? '<strong style="color:var(--high)">' + m.highPriorityCount + '</strong>' : '0') + '</td>' +
3634
+ '<td class="num">' + m.onlineAgentCount + '</td>' +
3635
+ '</tr>';
3636
+ }).join('');
3637
+ wireProjectLinks();
3638
+ }
3639
+
3640
+ function renderClients(clients) {
3641
+ const tbody = el('tbody-clients');
3642
+ if (!clients || clients.length === 0) {
3643
+ tbody.innerHTML = '<tr><td colspan="5" class="empty">No clients connected yet.</td></tr>';
3644
+ return;
3645
+ }
3646
+ tbody.innerHTML = clients.map((c) => {
3647
+ const caps = (c.capabilities || []).map((cap) => '<span class="badge">' + escapeHtml(cap) + '</span>').join('');
3648
+ return '<tr>' +
3649
+ '<td><code>' + escapeHtml(shortId(c.clientId)) + '</code></td>' +
3650
+ '<td>' + escapeHtml(c.kind) + '</td>' +
3651
+ '<td>' + escapeHtml(shortId(c.projectId)) + '</td>' +
3652
+ '<td>' + caps + '</td>' +
3653
+ '<td>' + fmtTime(c.lastSeenAt) + '</td>' +
3654
+ '</tr>';
3655
+ }).join('');
3656
+ }
3657
+
3658
+ function escapeHtml(s) {
3659
+ if (s === null || s === undefined) return '';
3660
+ return String(s).replace(/[&<>"']/g, function (c) {
3661
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
3662
+ });
3663
+ }
3664
+
3665
+ function applySnapshot(s) {
3666
+ el('stat-clients').textContent = s.totals.activeClients;
3667
+ el('stat-projects').textContent = s.totals.activeProjects;
3668
+ el('stat-unread').textContent = s.totals.unreadMailboxMessages;
3669
+ el('stat-incomplete').textContent = s.totals.incompleteMailboxMessages;
3670
+
3671
+ let totalMessages = 0;
3672
+ let highPriority = 0;
3673
+ let onlineAgents = 0;
3674
+ for (const m of (s.mailboxes || [])) {
3675
+ totalMessages += m.messageCount;
3676
+ highPriority += m.highPriorityCount;
3677
+ onlineAgents += m.onlineAgentCount;
3678
+ }
3679
+ el('stat-mailboxes').textContent = (s.mailboxes || []).length;
3680
+ el('stat-high').textContent = highPriority;
3681
+ el('stat-agents').textContent = onlineAgents;
3682
+
3683
+ renderMailboxes(s.mailboxes || []);
3684
+ renderClients(s.clients || []);
3685
+ renderProjectPicker(s.projects || []);
3686
+
3687
+ // Auto-refresh the open drawer if the open project is still active in
3688
+ // the live snapshot. Debounced so rapid burst updates trigger one fetch.
3689
+ if (currentDetailProjectId) {
3690
+ const stillActive = (s.projects || []).some((p) => p.projectId === currentDetailProjectId);
3691
+ if (stillActive) scheduleAutoRefresh();
3692
+ }
3693
+ }
3694
+
3695
+ // ---------- Project drilldown drawer ----------
3696
+
3697
+ // Current detail request token; if the URL changes (e.g. user picks a
3698
+ // different project) we discard stale responses.
3699
+ let currentDetailToken = 0;
3700
+ let currentDetailProjectId = null;
3701
+ let autoRefreshTimer = null;
3702
+ let lastAutoRefreshAt = null;
3703
+ // Per-project live event feed (ring buffer per project). Keyed by
3704
+ // projectId so switching drawers keeps each project's history.
3705
+ const eventFeeds = new Map();
3706
+ const FEED_MAX = 50;
3707
+ let feedIdleTimer = null;
3708
+
3709
+ function parseInitialProject() {
3710
+ // Prefer ?project=ID over #ID so URL copy/paste stays predictable even
3711
+ // when fragments would otherwise be lost on server round-trips.
3712
+ const url = new URL(location.href);
3713
+ const fromQuery = url.searchParams.get('project');
3714
+ if (fromQuery) return fromQuery;
3715
+ if (location.hash.length > 1) {
3716
+ try { return decodeURIComponent(location.hash.slice(1)); } catch { return null; }
3717
+ }
3718
+ return null;
3719
+ }
3720
+
3721
+ function pushProjectUrl(projectId) {
3722
+ const url = new URL(location.href);
3723
+ url.searchParams.set('project', projectId);
3724
+ url.hash = '';
3725
+ history.replaceState(null, '', url.pathname + url.search);
3726
+ }
3727
+
3728
+ function clearProjectUrl() {
3729
+ const url = new URL(location.href);
3730
+ url.searchParams.delete('project');
3731
+ url.hash = '';
3732
+ history.replaceState(null, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : ''));
3733
+ }
3734
+
3735
+ function renderProjectPicker(projects) {
3736
+ const sel = el('project-picker');
3737
+ if (!sel) return;
3738
+ const current = currentDetailProjectId || '';
3739
+ sel.innerHTML =
3740
+ '<option value="">\u2014 Select project \u2014</option>' +
3741
+ (projects || []).map((p) =>
3742
+ '<option value="' + escapeHtml(p.projectId) + '"' +
3743
+ (p.projectId === current ? ' selected' : '') +
3744
+ '>' + escapeHtml(p.projectName || p.projectId) + ' (' + (p.activeClients || 0) + ')</option>'
3745
+ ).join('');
3746
+ sel.onchange = () => {
3747
+ const v = sel.value;
3748
+ if (v) openProject(v);
3749
+ else closeProject();
3750
+ };
3751
+ }
3752
+
3753
+ function setDrawerMeta(detail) {
3754
+ if (!detail || !detail.project) return;
3755
+ const p = detail.project;
3756
+ const refreshed = lastAutoRefreshAt ? ' \xB7 Last refreshed ' + fmtTime(lastAutoRefreshAt) : '';
3757
+ el('drawer-meta').textContent =
3758
+ 'Active clients: ' + (p.activeClients || 0) +
3759
+ ' \xB7 Generated: ' + fmtTime(detail.generatedAt) +
3760
+ refreshed;
3761
+ }
3762
+
3763
+ function fetchProjectDetail(projectId, opts) {
3764
+ const token = ++currentDetailToken;
3765
+ const silent = opts && opts.silent;
3766
+ if (!silent) {
3767
+ el('drawer-meta').textContent = 'Refreshing\u2026';
3768
+ }
3769
+ fetch('/api/projects/' + encodeURIComponent(projectId))
3770
+ .then((r) => {
3771
+ if (!r.ok) throw new Error('HTTP ' + r.status);
3772
+ return r.json();
3773
+ })
3774
+ .then((d) => {
3775
+ if (token !== currentDetailToken) return; // stale
3776
+ renderProjectDetail(d);
3777
+ lastAutoRefreshAt = new Date();
3778
+ setDrawerMeta(d);
3779
+ })
3780
+ .catch((err) => {
3781
+ if (token !== currentDetailToken) return;
3782
+ if (!silent) {
3783
+ el('drawer-meta').textContent = 'Failed to load: ' + escapeHtml(String(err.message || err));
3784
+ }
3785
+ });
3786
+ }
3787
+
3788
+ function scheduleAutoRefresh() {
3789
+ if (autoRefreshTimer) clearTimeout(autoRefreshTimer);
3790
+ autoRefreshTimer = setTimeout(() => {
3791
+ autoRefreshTimer = null;
3792
+ if (currentDetailProjectId) fetchProjectDetail(currentDetailProjectId, { silent: true });
3793
+ }, 250);
3794
+ }
3795
+
3796
+ function openProject(projectId) {
3797
+ if (!projectId) return;
3798
+ const drawer = el('drawer');
3799
+ const backdrop = el('drawer-backdrop');
3800
+ el('drawer-title').textContent = projectId;
3801
+ el('drawer-meta').textContent = 'Loading\u2026';
3802
+ el('drawer-mailboxes').innerHTML = '<tr><td colspan="5" class="empty">Loading\u2026</td></tr>';
3803
+ el('drawer-messages').innerHTML = '<p class="empty">Loading\u2026</p>';
3804
+ el('drawer-clients').innerHTML = '<tr><td colspan="3" class="empty">Loading\u2026</td></tr>';
3805
+ drawer.classList.add('open');
3806
+ backdrop.classList.add('open');
3807
+ drawer.setAttribute('aria-hidden', 'false');
3808
+ pushProjectUrl(projectId);
3809
+ currentDetailProjectId = projectId;
3810
+ lastAutoRefreshAt = null;
3811
+ // Render any mailbox events the project received while the drawer was
3812
+ // closed so the live feed ring buffer is visible immediately on open.
3813
+ const existingFeed = eventFeeds.get(projectId);
3814
+ if (existingFeed) renderEventFeed(existingFeed);
3815
+ fetchProjectDetail(projectId, { silent: false });
3816
+ }
3817
+
3818
+ function closeProject() {
3819
+ const drawer = el('drawer');
3820
+ const backdrop = el('drawer-backdrop');
3821
+ drawer.classList.remove('open');
3822
+ backdrop.classList.remove('open');
3823
+ drawer.setAttribute('aria-hidden', 'true');
3824
+ currentDetailProjectId = null;
3825
+ currentDetailToken++;
3826
+ if (autoRefreshTimer) {
3827
+ clearTimeout(autoRefreshTimer);
3828
+ autoRefreshTimer = null;
3829
+ }
3830
+ lastAutoRefreshAt = null;
3831
+ clearProjectUrl();
3832
+ const sel = el('project-picker');
3833
+ if (sel) sel.value = '';
3834
+ }
3835
+
3836
+ function renderProjectDetail(detail) {
3837
+ if (!detail || !detail.project) {
3838
+ el('drawer-meta').textContent = 'No data.';
3839
+ return;
3840
+ }
3841
+ setDrawerMeta(detail);
3842
+
3843
+ // Mailboxes
3844
+ const mbs = detail.mailboxes || [];
3845
+ el('drawer-mailboxes').innerHTML = mbs.length === 0
3846
+ ? '<tr><td colspan="5" class="empty">No mailboxes reported for this project yet.</td></tr>'
3847
+ : mbs.map((m) => {
3848
+ const scopeClass = m.scope === 'global' ? 'global' : 'project';
3849
+ return '<tr>' +
3850
+ '<td><code>' + escapeHtml(shortId(m.mailboxId)) + '</code></td>' +
3851
+ '<td><span class="pill ' + scopeClass + '">' + escapeHtml(m.scope) + '</span></td>' +
3852
+ '<td class="num">' + m.totals.messages + '</td>' +
3853
+ '<td class="num">' + (m.totals.unread > 0 ? '<strong>' + m.totals.unread + '</strong>' : '0') + '</td>' +
3854
+ '<td class="num">' + m.totals.onlineAgents + '</td>' +
3855
+ '</tr>';
3856
+ }).join('');
3857
+
3858
+ // Recent messages \u2014 flatten + sort by timestamp desc, take 20
3859
+ const allMessages = [];
3860
+ for (const m of mbs) {
3861
+ for (const msg of (m.messages || [])) allMessages.push(msg);
3862
+ }
3863
+ allMessages.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
3864
+ const recent = allMessages.slice(0, 20);
3865
+ el('drawer-messages').innerHTML = recent.length === 0
3866
+ ? '<p class="empty">No messages in any mailbox snapshot yet.</p>'
3867
+ : recent.map((m) => {
3868
+ const priorityClass = 'priority-' + (m.priority || 'normal');
3869
+ const preview = m.bodyPreview ? '<div class="msg-preview">' + escapeHtml(m.bodyPreview) + '</div>' : '';
3870
+ const task = m.task ? ' \xB7 task: ' + escapeHtml(m.task.status || '?') : '';
3871
+ return '<div class="msg-row">' +
3872
+ '<span class="pill ' + priorityClass + '">' + escapeHtml(m.priority || 'normal') + '</span>' +
3873
+ '<span class="pill">' + escapeHtml(m.type || '?') + '</span>' +
3874
+ '<span class="msg-subject"> ' + escapeHtml(m.subject || '(no subject)') + '</span>' +
3875
+ '<div class="msg-meta">' + escapeHtml(m.from || '?') + ' \u2192 ' + escapeHtml(m.to || '?') + ' \xB7 ' + fmtTime(m.timestamp) + (m.completed ? ' \xB7 \u2713 completed' : '') + task + '</div>' +
3876
+ preview +
3877
+ '</div>';
3878
+ }).join('');
3879
+
3880
+ // Clients
3881
+ const cs = detail.clients || [];
3882
+ el('drawer-clients').innerHTML = cs.length === 0
3883
+ ? '<tr><td colspan="3" class="empty">No clients for this project.</td></tr>'
3884
+ : cs.map((c) =>
3885
+ '<tr>' +
3886
+ '<td><code>' + escapeHtml(shortId(c.clientId)) + '</code></td>' +
3887
+ '<td>' + escapeHtml(c.kind) + '</td>' +
3888
+ '<td>' + fmtTime(c.lastSeenAt) + '</td>' +
3889
+ '</tr>'
3890
+ ).join('');
3891
+ }
3892
+
3893
+ function wireProjectLinks() {
3894
+ const links = document.querySelectorAll('a.project-link');
3895
+ links.forEach((a) => {
3896
+ a.onclick = (ev) => {
3897
+ ev.preventDefault();
3898
+ const pid = a.getAttribute('data-project') || '';
3899
+ openProject(pid);
3900
+ };
3901
+ });
3902
+ }
3903
+
3904
+ el('drawer-close').onclick = closeProject;
3905
+ el('drawer-backdrop').onclick = closeProject;
3906
+ document.addEventListener('keydown', (ev) => {
3907
+ if (ev.key === 'Escape') closeProject();
3908
+ });
3909
+
3910
+ // Respond to browser back/forward to switch projects.
3911
+ window.addEventListener('popstate', () => {
3912
+ const pid = parseInitialProject();
3913
+ if (pid) {
3914
+ if (pid !== currentDetailProjectId) openProject(pid);
3915
+ } else if (currentDetailProjectId) {
3916
+ closeProject();
3917
+ }
3918
+ });
3919
+
3920
+ // Open drawer automatically if URL has ?project=ID or #projectId.
3921
+ const initialProject = parseInitialProject();
3922
+ if (initialProject) openProject(initialProject);
3923
+
3924
+ function connect() {
3925
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
3926
+ const ws = new WebSocket(proto + '//' + location.host + '/ws/browser');
3927
+ ws.onopen = () => {
3928
+ led.className = 'hq-led live';
3929
+ connText.innerHTML = '<span class="hq-led live"></span>Connected to HQ';
3930
+ };
3931
+ ws.onmessage = (ev) => {
3932
+ try {
3933
+ const msg = JSON.parse(ev.data);
3934
+ if (msg.type === 'hq.snapshot') applySnapshot(msg.snapshot);
3935
+ else if (msg.type === 'hq.event') handleHqEvent(msg.event);
3936
+ } catch {}
3937
+ };
3938
+ ws.onclose = () => {
3939
+ led.className = 'hq-led dead';
3940
+ connText.innerHTML = '<span class="hq-led dead"></span>Disconnected \u2014 reconnecting\u2026';
3941
+ setTimeout(connect, 2000);
3942
+ };
3943
+ ws.onerror = () => ws.close();
3944
+ }
3945
+
3946
+ // ---------- Live mailbox event feed ----------
3947
+
3948
+ function handleHqEvent(event) {
3949
+ if (!event || event.type !== 'mailbox.event') return;
3950
+ const projectId = event.projectId;
3951
+ if (!projectId) return;
3952
+ const list = eventFeeds.get(projectId) || [];
3953
+ list.unshift(event); // newest first
3954
+ if (list.length > FEED_MAX) list.length = FEED_MAX;
3955
+ eventFeeds.set(projectId, list);
3956
+
3957
+ // Only re-render if the open drawer matches this project's id.
3958
+ if (projectId === currentDetailProjectId) {
3959
+ renderEventFeed(list);
3960
+ flashFeedStatus();
3961
+ }
3962
+ }
3963
+
3964
+ function renderEventFeed(list) {
3965
+ const elFeed = el('drawer-event-feed');
3966
+ if (!elFeed) return;
3967
+ if (!list || list.length === 0) {
3968
+ elFeed.innerHTML = '<p class="empty">No mailbox events yet for this project.</p>';
3969
+ return;
3970
+ }
3971
+ elFeed.innerHTML = list.map((evt) => {
3972
+ const p = evt.payload || {};
3973
+ const action = escapeHtml(p.action || '?');
3974
+ let detail = '';
3975
+ if (p.summary) detail = escapeHtml(p.summary);
3976
+ else if (p.message) detail = escapeHtml((p.message.subject || '(no subject)') + ' \xB7 ' + (p.message.from || '?') + ' \u2192 ' + (p.message.to || '?'));
3977
+ else if (p.agent) detail = escapeHtml((p.agent.name || p.agent.agentId || '?') + ' (' + (p.agent.status || '?') + ')');
3978
+ else detail = '<em>no detail</em>';
3979
+ const mailboxShort = p.mailboxId ? ' \xB7 ' + escapeHtml(shortId(p.mailboxId)) : '';
3980
+ return '<div class="feed-row">' +
3981
+ '<span class="feed-action ' + action + '">' + action + '</span>' +
3982
+ '<span>' + detail + '</span>' +
3983
+ '<div class="feed-meta">' + fmtTime(evt.timestamp) + mailboxShort + '</div>' +
3984
+ '</div>';
3985
+ }).join('');
3986
+ }
3987
+
3988
+ function clearEventFeed(projectId) {
3989
+ eventFeeds.delete(projectId);
3990
+ }
3991
+
3992
+ function flashFeedStatus() {
3993
+ const status = el('feed-status');
3994
+ if (!status) return;
3995
+ status.textContent = '(live)';
3996
+ status.className = 'feed-status live';
3997
+ if (feedIdleTimer) clearTimeout(feedIdleTimer);
3998
+ feedIdleTimer = setTimeout(() => {
3999
+ status.textContent = '(idle)';
4000
+ status.className = 'feed-status';
4001
+ feedIdleTimer = null;
4002
+ }, 1500);
4003
+ }
4004
+
4005
+ connect();
4006
+ </script>
4007
+ </body>
4008
+ </html>`;
4009
+ }
4010
+ });
4011
+
2909
4012
  // src/update-check.ts
2910
4013
  var update_check_exports = {};
2911
4014
  __export(update_check_exports, {
@@ -3457,10 +4560,10 @@ function handlePing(ctx, ws) {
3457
4560
  ctx.send(ws, { type: "pong", payload: {} });
3458
4561
  }
3459
4562
  function handleToolConfirmResult(ctx, id, decision) {
3460
- const resolve11 = ctx.pendingConfirms.get(id);
3461
- if (resolve11) {
4563
+ const resolve12 = ctx.pendingConfirms.get(id);
4564
+ if (resolve12) {
3462
4565
  ctx.pendingConfirms.delete(id);
3463
- resolve11(decision);
4566
+ resolve12(decision);
3464
4567
  }
3465
4568
  }
3466
4569
  var init_connection = __esm({
@@ -3872,8 +4975,8 @@ async function handleProjectsSelect(ctx, ws, payload) {
3872
4975
  const { root, name: projectName } = payload;
3873
4976
  try {
3874
4977
  const resolved = path4.resolve(root);
3875
- const stat7 = await fsp5.stat(resolved).catch(() => null);
3876
- if (!stat7?.isDirectory()) {
4978
+ const stat8 = await fsp5.stat(resolved).catch(() => null);
4979
+ if (!stat8?.isDirectory()) {
3877
4980
  ctx.send(ws, {
3878
4981
  type: "projects.selected",
3879
4982
  payload: {
@@ -3964,8 +5067,8 @@ async function handleProjectsAdd(ctx, ws, payload) {
3964
5067
  const { root: addRoot, name: addName } = payload;
3965
5068
  try {
3966
5069
  const resolved = path4.resolve(addRoot);
3967
- const stat7 = await fsp5.stat(resolved).catch(() => null);
3968
- if (!stat7?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
5070
+ const stat8 = await fsp5.stat(resolved).catch(() => null);
5071
+ if (!stat8?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
3969
5072
  const manifest = await loadManifest(ctx.opts.globalConfigPath);
3970
5073
  const existing = manifest.projects.find((p) => path4.resolve(p.root) === resolved);
3971
5074
  if (existing) {
@@ -4010,8 +5113,8 @@ async function handleWorkingDirSet(ctx, ws, newPath) {
4010
5113
  sendResult6(ctx, ws, false, `Path must stay inside the project root: ${wdRoot}`);
4011
5114
  return;
4012
5115
  }
4013
- const stat7 = await fsp5.stat(resolved).catch(() => null);
4014
- if (!stat7?.isDirectory()) {
5116
+ const stat8 = await fsp5.stat(resolved).catch(() => null);
5117
+ if (!stat8?.isDirectory()) {
4015
5118
  sendResult6(ctx, ws, false, `Directory not found or not accessible: ${resolved}`);
4016
5119
  return;
4017
5120
  }
@@ -4750,6 +5853,10 @@ async function runWebUI(opts) {
4750
5853
  "contextStrategy",
4751
5854
  "logLevel",
4752
5855
  "auditLevel",
5856
+ "hqEnabled",
5857
+ "hqUrl",
5858
+ "hqToken",
5859
+ "hqRawContent",
4753
5860
  // Telegram plugin notification settings (parity with the standalone server).
4754
5861
  "tgConfigured",
4755
5862
  "tgSessionEnd",
@@ -4793,6 +5900,11 @@ async function runWebUI(opts) {
4793
5900
  meta["logLevel"] = cfg.log?.["level"] ?? "info";
4794
5901
  meta["auditLevel"] = cfg.session?.["auditLevel"] ?? "standard";
4795
5902
  meta["maxIterations"] = cfg.tools?.["maxIterations"] ?? 500;
5903
+ const hqCfg = cfg.hq ?? {};
5904
+ meta["hqEnabled"] = hqCfg["enabled"] === true;
5905
+ meta["hqUrl"] = typeof hqCfg["url"] === "string" ? hqCfg["url"] : "";
5906
+ meta["hqToken"] = typeof hqCfg["token"] === "string" ? hqCfg["token"] : "";
5907
+ meta["hqRawContent"] = hqCfg["rawContent"] === true;
4796
5908
  const tgExt = cfg.extensions?.["telegram"];
4797
5909
  meta["tgConfigured"] = typeof tgExt?.["botToken"] === "string" && tgExt["botToken"].length > 0;
4798
5910
  meta["tgSessionEnd"] = tgExt?.["notifyOnSessionEnd"] === true;
@@ -4893,6 +6005,15 @@ async function runWebUI(opts) {
4893
6005
  toolsCfg.maxIterations = payload["maxIterations"];
4894
6006
  decrypted.tools = toolsCfg;
4895
6007
  }
6008
+ const hqTouched = typeof payload["hqEnabled"] === "boolean" || typeof payload["hqUrl"] === "string" || typeof payload["hqToken"] === "string" || typeof payload["hqRawContent"] === "boolean";
6009
+ if (hqTouched) {
6010
+ const hqCfg = decrypted.hq ?? {};
6011
+ if (typeof payload["hqEnabled"] === "boolean") hqCfg.enabled = payload["hqEnabled"];
6012
+ if (typeof payload["hqUrl"] === "string") hqCfg.url = payload["hqUrl"];
6013
+ if (typeof payload["hqToken"] === "string") hqCfg.token = payload["hqToken"];
6014
+ if (typeof payload["hqRawContent"] === "boolean") hqCfg.rawContent = payload["hqRawContent"];
6015
+ decrypted.hq = hqCfg;
6016
+ }
4896
6017
  const tgTouched = typeof payload["tgSessionEnd"] === "boolean" || typeof payload["tgDelegate"] === "boolean" || typeof payload["tgLongToolMs"] === "number";
4897
6018
  if (tgTouched) {
4898
6019
  const ext = decrypted.extensions ?? {};
@@ -4979,7 +6100,9 @@ async function runWebUI(opts) {
4979
6100
  if (!opts.projectRoot) return null;
4980
6101
  try {
4981
6102
  const projectDir = resolveProjectDir(opts.projectRoot, wstackGlobalRoot());
4982
- const mailbox = new GlobalMailbox(projectDir, opts.events);
6103
+ const hqPublisher = createHqPublisherFromEnv({ clientKind: "webui", projectRoot: opts.projectRoot, projectName: path4.basename(opts.projectRoot), appConfig: opts.appConfig });
6104
+ hqPublisher?.connect();
6105
+ const mailbox = new GlobalMailbox(projectDir, opts.events, hqPublisher);
4983
6106
  webuiClientId = `webui@${crypto3.randomUUID().slice(0, 8)}`;
4984
6107
  const projectName = opts.projectRoot ? path4.basename(opts.projectRoot) : "unknown";
4985
6108
  await mailbox.registerClient({
@@ -5053,6 +6176,113 @@ async function runWebUI(opts) {
5053
6176
  type: "fleet.concurrency_update",
5054
6177
  payload: { fleetConcurrency, fleetConcurrencyMax: FLEET_CONCURRENCY_MAX }
5055
6178
  });
6179
+ const STREAM_COALESCE_MS = 16;
6180
+ const STREAM_COALESCE_MAX_CHARS = 8 * 1024;
6181
+ let textDeltaBuffer = "";
6182
+ let textDeltaTimer = null;
6183
+ let thinkingDeltaBuffer = "";
6184
+ let thinkingDeltaTimer = null;
6185
+ const toolProgressBuffers = /* @__PURE__ */ new Map();
6186
+ const flushTextDelta = () => {
6187
+ if (textDeltaTimer) {
6188
+ clearTimeout(textDeltaTimer);
6189
+ textDeltaTimer = null;
6190
+ }
6191
+ if (!textDeltaBuffer) return;
6192
+ const text = textDeltaBuffer;
6193
+ textDeltaBuffer = "";
6194
+ broadcast({
6195
+ type: "provider.text_delta",
6196
+ payload: { text, messageId: "current" }
6197
+ });
6198
+ };
6199
+ const flushThinkingDelta = () => {
6200
+ if (thinkingDeltaTimer) {
6201
+ clearTimeout(thinkingDeltaTimer);
6202
+ thinkingDeltaTimer = null;
6203
+ }
6204
+ if (!thinkingDeltaBuffer) return;
6205
+ const text = thinkingDeltaBuffer;
6206
+ thinkingDeltaBuffer = "";
6207
+ broadcast({
6208
+ type: "provider.thinking_delta",
6209
+ payload: { text }
6210
+ });
6211
+ };
6212
+ const queueTextDelta = (text) => {
6213
+ if (!text) return;
6214
+ textDeltaBuffer += text;
6215
+ if (textDeltaBuffer.length >= STREAM_COALESCE_MAX_CHARS) {
6216
+ flushTextDelta();
6217
+ return;
6218
+ }
6219
+ if (!textDeltaTimer) {
6220
+ textDeltaTimer = setTimeout(flushTextDelta, STREAM_COALESCE_MS);
6221
+ textDeltaTimer.unref?.();
6222
+ }
6223
+ };
6224
+ const queueThinkingDelta = (text) => {
6225
+ if (!text) return;
6226
+ thinkingDeltaBuffer += text;
6227
+ if (thinkingDeltaBuffer.length >= STREAM_COALESCE_MAX_CHARS) {
6228
+ flushThinkingDelta();
6229
+ return;
6230
+ }
6231
+ if (!thinkingDeltaTimer) {
6232
+ thinkingDeltaTimer = setTimeout(flushThinkingDelta, STREAM_COALESCE_MS);
6233
+ thinkingDeltaTimer.unref?.();
6234
+ }
6235
+ };
6236
+ const flushToolProgress = (id) => {
6237
+ const buffered = toolProgressBuffers.get(id);
6238
+ if (!buffered) return;
6239
+ if (buffered.timer) clearTimeout(buffered.timer);
6240
+ toolProgressBuffers.delete(id);
6241
+ if (!buffered.text) return;
6242
+ broadcast({
6243
+ type: "tool.progress",
6244
+ payload: {
6245
+ name: buffered.name,
6246
+ id: buffered.id,
6247
+ event: { type: buffered.eventType, text: buffered.text }
6248
+ }
6249
+ });
6250
+ };
6251
+ const flushAllStreamBuffers = () => {
6252
+ flushTextDelta();
6253
+ flushThinkingDelta();
6254
+ for (const id of [...toolProgressBuffers.keys()]) flushToolProgress(id);
6255
+ };
6256
+ const queueToolProgress = (payload) => {
6257
+ const text = payload.event.text;
6258
+ if (!text) {
6259
+ flushToolProgress(payload.id);
6260
+ broadcast({ type: "tool.progress", payload });
6261
+ return;
6262
+ }
6263
+ const eventType = payload.event.type ?? "progress";
6264
+ const existing = toolProgressBuffers.get(payload.id);
6265
+ if (existing && existing.eventType !== eventType) flushToolProgress(payload.id);
6266
+ const buffered = toolProgressBuffers.get(payload.id) ?? {
6267
+ id: payload.id,
6268
+ name: payload.name,
6269
+ eventType,
6270
+ text: "",
6271
+ timer: null
6272
+ };
6273
+ buffered.name = payload.name;
6274
+ buffered.text += buffered.text ? `
6275
+ ${text}` : text;
6276
+ toolProgressBuffers.set(payload.id, buffered);
6277
+ if (buffered.text.length >= STREAM_COALESCE_MAX_CHARS) {
6278
+ flushToolProgress(payload.id);
6279
+ return;
6280
+ }
6281
+ if (!buffered.timer) {
6282
+ buffered.timer = setTimeout(() => flushToolProgress(payload.id), STREAM_COALESCE_MS);
6283
+ buffered.timer.unref?.();
6284
+ }
6285
+ };
5056
6286
  function setupEvents() {
5057
6287
  for (const unsub of eventUnsubscribers) unsub();
5058
6288
  eventUnsubscribers.length = 0;
@@ -5081,22 +6311,18 @@ async function runWebUI(opts) {
5081
6311
  );
5082
6312
  eventUnsubscribers.push(
5083
6313
  opts.events.on("provider.text_delta", (e) => {
5084
- broadcast({
5085
- type: "provider.text_delta",
5086
- payload: { text: e.text, messageId: "current" }
5087
- });
6314
+ flushThinkingDelta();
6315
+ queueTextDelta(e.text);
5088
6316
  })
5089
6317
  );
5090
6318
  eventUnsubscribers.push(
5091
6319
  opts.events.on("provider.thinking_delta", (e) => {
5092
- broadcast({
5093
- type: "provider.thinking_delta",
5094
- payload: { text: e.text }
5095
- });
6320
+ queueThinkingDelta(e.text);
5096
6321
  })
5097
6322
  );
5098
6323
  eventUnsubscribers.push(
5099
6324
  opts.events.on("tool.started", (e) => {
6325
+ flushAllStreamBuffers();
5100
6326
  broadcast({
5101
6327
  type: "tool.started",
5102
6328
  payload: {
@@ -5110,18 +6336,16 @@ async function runWebUI(opts) {
5110
6336
  );
5111
6337
  eventUnsubscribers.push(
5112
6338
  opts.events.on("tool.progress", (e) => {
5113
- broadcast({
5114
- type: "tool.progress",
5115
- payload: {
5116
- name: e.name,
5117
- id: e.id,
5118
- event: e.event
5119
- }
6339
+ queueToolProgress({
6340
+ name: e.name,
6341
+ id: e.id,
6342
+ event: e.event
5120
6343
  });
5121
6344
  })
5122
6345
  );
5123
6346
  eventUnsubscribers.push(
5124
6347
  opts.events.on("tool.executed", (e) => {
6348
+ flushAllStreamBuffers();
5125
6349
  broadcast({
5126
6350
  type: "tool.executed",
5127
6351
  payload: {
@@ -5178,6 +6402,7 @@ async function runWebUI(opts) {
5178
6402
  );
5179
6403
  eventUnsubscribers.push(
5180
6404
  opts.events.on("provider.response", (e) => {
6405
+ flushAllStreamBuffers();
5181
6406
  broadcast({
5182
6407
  type: "provider.response",
5183
6408
  payload: {
@@ -5392,6 +6617,19 @@ async function runWebUI(opts) {
5392
6617
  log: (m) => console.log(m)
5393
6618
  };
5394
6619
  const wsCommon = { send, broadcast, log: (m) => console.log(m) };
6620
+ const mailboxCache = /* @__PURE__ */ new Map();
6621
+ const getWebuiMailbox = () => {
6622
+ const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6623
+ const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
6624
+ if (!projectRoot || !globalRoot) return null;
6625
+ const mbDir = resolveProjectDir(projectRoot, globalRoot);
6626
+ let mailbox = mailboxCache.get(mbDir);
6627
+ if (!mailbox) {
6628
+ mailbox = new GlobalMailbox(mbDir, opts.events);
6629
+ mailboxCache.set(mbDir, mailbox);
6630
+ }
6631
+ return mailbox;
6632
+ };
5395
6633
  const sessionsCtx = {
5396
6634
  opts,
5397
6635
  buildSessionStart: (overrides) => buildSessionStartPayload(overrides),
@@ -5407,7 +6645,7 @@ async function runWebUI(opts) {
5407
6645
  broadcast,
5408
6646
  log: (m) => console.log(m)
5409
6647
  };
5410
- return new Promise((resolve11) => {
6648
+ return new Promise((resolve12) => {
5411
6649
  wss.on("listening", () => {
5412
6650
  console.log(`[WebUI] WebSocket server running on ws://${host}:${port}`);
5413
6651
  setupEvents();
@@ -5525,8 +6763,8 @@ async function runWebUI(opts) {
5525
6763
  clients.delete(ws);
5526
6764
  abortControllers.delete(ws);
5527
6765
  if (clients.size === 0 && pendingConfirms.size > 0) {
5528
- for (const [id, resolve12] of pendingConfirms) {
5529
- resolve12("no");
6766
+ for (const [id, resolve13] of pendingConfirms) {
6767
+ resolve13("no");
5530
6768
  pendingConfirms.delete(id);
5531
6769
  }
5532
6770
  }
@@ -5553,6 +6791,7 @@ async function runWebUI(opts) {
5553
6791
  abortControllers.clear();
5554
6792
  },
5555
6793
  unsubscribeEvents: () => {
6794
+ flushAllStreamBuffers();
5556
6795
  for (const unsub of eventUnsubscribers) unsub();
5557
6796
  },
5558
6797
  closeClients: () => {
@@ -5565,7 +6804,7 @@ async function runWebUI(opts) {
5565
6804
  wss,
5566
6805
  pid: process.pid,
5567
6806
  registryBaseDir,
5568
- onStopped: resolve11
6807
+ onStopped: resolve12
5569
6808
  });
5570
6809
  registerWebuiSignalHandlers(signalShutdown);
5571
6810
  });
@@ -6005,6 +7244,16 @@ async function runWebUI(opts) {
6005
7244
  case "collab.request_pause":
6006
7245
  case "collab.resume":
6007
7246
  break;
7247
+ // Integrated terminal — the CLI embedded server doesn't run a pty
7248
+ // transport (it already has its own terminal); silently acknowledge
7249
+ // and ignore so the browser client's terminal panel doesn't trip the
7250
+ // "Unhandled message type" warning. The standalone webui server wires
7251
+ // the real TerminalWebSocketHandler.
7252
+ case "terminal.create":
7253
+ case "terminal.input":
7254
+ case "terminal.resize":
7255
+ case "terminal.close":
7256
+ break;
6008
7257
  case "projects.list": {
6009
7258
  await handleProjectsList(projectsCtx, ws);
6010
7259
  break;
@@ -6066,9 +7315,8 @@ async function runWebUI(opts) {
6066
7315
  break;
6067
7316
  // ── Mailbox operations — project-level inter-agent messaging ────
6068
7317
  case "mailbox.messages": {
6069
- const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6070
- const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
6071
- if (!projectRoot || !globalRoot) {
7318
+ const mb = getWebuiMailbox();
7319
+ if (!mb) {
6072
7320
  send(ws, {
6073
7321
  type: "mailbox.messages",
6074
7322
  payload: { messages: [], error: "No project root available" }
@@ -6076,13 +7324,12 @@ async function runWebUI(opts) {
6076
7324
  break;
6077
7325
  }
6078
7326
  try {
6079
- const mbDir = resolveProjectDir(projectRoot, globalRoot);
6080
- const mb = new GlobalMailbox(mbDir);
6081
7327
  const payload = msg.payload;
6082
7328
  const messages = await mb.query({
6083
7329
  limit: payload?.limit ?? 30,
6084
7330
  to: payload?.agentId,
6085
- unreadBy: payload?.unreadOnly ? payload.agentId : void 0
7331
+ unreadBy: payload?.unreadOnly ? payload.agentId : void 0,
7332
+ incompleteOnly: payload?.incompleteOnly
6086
7333
  });
6087
7334
  send(ws, {
6088
7335
  type: "mailbox.messages",
@@ -6115,9 +7362,8 @@ async function runWebUI(opts) {
6115
7362
  break;
6116
7363
  }
6117
7364
  case "mailbox.agents": {
6118
- const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6119
- const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
6120
- if (!projectRoot || !globalRoot) {
7365
+ const mb = getWebuiMailbox();
7366
+ if (!mb) {
6121
7367
  send(ws, {
6122
7368
  type: "mailbox.agents",
6123
7369
  payload: { agents: [], error: "No project root available" }
@@ -6125,8 +7371,6 @@ async function runWebUI(opts) {
6125
7371
  break;
6126
7372
  }
6127
7373
  try {
6128
- const mbDir = resolveProjectDir(projectRoot, globalRoot);
6129
- const mb = new GlobalMailbox(mbDir);
6130
7374
  const payload = msg.payload;
6131
7375
  const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
6132
7376
  send(ws, {
@@ -6158,15 +7402,12 @@ async function runWebUI(opts) {
6158
7402
  break;
6159
7403
  }
6160
7404
  case "mailbox.clear": {
6161
- const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6162
- const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
6163
- if (!projectRoot || !globalRoot) {
7405
+ const mb = getWebuiMailbox();
7406
+ if (!mb) {
6164
7407
  send(ws, { type: "mailbox.cleared", payload: { error: "No project root available" } });
6165
7408
  break;
6166
7409
  }
6167
7410
  try {
6168
- const mbDir = resolveProjectDir(projectRoot, globalRoot);
6169
- const mb = new GlobalMailbox(mbDir);
6170
7411
  await mb.clearAll();
6171
7412
  send(ws, { type: "mailbox.cleared", payload: {} });
6172
7413
  } catch (err) {
@@ -6178,15 +7419,12 @@ async function runWebUI(opts) {
6178
7419
  break;
6179
7420
  }
6180
7421
  case "mailbox.purge": {
6181
- const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6182
- const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
6183
- if (!projectRoot || !globalRoot) {
7422
+ const mb = getWebuiMailbox();
7423
+ if (!mb) {
6184
7424
  send(ws, { type: "mailbox.purged", payload: { error: "No project root available" } });
6185
7425
  break;
6186
7426
  }
6187
7427
  try {
6188
- const mbDir = resolveProjectDir(projectRoot, globalRoot);
6189
- const mb = new GlobalMailbox(mbDir);
6190
7428
  const payload = msg;
6191
7429
  const result = await mb.purgeStale(payload.payload);
6192
7430
  send(ws, { type: "mailbox.purged", payload: result });
@@ -6223,6 +7461,7 @@ async function runWebUI(opts) {
6223
7461
  }
6224
7462
  function shutdown() {
6225
7463
  console.log("[WebUI] Shutting down...");
7464
+ flushAllStreamBuffers();
6226
7465
  unregisterWebuiClient();
6227
7466
  httpServer?.server.close();
6228
7467
  opts.onExit?.();
@@ -6258,7 +7497,7 @@ init_provider_config_utils();
6258
7497
  var WORKTREE_PHASE_CONCURRENCY = 4;
6259
7498
  var MAX_CMD_OUTPUT = 2e5;
6260
7499
  function gitText(args, cwd) {
6261
- return new Promise((resolve11, reject) => {
7500
+ return new Promise((resolve12, reject) => {
6262
7501
  let child;
6263
7502
  try {
6264
7503
  child = spawn("git", args, {
@@ -6278,8 +7517,8 @@ function gitText(args, cwd) {
6278
7517
  };
6279
7518
  child.stdout?.on("data", emit);
6280
7519
  child.stderr?.on("data", emit);
6281
- child.on("error", () => resolve11({ code: 1, out: chunks.join("") }));
6282
- child.on("close", (code) => resolve11({ code: code ?? 1, out: chunks.join("").trim() }));
7520
+ child.on("error", () => resolve12({ code: 1, out: chunks.join("") }));
7521
+ child.on("close", (code) => resolve12({ code: code ?? 1, out: chunks.join("").trim() }));
6283
7522
  });
6284
7523
  }
6285
7524
  async function isGitRepo(cwd) {
@@ -6315,7 +7554,7 @@ function runCmd(cmd, args, cwd, shell = false) {
6315
7554
  });
6316
7555
  }
6317
7556
  }
6318
- return new Promise((resolve11, reject) => {
7557
+ return new Promise((resolve12, reject) => {
6319
7558
  const chunks = [];
6320
7559
  let child;
6321
7560
  try {
@@ -6338,11 +7577,11 @@ function runCmd(cmd, args, cwd, shell = false) {
6338
7577
  };
6339
7578
  child.stdout?.on("data", append);
6340
7579
  child.stderr?.on("data", append);
6341
- child.on("error", (e) => resolve11({ code: 1, out: `${chunks.join("")}${String(e)}` }));
7580
+ child.on("error", (e) => resolve12({ code: 1, out: `${chunks.join("")}${String(e)}` }));
6342
7581
  child.on("close", (code) => {
6343
7582
  let out = chunks.join("");
6344
7583
  if (out.length > MAX_CMD_OUTPUT) out = out.slice(-MAX_CMD_OUTPUT);
6345
- resolve11({ code: code ?? 1, out: out.trim() });
7584
+ resolve12({ code: code ?? 1, out: out.trim() });
6346
7585
  });
6347
7586
  });
6348
7587
  }
@@ -6698,7 +7937,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
6698
7937
  "skip-index",
6699
7938
  "mouse",
6700
7939
  "no-interactive",
6701
- "token-saving-mode"
7940
+ "token-saving-mode",
7941
+ "hq"
6702
7942
  ]);
6703
7943
  function parseArgs(argv) {
6704
7944
  const flags = {};
@@ -6849,31 +8089,31 @@ var ReadlineInputReader = class {
6849
8089
  async readLine(prompt) {
6850
8090
  if (this.history.length === 0) await this.loadHistory();
6851
8091
  while (this.pending) {
6852
- await new Promise((resolve11) => setTimeout(resolve11, 50));
8092
+ await new Promise((resolve12) => setTimeout(resolve12, 50));
6853
8093
  }
6854
8094
  this.pending = true;
6855
8095
  try {
6856
8096
  if (this.rl) {
6857
8097
  const old = this.rl;
6858
8098
  this.rl = void 0;
6859
- await new Promise((resolve11) => {
8099
+ await new Promise((resolve12) => {
6860
8100
  if (old.closed) {
6861
- resolve11();
8101
+ resolve12();
6862
8102
  } else {
6863
- old.once("close", resolve11);
8103
+ old.once("close", resolve12);
6864
8104
  old.close();
6865
8105
  }
6866
8106
  });
6867
8107
  }
6868
8108
  const fresh = this.ensure();
6869
8109
  this.installPromptGuard(fresh);
6870
- return new Promise((resolve11) => {
8110
+ return new Promise((resolve12) => {
6871
8111
  let settled = false;
6872
8112
  const settle = (line) => {
6873
8113
  if (settled) return;
6874
8114
  settled = true;
6875
8115
  setOutputLineGuard(null);
6876
- resolve11(line);
8116
+ resolve12(line);
6877
8117
  };
6878
8118
  fresh.question(prompt ?? "> ", (line) => {
6879
8119
  if (line.trim()) {
@@ -6926,7 +8166,7 @@ var ReadlineInputReader = class {
6926
8166
  async readKey(prompt, options) {
6927
8167
  setOutputLineGuard(null);
6928
8168
  writeOut(prompt);
6929
- return new Promise((resolve11) => {
8169
+ return new Promise((resolve12) => {
6930
8170
  const stdin = process.stdin;
6931
8171
  const wasRaw = stdin.isRaw;
6932
8172
  const wasPaused = stdin.isPaused();
@@ -6937,7 +8177,7 @@ var ReadlineInputReader = class {
6937
8177
  if (key === "") {
6938
8178
  cleanup();
6939
8179
  writeOut("\n");
6940
- resolve11("");
8180
+ resolve12("");
6941
8181
  return;
6942
8182
  }
6943
8183
  const opt = options.find(
@@ -6947,12 +8187,12 @@ var ReadlineInputReader = class {
6947
8187
  cleanup();
6948
8188
  writeOut(`${opt.key}
6949
8189
  `);
6950
- resolve11(opt.value);
8190
+ resolve12(opt.value);
6951
8191
  }
6952
8192
  };
6953
8193
  const onClose = () => {
6954
8194
  cleanup();
6955
- resolve11("");
8195
+ resolve12("");
6956
8196
  };
6957
8197
  const cleanup = () => {
6958
8198
  stdin.off("data", onData);
@@ -6981,7 +8221,7 @@ var ReadlineInputReader = class {
6981
8221
  this.rl?.close();
6982
8222
  this.rl = void 0;
6983
8223
  writeOut(prompt);
6984
- return new Promise((resolve11) => {
8224
+ return new Promise((resolve12) => {
6985
8225
  let buf = "";
6986
8226
  const wasRaw = stdin.isRaw;
6987
8227
  setRawMode(stdin, true);
@@ -6999,7 +8239,7 @@ var ReadlineInputReader = class {
6999
8239
  cleanup();
7000
8240
  writeOut(` ${dim(`[${buf.length} chars]`)}
7001
8241
  `);
7002
- resolve11(buf);
8242
+ resolve12(buf);
7003
8243
  return;
7004
8244
  }
7005
8245
  if (ch === "") {
@@ -7194,8 +8434,8 @@ function printLaunchHints(renderer, flags, opts = {}) {
7194
8434
  var defaultUidFn = () => os__default.userInfo().uid;
7195
8435
  async function getFileUid(filePath) {
7196
8436
  try {
7197
- const stat7 = await fsp5.stat(filePath);
7198
- return stat7.uid;
8437
+ const stat8 = await fsp5.stat(filePath);
8438
+ return stat8.uid;
7199
8439
  } catch {
7200
8440
  return void 0;
7201
8441
  }
@@ -7530,11 +8770,11 @@ async function buildPickableProviders(modelsRegistry, config) {
7530
8770
  var theme = { primary: color.amber };
7531
8771
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os__default.homedir()) {
7532
8772
  try {
7533
- const { atomicWrite: atomicWrite16 } = await import('@wrongstack/core');
7534
- const fs38 = await import('fs/promises');
8773
+ const { atomicWrite: atomicWrite15 } = await import('@wrongstack/core');
8774
+ const fs36 = await import('fs/promises');
7535
8775
  let existing = {};
7536
8776
  try {
7537
- const raw = await fs38.readFile(configPath2, "utf8");
8777
+ const raw = await fs36.readFile(configPath2, "utf8");
7538
8778
  existing = JSON.parse(raw);
7539
8779
  } catch {
7540
8780
  }
@@ -7553,7 +8793,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
7553
8793
  })
7554
8794
  );
7555
8795
  }
7556
- await atomicWrite16(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
8796
+ await atomicWrite15(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7557
8797
  try {
7558
8798
  await appendHistory(
7559
8799
  oldCfg,
@@ -9220,7 +10460,7 @@ The following characters are not allowed: ; & | < > ^ $ , ( ) { } [ ] ! # % ' "
9220
10460
  }
9221
10461
  }
9222
10462
  function runCommand(cmd, cwd, timeout) {
9223
- return new Promise((resolve11) => {
10463
+ return new Promise((resolve12) => {
9224
10464
  validateCommand(cmd);
9225
10465
  const opts = {
9226
10466
  cwd,
@@ -9233,7 +10473,7 @@ function runCommand(cmd, cwd, timeout) {
9233
10473
  shell: process.platform === "win32" ? true : false
9234
10474
  };
9235
10475
  execFile(cmd, [], opts, (error, stdout, stderr) => {
9236
- resolve11({
10476
+ resolve12({
9237
10477
  stdout,
9238
10478
  stderr,
9239
10479
  exitCode: typeof error?.code === "number" ? error.code : 0,
@@ -9679,6 +10919,7 @@ function formatDelay(ms) {
9679
10919
 
9680
10920
  // src/settings-menu.ts
9681
10921
  function resolvePersistPath(deps) {
10922
+ if (deps.forceGlobal) return deps.globalConfigPath;
9682
10923
  const scope = deps.configStore.get().configScope;
9683
10924
  if (scope === "project" && deps.inProjectConfigPath) {
9684
10925
  return deps.inProjectConfigPath;
@@ -12770,7 +14011,7 @@ function parseMcpArgs(args) {
12770
14011
  }
12771
14012
  async function runMcpManagementCommand(parsed, deps) {
12772
14013
  const { config, configPath: configPath2, mcpRegistry, allServerPresets } = deps;
12773
- const diskConfig = await readConfig(configPath2);
14014
+ const diskConfig = await readJsonObjectFile(configPath2);
12774
14015
  const configured = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : config.mcpServers ?? {};
12775
14016
  switch (parsed.action) {
12776
14017
  case "list":
@@ -12835,13 +14076,10 @@ async function runAdd(name, enable, configured, configPath2, mcpRegistry, all) {
12835
14076
  }
12836
14077
  const existing = configured[name];
12837
14078
  const nextCfg = existing ? { ...preset, ...existing, enabled: enable } : { ...preset, enabled: enable };
12838
- const full = await readConfig(configPath2);
12839
- const mcpServers = {
12840
- ...isMcpServerRecord(full.mcpServers) ? full.mcpServers : {},
12841
- [name]: nextCfg
12842
- };
12843
- full.mcpServers = mcpServers;
12844
- await writeConfig(configPath2, full);
14079
+ await updateJsonObjectFile(configPath2, (full) => {
14080
+ const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
14081
+ setJsonPath(full, ["mcpServers", name], { ...current[name], ...nextCfg });
14082
+ });
12845
14083
  if (!enable) {
12846
14084
  const verb = existing ? "Updated" : "Added (disabled \u2014 /mcp enable to start)";
12847
14085
  return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
@@ -12875,13 +14113,11 @@ async function runRemove(name, configured, configPath2, mcpRegistry) {
12875
14113
  })
12876
14114
  );
12877
14115
  }
12878
- const full = await readConfig(configPath2);
12879
- const mcpServers = {
12880
- ...full.mcpServers ?? {}
12881
- };
12882
- delete mcpServers[name];
12883
- full.mcpServers = mcpServers;
12884
- await writeConfig(configPath2, full);
14116
+ await updateJsonObjectFile(configPath2, (full) => {
14117
+ const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : configured;
14118
+ setJsonPath(full, ["mcpServers"], { ...current });
14119
+ removeJsonPath(full, ["mcpServers", name]);
14120
+ });
12885
14121
  return `${color.yellow("Removed")} "${name}" from config.`;
12886
14122
  }
12887
14123
  async function runEnable(name, configured, configPath2, mcpRegistry) {
@@ -12896,13 +14132,10 @@ async function runEnable(name, configured, configPath2, mcpRegistry) {
12896
14132
  return `${color.green("Enabled")} "${name}" and started.`;
12897
14133
  }
12898
14134
  }
12899
- const full = await readConfig(configPath2);
12900
- const mcpServers = {
12901
- ...full.mcpServers ?? {}
12902
- };
12903
- mcpServers[name] = { ...cfg, ...mcpServers[name] ?? {}, enabled: true };
12904
- full.mcpServers = mcpServers;
12905
- await writeConfig(configPath2, full);
14135
+ await updateJsonObjectFile(configPath2, (full) => {
14136
+ const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
14137
+ setJsonPath(full, ["mcpServers", name], { ...cfg, ...current[name], enabled: true });
14138
+ });
12906
14139
  try {
12907
14140
  await mcpRegistry.restart(name);
12908
14141
  } catch {
@@ -12927,13 +14160,10 @@ async function runDisable(name, configured, configPath2, mcpRegistry) {
12927
14160
  })
12928
14161
  );
12929
14162
  }
12930
- const full = await readConfig(configPath2);
12931
- const mcpServers = {
12932
- ...full.mcpServers ?? {}
12933
- };
12934
- mcpServers[name] = { ...cfg, ...mcpServers[name] ?? {}, enabled: false };
12935
- full.mcpServers = mcpServers;
12936
- await writeConfig(configPath2, full);
14163
+ await updateJsonObjectFile(configPath2, (full) => {
14164
+ const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
14165
+ setJsonPath(full, ["mcpServers", name], { ...cfg, ...current[name], enabled: false });
14166
+ });
12937
14167
  return `${color.yellow("Disabled")} "${name}" and stopped.`;
12938
14168
  }
12939
14169
  async function runRestart(name, mcpRegistry) {
@@ -12964,22 +14194,9 @@ function stateBadge(state) {
12964
14194
  return color.dim(state);
12965
14195
  }
12966
14196
  }
12967
- async function readConfig(path39) {
12968
- try {
12969
- return JSON.parse(await fsp5.readFile(path39, "utf8"));
12970
- } catch {
12971
- return {};
12972
- }
12973
- }
12974
14197
  function isMcpServerRecord(value) {
12975
14198
  return !!value && typeof value === "object" && !Array.isArray(value);
12976
14199
  }
12977
- async function writeConfig(path39, cfg) {
12978
- const raw = JSON.stringify(cfg, null, 2);
12979
- const tmp = path39 + ".tmp";
12980
- await fsp5.writeFile(tmp, raw, "utf8");
12981
- await fsp5.rename(tmp, path39);
12982
- }
12983
14200
 
12984
14201
  // src/slash-commands/mcp.ts
12985
14202
  function buildMcpSlashCommand(opts) {
@@ -14335,12 +15552,12 @@ function buildLoadCommand(opts) {
14335
15552
  const badge = s.outcome === "completed" ? color.green("\u2713") : s.outcome === "aborted" ? color.yellow("\u26A0") : s.outcome === "error" ? color.red("\u2717") : color.dim("?");
14336
15553
  parts2.push(badge);
14337
15554
  }
14338
- const stat7 = parts2.join(" ");
15555
+ const stat8 = parts2.join(" ");
14339
15556
  const date = color.dim(s.startedAt.slice(0, 16).replace("T", " "));
14340
15557
  const isCurrent = s.id === currentId;
14341
15558
  const marker = isCurrent ? color.cyan(" (current)") : "";
14342
15559
  return ` ${color.bold(s.id)}${marker}
14343
- ${date} ${stat7}
15560
+ ${date} ${stat8}
14344
15561
  ${color.dim(s.title)}`;
14345
15562
  });
14346
15563
  const msg = [
@@ -15099,15 +16316,26 @@ function formatSuggestions(suggestions) {
15099
16316
 
15100
16317
  // src/slash-commands/coordinator.ts
15101
16318
  function buildCoordinatorCommand(opts) {
16319
+ const getStart = () => opts.onCoordinatorStart ?? opts.coordinatorController?.onCoordinatorStart;
16320
+ const getStop = () => opts.onCoordinatorStop ?? opts.coordinatorController?.onCoordinatorStop;
16321
+ const getTasks = () => opts.onCoordinatorTasks ?? opts.coordinatorController?.onCoordinatorTasks;
16322
+ const getClaim = () => opts.onCoordinatorClaim ?? opts.coordinatorController?.onCoordinatorClaim;
16323
+ const getComplete = () => opts.onCoordinatorComplete ?? opts.coordinatorController?.onCoordinatorComplete;
16324
+ const getFail = () => opts.onCoordinatorFail ?? opts.coordinatorController?.onCoordinatorFail;
16325
+ const getStatus = () => opts.onCoordinatorStatus ?? opts.coordinatorController?.onCoordinatorStatus;
15102
16326
  return {
15103
16327
  name: "coordinator",
15104
16328
  category: "Agent",
15105
16329
  description: "Start, stop, or inspect the AutonomousCoordinator \u2014 the fleet brain that auctions tasks and consults Brain for risky decisions.",
15106
16330
  help: [
15107
16331
  "Usage:",
15108
- " /coordinator start <goal> Start the coordinator with a goal",
15109
- " /coordinator stop Stop the running coordinator",
15110
- " /coordinator status Show current coordinator status",
16332
+ " /coordinator start <goal> Start the coordinator with a goal",
16333
+ " /coordinator stop Stop the running coordinator",
16334
+ " /coordinator status Show current coordinator status",
16335
+ " /coordinator tasks List available tasks the current terminal can claim",
16336
+ " /coordinator claim <id> Claim a task and inject its description as the next prompt",
16337
+ " /coordinator done <id> [note] Mark a claimed task as completed",
16338
+ " /coordinator fail <id> <reason> Mark a claimed task as failed",
15111
16339
  "",
15112
16340
  "The AutonomousCoordinator runs alongside the agent loop and:",
15113
16341
  " \u2022 Maintains a shared knowledge graph of facts and decisions",
@@ -15115,7 +16343,10 @@ function buildCoordinatorCommand(opts) {
15115
16343
  " \u2022 Consults the Brain for risky decisions",
15116
16344
  " \u2022 Uses ConsensusProtocol to vote on multi-agent changes",
15117
16345
  "",
15118
- "It is separate from /autonomy eternal \u2014 both can run concurrently."
16346
+ "It is separate from /autonomy eternal \u2014 both can run concurrently.",
16347
+ "",
16348
+ "Terminals are eligible workers: an open terminal can discover and claim",
16349
+ "pending tasks without spawning a subagent."
15119
16350
  ].join("\n"),
15120
16351
  async run(args) {
15121
16352
  const trimmed = args.trim();
@@ -15126,19 +16357,108 @@ function buildCoordinatorCommand(opts) {
15126
16357
  if (!goal) {
15127
16358
  return { message: "Usage: /coordinator start <goal>\nA goal is required to start the coordinator." };
15128
16359
  }
15129
- opts.onCoordinatorStart?.(goal);
16360
+ getStart()?.(goal);
15130
16361
  return {
15131
16362
  message: `AutonomousCoordinator started with goal: "${goal}"
15132
16363
  Use /coordinator status to monitor progress.`
15133
16364
  };
15134
16365
  }
15135
16366
  if (verb === "stop") {
15136
- opts.onCoordinatorStop?.();
16367
+ getStop()?.();
15137
16368
  return { message: "AutonomousCoordinator stop signal sent." };
15138
16369
  }
16370
+ if (verb === "tasks") {
16371
+ const tasksFn = getTasks();
16372
+ if (!tasksFn) {
16373
+ return { message: "Coordinator task listing is not wired in this surface." };
16374
+ }
16375
+ const tasks = await tasksFn();
16376
+ if (!tasks) {
16377
+ return { message: "No coordinator is active. Start one with /coordinator start <goal>." };
16378
+ }
16379
+ if (tasks.length === 0) {
16380
+ return { message: "No pending coordinator tasks. Use /coordinator status for overall progress." };
16381
+ }
16382
+ const lines = ["Pending coordinator tasks available to claim:"];
16383
+ for (const task of tasks) {
16384
+ lines.push(` ${task.id} [${task.priority}] ${task.title}${task.tags.length > 0 ? ` \xB7 ${task.tags.join(", ")}` : ""}`);
16385
+ }
16386
+ lines.push("", "Claim one with /coordinator claim <id> (id prefix allowed).");
16387
+ return { message: lines.join("\n") };
16388
+ }
16389
+ if (verb === "claim") {
16390
+ const target = rest.join(" ").trim();
16391
+ if (!target) {
16392
+ return { message: "Usage: /coordinator claim <taskId>" };
16393
+ }
16394
+ const claimFn = getClaim();
16395
+ if (!claimFn) {
16396
+ return { message: "Coordinator task claiming is not wired in this surface." };
16397
+ }
16398
+ const tasks = await getTasks()?.();
16399
+ const matched = tasks?.find((task) => task.id === target || task.id.startsWith(target));
16400
+ if (!matched) {
16401
+ return { message: `No pending coordinator task matched "${target}".` };
16402
+ }
16403
+ const result = await claimFn(matched.id);
16404
+ if (typeof result === "string") return { message: result };
16405
+ if (result === null) return { message: "No coordinator is active." };
16406
+ const description = result.description ?? matched.title;
16407
+ return {
16408
+ message: `Claimed task ${matched.id.slice(0, 8)}: ${matched.title}`,
16409
+ runText: `Work on this coordinator task (id: ${matched.id}):
16410
+
16411
+ ${description}`
16412
+ };
16413
+ }
16414
+ if (verb === "done" || verb === "complete") {
16415
+ const taskId = rest[0]?.trim() ?? "";
16416
+ if (!taskId) {
16417
+ return { message: "Usage: /coordinator done <taskId> [note]" };
16418
+ }
16419
+ const completeFn = getComplete();
16420
+ if (!completeFn) {
16421
+ return { message: "Coordinator task completion is not wired in this surface." };
16422
+ }
16423
+ const note = rest.slice(1).join(" ").trim();
16424
+ const err = await completeFn(taskId, note || void 0);
16425
+ if (err) return { message: err };
16426
+ return { message: `Task ${taskId.slice(0, 8)} marked completed.` };
16427
+ }
16428
+ if (verb === "fail") {
16429
+ const taskId = rest[0]?.trim() ?? "";
16430
+ if (!taskId) {
16431
+ return { message: "Usage: /coordinator fail <taskId> <reason>" };
16432
+ }
16433
+ const failFn = getFail();
16434
+ if (!failFn) {
16435
+ return { message: "Coordinator task failure reporting is not wired in this surface." };
16436
+ }
16437
+ const reason = rest.slice(1).join(" ").trim() || "Terminal worker reported failure";
16438
+ const err = await failFn(taskId, reason);
16439
+ if (err) return { message: err };
16440
+ return { message: `Task ${taskId.slice(0, 8)} marked failed: ${reason}` };
16441
+ }
15139
16442
  if (verb === "status") {
15140
- const canStart = opts.onCoordinatorStart != null;
15141
- const canStop = opts.onCoordinatorStop != null;
16443
+ const statusFn = getStatus();
16444
+ if (statusFn) {
16445
+ const stats = await statusFn();
16446
+ if (!stats) {
16447
+ return { message: "No coordinator is active. Start one with /coordinator start <goal>." };
16448
+ }
16449
+ const lines = [
16450
+ "Coordinator Status:",
16451
+ ` Goals: ${stats.goals.total} total \xB7 ${stats.goals.done} done \xB7 ${stats.goals.pending} pending \xB7 ${stats.goals.failed} failed`,
16452
+ ` DAG: ${stats.dag.running} running \xB7 ${stats.dag.ready} ready \xB7 ${stats.dag.done} done \xB7 ${stats.dag.failed} failed`,
16453
+ ` Auction: ${stats.auction.pending} pending \xB7 ${stats.auction.inProgress} in progress`
16454
+ ];
16455
+ if (stats.goals.pending > 0 || stats.auction.pending > 0) {
16456
+ lines.push("", "Use /coordinator tasks to list claimable work.");
16457
+ }
16458
+ return { message: lines.join("\n") };
16459
+ }
16460
+ const canStart = getStart() != null;
16461
+ const canStop = getStop() != null;
15142
16462
  return {
15143
16463
  message: [
15144
16464
  `Coordinator wired: start=${canStart ? "yes" : "no"}, stop=${canStop ? "yes" : "no"}`,
@@ -15149,9 +16469,13 @@ Use /coordinator status to monitor progress.`
15149
16469
  return {
15150
16470
  message: [
15151
16471
  "Usage:",
15152
- " /coordinator start <goal> Start with a goal",
15153
- " /coordinator stop Stop the coordinator",
15154
- " /coordinator status Show status",
16472
+ " /coordinator start <goal> Start with a goal",
16473
+ " /coordinator stop Stop the coordinator",
16474
+ " /coordinator status Show status",
16475
+ " /coordinator tasks List tasks this terminal can claim",
16476
+ " /coordinator claim <id> Claim a task and inject its description",
16477
+ " /coordinator done <id> [note] Mark a claimed task as completed",
16478
+ " /coordinator fail <id> <reason> Mark a claimed task as failed",
15155
16479
  "",
15156
16480
  "The coordinator is a fleet brain that:",
15157
16481
  " \u2022 Auctions tasks to subagents via TaskAuctioneer",
@@ -15350,8 +16674,8 @@ async function addProjectCommand(opts, ctx, targetPath, displayName) {
15350
16674
  } catch {
15351
16675
  return { message: color.red(`Directory not found: ${resolved}`) };
15352
16676
  }
15353
- const stat7 = await fsp5.stat(resolved);
15354
- if (!stat7.isDirectory()) {
16677
+ const stat8 = await fsp5.stat(resolved);
16678
+ if (!stat8.isDirectory()) {
15355
16679
  return { message: color.red(`Not a directory: ${resolved}`) };
15356
16680
  }
15357
16681
  const manifest = await loadManifest(opts.paths?.globalConfig);
@@ -15420,8 +16744,8 @@ async function switchProjectCommand(opts, ctx, target, displayName) {
15420
16744
  } catch {
15421
16745
  return { message: color.red(`Directory not found: ${resolved}`) };
15422
16746
  }
15423
- const stat7 = await fsp5.stat(resolved);
15424
- if (!stat7.isDirectory()) {
16747
+ const stat8 = await fsp5.stat(resolved);
16748
+ if (!stat8.isDirectory()) {
15425
16749
  return { message: color.red(`Not a directory: ${resolved}`) };
15426
16750
  }
15427
16751
  let cliPath;
@@ -15677,7 +17001,7 @@ async function handlePrevSessions(opts, _ctx) {
15677
17001
  return { message: lines.join("\n") };
15678
17002
  }
15679
17003
  async function runGit(args, cwd) {
15680
- return new Promise((resolve11) => {
17004
+ return new Promise((resolve12) => {
15681
17005
  const child = spawn("git", args, {
15682
17006
  cwd,
15683
17007
  stdio: ["ignore", "pipe", "pipe"],
@@ -15688,8 +17012,8 @@ async function runGit(args, cwd) {
15688
17012
  child.stdout?.on("data", (d) => {
15689
17013
  stdout += d;
15690
17014
  });
15691
- child.on("error", () => resolve11({ stdout, code: 1 }));
15692
- child.on("close", (code) => resolve11({ stdout, code: code ?? 0 }));
17015
+ child.on("error", () => resolve12({ stdout, code: 1 }));
17016
+ child.on("close", (code) => resolve12({ stdout, code: code ?? 0 }));
15693
17017
  });
15694
17018
  }
15695
17019
  async function getChangedFiles(cwd) {
@@ -15788,6 +17112,14 @@ function buildSettingsCommand(opts) {
15788
17112
  " /settings token-saving off|minimal|light|medium|aggressive Token-saving mode",
15789
17113
  " /settings max-concurrent <n> Max concurrent subagents (0 = unlimited)",
15790
17114
  " /settings title-animation on|off Terminal title animation",
17115
+ " /settings reasoning auto|on|off Reasoning mode (auto = provider default)",
17116
+ " /settings reasoning-effort none|minimal|low|medium|high|xhigh|max Reasoning effort",
17117
+ " /settings reasoning-preserve on|off Preserve thinking across turns",
17118
+ " /settings cache-ttl 5m|1h Prompt cache TTL (Anthropic)",
17119
+ " /settings hq on|off Enable/disable HQ client publishing",
17120
+ " /settings hq-url <url> HQ URL for remote clients (http://host:3499)",
17121
+ " /settings hq-token <token> HQ client token for remote clients",
17122
+ " /settings hq-raw on|off Send raw content previews to HQ",
15791
17123
  " /settings defaults Show built-in default values",
15792
17124
  "",
15793
17125
  "Settings are persisted to ~/.wrongstack/config.json."
@@ -15814,6 +17146,15 @@ function buildSettingsCommand(opts) {
15814
17146
  const features = opts.configStore.get().features;
15815
17147
  const tokenSavingTier = features?.tokenSavingMode ?? "off";
15816
17148
  const maxConcurrent = opts.configStore.get().maxConcurrent ?? 0;
17149
+ const modelRuntime = opts.configStore.get().modelRuntime;
17150
+ const reasoningMode = modelRuntime?.reasoning?.mode ?? "auto";
17151
+ const reasoningEffort = modelRuntime?.reasoning?.effort ?? "(unset)";
17152
+ const reasoningPreserve = modelRuntime?.reasoning?.preserve === true;
17153
+ const cacheTtl = modelRuntime?.cache?.ttl ?? "default";
17154
+ const hq = opts.configStore.get().hq;
17155
+ const hqEnabled = hq?.enabled === true;
17156
+ const hqUrl = hq?.url ?? "(auto/local)";
17157
+ const hqToken = hq?.token ? `${hq.token.slice(0, 6)}\u2026${hq.token.slice(-4)} (${hq.token.length} chars)` : "(auto/local)";
15817
17158
  return [
15818
17159
  `${color.bold("WrongStack")} ${color.dim("\u2014 Settings")}`,
15819
17160
  "",
@@ -15833,6 +17174,14 @@ function buildSettingsCommand(opts) {
15833
17174
  ` context auto-compact: ${contextAutoCompact ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings context-auto-compact on|off")}`,
15834
17175
  ` token-saving: ${color.cyan(tokenSavingTier)} ${color.dim("change: /settings token-saving off|minimal|light|medium|aggressive")}`,
15835
17176
  ` max-concurrent: ${color.cyan(maxConcurrent === 0 ? "unlimited" : String(maxConcurrent))} ${color.dim("change: /settings max-concurrent <n>")}`,
17177
+ ` reasoning mode: ${color.cyan(reasoningMode)} ${color.dim("change: /settings reasoning auto|on|off")}`,
17178
+ ` reasoning effort: ${color.cyan(reasoningEffort)} ${color.dim("change: /settings reasoning-effort <level>")}`,
17179
+ ` reasoning preserve: ${reasoningPreserve ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings reasoning-preserve on|off")}`,
17180
+ ` cache TTL: ${color.cyan(cacheTtl)} ${color.dim("change: /settings cache-ttl 5m|1h")}`,
17181
+ ` HQ publishing: ${hqEnabled ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings hq on|off")}`,
17182
+ ` HQ URL: ${color.cyan(hqUrl)} ${color.dim("change: /settings hq-url <url>")}`,
17183
+ ` HQ token: ${color.cyan(hqToken)} ${color.dim("change: /settings hq-token <token>")}`,
17184
+ ` HQ raw content: ${hq?.rawContent === true ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings hq-raw on|off")}`,
15836
17185
  "",
15837
17186
  color.dim(" Persisted to ~/.wrongstack/config.json \xB7 /settings help for more")
15838
17187
  ].join("\n");
@@ -15876,6 +17225,60 @@ function buildSettingsCommand(opts) {
15876
17225
  vault: noOpVault
15877
17226
  };
15878
17227
  try {
17228
+ if (sub === "hq") {
17229
+ const raw = (rest[0] ?? "").toLowerCase();
17230
+ if (!["on", "off"].includes(raw)) {
17231
+ return { message: `${color.amber("Usage:")} /settings hq on|off` };
17232
+ }
17233
+ const on = raw === "on";
17234
+ await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
17235
+ const hq = cfg.hq ?? {};
17236
+ hq.enabled = on;
17237
+ cfg.hq = hq;
17238
+ });
17239
+ return { message: `${color.green("\u2713")} HQ publishing \u2192 ${on ? color.cyan("on") : color.dim("off")}` };
17240
+ }
17241
+ if (sub === "hq-url") {
17242
+ const raw = rest.join(" ").trim();
17243
+ if (!raw) return { message: `${color.amber("Usage:")} /settings hq-url <http://host:3499>` };
17244
+ try {
17245
+ const url = new URL(raw);
17246
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("bad protocol");
17247
+ } catch {
17248
+ return { message: `${color.red("Invalid URL")}: ${raw}` };
17249
+ }
17250
+ await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
17251
+ const hq = cfg.hq ?? {};
17252
+ hq.url = raw;
17253
+ hq.enabled = true;
17254
+ cfg.hq = hq;
17255
+ });
17256
+ return { message: `${color.green("\u2713")} HQ URL \u2192 ${color.cyan(raw)}` };
17257
+ }
17258
+ if (sub === "hq-token") {
17259
+ const token = rest.join(" ").trim();
17260
+ if (!token) return { message: `${color.amber("Usage:")} /settings hq-token <client-token>` };
17261
+ await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
17262
+ const hq = cfg.hq ?? {};
17263
+ hq.token = token;
17264
+ hq.enabled = true;
17265
+ cfg.hq = hq;
17266
+ });
17267
+ return { message: `${color.green("\u2713")} HQ token saved ${color.dim("(global config)")}` };
17268
+ }
17269
+ if (sub === "hq-raw") {
17270
+ const raw = (rest[0] ?? "").toLowerCase();
17271
+ if (!["on", "off"].includes(raw)) {
17272
+ return { message: `${color.amber("Usage:")} /settings hq-raw on|off` };
17273
+ }
17274
+ const on = raw === "on";
17275
+ await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
17276
+ const hq = cfg.hq ?? {};
17277
+ hq.rawContent = on;
17278
+ cfg.hq = hq;
17279
+ });
17280
+ return { message: `${color.green("\u2713")} HQ raw content \u2192 ${on ? color.cyan("on") : color.dim("off")}` };
17281
+ }
15879
17282
  if (sub === "delay") {
15880
17283
  const raw = rest[0];
15881
17284
  if (raw === void 0) {
@@ -16155,8 +17558,65 @@ function buildSettingsCommand(opts) {
16155
17558
  message: `${color.green("\u2713")} title animation \u2192 ${on ? color.cyan("on") : color.dim("off")} ${color.dim("terminal title animation")}`
16156
17559
  };
16157
17560
  }
17561
+ if (sub === "reasoning") {
17562
+ const raw = (rest[0] ?? "").toLowerCase();
17563
+ const modes = ["auto", "on", "off"];
17564
+ if (!modes.includes(raw)) {
17565
+ return { message: `${color.amber("Usage:")} /settings reasoning auto|on|off` };
17566
+ }
17567
+ await persistConfigSetting(persistDeps, (cfg) => {
17568
+ const mr = cfg.modelRuntime;
17569
+ const reasoning = mr?.reasoning ?? {};
17570
+ reasoning.mode = raw;
17571
+ cfg.modelRuntime = { ...mr, reasoning };
17572
+ });
17573
+ return { message: `${color.green("\u2713")} reasoning mode \u2192 ${color.bold(raw)}` };
17574
+ }
17575
+ if (sub === "reasoning-effort") {
17576
+ const raw = (rest[0] ?? "").toLowerCase();
17577
+ const efforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
17578
+ if (!efforts.includes(raw)) {
17579
+ return {
17580
+ message: `${color.amber("Usage:")} /settings reasoning-effort none|minimal|low|medium|high|xhigh|max`
17581
+ };
17582
+ }
17583
+ await persistConfigSetting(persistDeps, (cfg) => {
17584
+ const mr = cfg.modelRuntime;
17585
+ const reasoning = mr?.reasoning ?? {};
17586
+ reasoning.effort = raw;
17587
+ cfg.modelRuntime = { ...mr, reasoning };
17588
+ });
17589
+ return { message: `${color.green("\u2713")} reasoning effort \u2192 ${color.bold(raw)}` };
17590
+ }
17591
+ if (sub === "reasoning-preserve") {
17592
+ const raw = (rest[0] ?? "").toLowerCase();
17593
+ if (!["on", "off"].includes(raw)) {
17594
+ return { message: `${color.amber("Usage:")} /settings reasoning-preserve on|off` };
17595
+ }
17596
+ const on = raw === "on";
17597
+ await persistConfigSetting(persistDeps, (cfg) => {
17598
+ const mr = cfg.modelRuntime;
17599
+ const reasoning = mr?.reasoning ?? {};
17600
+ reasoning.preserve = on;
17601
+ cfg.modelRuntime = { ...mr, reasoning };
17602
+ });
17603
+ return {
17604
+ message: `${color.green("\u2713")} reasoning preserve \u2192 ${on ? color.cyan("on") : color.dim("off")}`
17605
+ };
17606
+ }
17607
+ if (sub === "cache-ttl") {
17608
+ const raw = (rest[0] ?? "").toLowerCase();
17609
+ if (!["5m", "1h"].includes(raw)) {
17610
+ return { message: `${color.amber("Usage:")} /settings cache-ttl 5m|1h` };
17611
+ }
17612
+ await persistConfigSetting(persistDeps, (cfg) => {
17613
+ const mr = cfg.modelRuntime;
17614
+ cfg.modelRuntime = { ...mr, cache: { ttl: raw } };
17615
+ });
17616
+ return { message: `${color.green("\u2713")} cache TTL \u2192 ${color.bold(raw)}` };
17617
+ }
16158
17618
  return {
16159
- message: `${color.red("Unknown setting")} "${sub}". ${unknownSubcommand(sub, ["delay", "mode", "hints", "debug-stream", "config-scope", "fs-access", "refine", "refine-delay", "refine-language", "semver-part", "breaker", "breaker-timeout", "context-mode", "context-strategy", "context-auto-compact", "token-saving", "max-concurrent", "title-animation", "defaults"], "settings")}`
17619
+ message: `${color.red("Unknown setting")} "${sub}". ${unknownSubcommand(sub, ["delay", "mode", "hints", "debug-stream", "config-scope", "fs-access", "refine", "refine-delay", "refine-language", "semver-part", "breaker", "breaker-timeout", "context-mode", "context-strategy", "context-auto-compact", "token-saving", "max-concurrent", "title-animation", "reasoning", "reasoning-effort", "reasoning-preserve", "cache-ttl", "defaults"], "settings")}`
16160
17620
  };
16161
17621
  } catch (err) {
16162
17622
  return {
@@ -17392,8 +18852,8 @@ function buildWorkingDirCommand(_opts) {
17392
18852
  };
17393
18853
  }
17394
18854
  try {
17395
- const stat7 = await fsp5.stat(resolved);
17396
- if (!stat7.isDirectory()) {
18855
+ const stat8 = await fsp5.stat(resolved);
18856
+ if (!stat8.isDirectory()) {
17397
18857
  return { message: color.red(`Not a directory: ${resolved}`) };
17398
18858
  }
17399
18859
  } catch {
@@ -17695,7 +19155,7 @@ async function runProjectCheck(opts) {
17695
19155
  if (answer2 === "y" || answer2 === "yes") {
17696
19156
  try {
17697
19157
  const { spawn: spawn9 } = await import('child_process');
17698
- await new Promise((resolve11, reject) => {
19158
+ await new Promise((resolve12, reject) => {
17699
19159
  const child = spawn9("git", ["init"], {
17700
19160
  cwd,
17701
19161
  signal: AbortSignal.timeout(1e4),
@@ -17704,7 +19164,7 @@ async function runProjectCheck(opts) {
17704
19164
  child.on("error", reject);
17705
19165
  child.on(
17706
19166
  "close",
17707
- (code) => code === 0 ? resolve11() : reject(new Error(`git init failed with ${code}`))
19167
+ (code) => code === 0 ? resolve12() : reject(new Error(`git init failed with ${code}`))
17708
19168
  );
17709
19169
  });
17710
19170
  renderer.write(` ${color.green("\u2713")} Git repository initialized
@@ -18596,14 +20056,14 @@ var auditCmd = async (args, deps) => {
18596
20056
  return verify.ok ? 0 : 1;
18597
20057
  };
18598
20058
  async function listAudits(log, dir, deps) {
18599
- const fs38 = await import('fs/promises');
20059
+ const fs36 = await import('fs/promises');
18600
20060
  const path39 = await import('path');
18601
20061
  const out = [];
18602
20062
  let foundRoot = true;
18603
20063
  const scan = async (scanDir, prefix, depth) => {
18604
20064
  let entries;
18605
20065
  try {
18606
- entries = await fs38.readdir(scanDir, { withFileTypes: true });
20066
+ entries = await fs36.readdir(scanDir, { withFileTypes: true });
18607
20067
  } catch {
18608
20068
  if (depth === 0) foundRoot = false;
18609
20069
  return;
@@ -18769,7 +20229,7 @@ ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
18769
20229
  renderer.write(` ${color.bold("c")} Add a custom provider
18770
20230
  `);
18771
20231
  renderer.write(
18772
- ` ${color.bold("s")} Sign in with a subscription ${color.dim("(ChatGPT / Claude / Copilot)")}
20232
+ ` ${color.bold("s")} Login with OAuth ${color.dim("(ChatGPT / Claude / Copilot)")}
18773
20233
  `
18774
20234
  );
18775
20235
  if (ids.length > 0) {
@@ -18865,341 +20325,78 @@ async function runAuthDirect(deps, opts) {
18865
20325
 
18866
20326
  // src/auth-menu/add-provider.ts
18867
20327
  init_provider_config_utils();
18868
- async function addFromCatalog(deps) {
18869
- let catalog = [];
20328
+
20329
+ // src/auth-menu/anthropic-oauth.ts
20330
+ init_provider_config_utils();
20331
+ var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
20332
+ var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
20333
+ var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
20334
+ var REDIRECT_PORT = 53692;
20335
+ var REDIRECT_HOST = "127.0.0.1";
20336
+ var REDIRECT_PATH = "/callback";
20337
+ var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
20338
+ var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
20339
+ var CLAUDE_PROVIDER_ID = "anthropic-oauth";
20340
+ var CLAUDE_BASE_URL = "https://api.anthropic.com";
20341
+ function base64url(buf) {
20342
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
20343
+ }
20344
+ function generatePkce() {
20345
+ const verifier = base64url(randomBytes(32));
20346
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
20347
+ return { verifier, challenge };
20348
+ }
20349
+ function buildAuthorizeUrl(challenge, verifier) {
20350
+ const params = new URLSearchParams({
20351
+ code: "true",
20352
+ client_id: CLIENT_ID,
20353
+ response_type: "code",
20354
+ redirect_uri: REDIRECT_URI,
20355
+ scope: SCOPES,
20356
+ code_challenge: challenge,
20357
+ code_challenge_method: "S256",
20358
+ state: verifier
20359
+ });
20360
+ return `${AUTHORIZE_URL}?${params.toString()}`;
20361
+ }
20362
+ function parseAuthorizationInput(input) {
20363
+ const value = input.trim();
20364
+ if (!value) return {};
18870
20365
  try {
18871
- catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
20366
+ const url = new URL(value);
20367
+ return {
20368
+ code: url.searchParams.get("code") ?? void 0,
20369
+ state: url.searchParams.get("state") ?? void 0
20370
+ };
18872
20371
  } catch {
18873
- deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.\n");
18874
- }
18875
- if (catalog.length === 0) {
18876
- return addManualEntry(deps);
18877
- }
18878
- const saved = new Set(Object.keys(await loadProviders(deps)));
18879
- deps.renderer.write(
18880
- color.dim(
18881
- ` Catalog: ${catalog.length} providers. Filter to narrow, "s" for unsaved-only, or enter to show all.
18882
- `
18883
- )
18884
- );
18885
- const filterRaw = (await deps.reader.readLine(
18886
- ` ${color.amber("?")} Filter ${color.dim('(substring / "s" / q to quit)')}: `
18887
- )).trim();
18888
- if (filterRaw === "q") return false;
18889
- const filterLc = filterRaw.toLowerCase();
18890
- const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
18891
- function matches(p) {
18892
- if (showUnsavedOnly) return !saved.has(p.id);
18893
- if (!filterLc) return true;
18894
- return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
18895
- }
18896
- const byFamily = /* @__PURE__ */ new Map();
18897
- let filteredCount = 0;
18898
- for (const p of catalog) {
18899
- if (!matches(p)) continue;
18900
- filteredCount++;
18901
- const list = byFamily.get(p.family) ?? [];
18902
- list.push(p);
18903
- byFamily.set(p.family, list);
18904
- }
18905
- if (filteredCount === 0) {
18906
- deps.renderer.writeError(
18907
- `No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\`.`
18908
- );
18909
- return false;
18910
20372
  }
18911
- if (filterRaw && !showUnsavedOnly) {
18912
- deps.renderer.write(
18913
- color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
18914
- `)
18915
- );
20373
+ if (value.includes("#")) {
20374
+ const [code, state] = value.split("#", 2);
20375
+ return { code, state };
18916
20376
  }
18917
- const ordered = [];
18918
- const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
18919
- let idx = 1;
18920
- deps.renderer.write("\n");
18921
- for (const fam of familyOrder) {
18922
- const list = byFamily.get(fam);
18923
- if (!list || list.length === 0) continue;
18924
- deps.renderer.write(` ${color.bold(fam)}
18925
- `);
18926
- for (const p of list) {
18927
- const savedMark = saved.has(p.id) ? color.cyan("\u25C9") : color.dim("\u25CB");
18928
- const env = p.envVars[0] ? color.dim(`[${p.envVars[0]}]`) : "";
18929
- deps.renderer.write(
18930
- ` ${color.dim(`${idx}.`.padStart(4))} ${savedMark} ${p.id.padEnd(22)} ${color.dim(p.name)} ${env}
18931
- `
18932
- );
18933
- ordered.push(p);
18934
- idx++;
18935
- }
20377
+ if (value.includes("code=")) {
20378
+ const params = new URLSearchParams(value);
20379
+ return {
20380
+ code: params.get("code") ?? void 0,
20381
+ state: params.get("state") ?? void 0
20382
+ };
18936
20383
  }
18937
- deps.renderer.write(`
18938
- ${color.dim("\u25C9 already saved \u25CB no key yet")}
18939
- `);
18940
- const answer = (await deps.reader.readLine(
18941
- `
18942
- ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("[q to quit]")}: `
18943
- )).trim();
18944
- if (!answer || answer === "q") return false;
18945
- let chosen;
18946
- const num = Number.parseInt(answer, 10);
18947
- if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
18948
- chosen = ordered[num - 1];
18949
- } else {
18950
- chosen = ordered.find((p) => p.id.toLowerCase() === answer.toLowerCase()) ?? catalog.find((p) => p.id.toLowerCase() === answer.toLowerCase());
20384
+ return { code: value };
20385
+ }
20386
+ async function readTokens(res, op) {
20387
+ if (!res.ok) {
20388
+ const text = await res.text().catch(() => "");
20389
+ throw new Error(`Claude token ${op} failed (${res.status}): ${text || res.statusText}`);
18951
20390
  }
18952
- if (!chosen) {
18953
- deps.renderer.writeError(`No such provider: "${answer}"`);
18954
- return false;
20391
+ const json = await res.json();
20392
+ if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
20393
+ throw new Error(`Claude token ${op} response missing fields`);
18955
20394
  }
18956
- return addKeyForCatalogProvider(deps, chosen);
18957
- }
18958
- async function addKeyForCatalogProvider(deps, chosen) {
18959
- deps.renderer.write(
18960
- color.dim(`
18961
- Defaults from models.dev \u2014 press Enter to keep, or type overrides.
18962
- `)
18963
- );
18964
- const famRaw = (await deps.reader.readLine(
18965
- ` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `
18966
- )).trim();
18967
- if (famRaw === "q") return false;
18968
- let family = chosen.family;
18969
- if (famRaw) {
18970
- const validated = validateFamily(famRaw);
18971
- if (!validated) {
18972
- deps.renderer.writeError(
18973
- `Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
18974
- );
18975
- return false;
18976
- }
18977
- family = validated;
18978
- }
18979
- const baseRaw = (await deps.reader.readLine(
18980
- ` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
18981
- )).trim();
18982
- if (baseRaw === "q") return false;
18983
- const baseUrl = baseRaw || chosen.apiBase;
18984
- const providersNow = await loadProviders(deps);
18985
- let suggestedAlias = chosen.id;
18986
- if (family !== chosen.family) {
18987
- let candidate = `${chosen.id}-${family}`;
18988
- let n = 2;
18989
- while (providersNow[candidate]) {
18990
- candidate = `${chosen.id}-${family}-${n}`;
18991
- n++;
18992
- }
18993
- suggestedAlias = candidate;
18994
- }
18995
- const aliasRaw = (await deps.reader.readLine(
18996
- ` ${color.amber("?")} Save as alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used with --provider <alias>)")}: `
18997
- )).trim();
18998
- const alias = aliasRaw || suggestedAlias;
18999
- const existing = providersNow[alias];
19000
- if (existing) {
19001
- const sameFamily = (existing.family ?? chosen.family) === family;
19002
- const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
19003
- if (!sameFamily || !sameBase) {
19004
- deps.renderer.writeError(
19005
- `Alias "${alias}" already exists with different family/baseUrl.
19006
- Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
19007
- New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
19008
- Pick a different alias to keep them separate.`
19009
- );
19010
- return false;
19011
- }
19012
- }
19013
- return addKeyForProvider(alias, deps, {
19014
- type: chosen.id,
19015
- family,
19016
- baseUrl,
19017
- envVars: chosen.envVars
19018
- });
19019
- }
19020
- async function addCustomProvider(deps) {
19021
- deps.renderer.write(
19022
- `
19023
- ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the catalog.")}
19024
- `
19025
- );
19026
- const type = (await deps.reader.readLine(
19027
- ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
19028
- )).trim();
19029
- if (!type || type === "q") return false;
19030
- const existing = (await loadProviders(deps))[type];
19031
- if (existing) {
19032
- deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
19033
- return false;
19034
- }
19035
- const familyRaw = (await deps.reader.readLine(
19036
- ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
19037
- )).trim();
19038
- if (familyRaw === "q") return false;
19039
- const family = validateFamily(familyRaw);
19040
- if (!family) {
19041
- deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
19042
- return false;
19043
- }
19044
- const baseUrl = (await deps.reader.readLine(
19045
- ` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, optional)")}: `
19046
- )).trim();
19047
- const modelsRaw = (await deps.reader.readLine(
19048
- ` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
19049
- )).trim();
19050
- const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
19051
- const envVarsRaw = (await deps.reader.readLine(
19052
- ` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional)")}: `
19053
- )).trim();
19054
- const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
19055
- return addKeyForProvider(type, deps, {
19056
- type,
19057
- family,
19058
- ...baseUrl ? { baseUrl } : {},
19059
- ...models ? { models } : {},
19060
- ...envVars ? { envVars } : {}
19061
- });
19062
- }
19063
- async function addManualEntry(deps) {
19064
- const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
19065
- if (!pid || pid === "q") return false;
19066
- const famRaw = (await deps.reader.readLine(
19067
- ` ${color.amber("?")} Family ${color.dim("(anthropic/openai/openai-compatible/google)")}: `
19068
- )).trim();
19069
- const family = validateFamily(famRaw);
19070
- if (!family) {
19071
- deps.renderer.writeError(`Invalid family: "${famRaw}"`);
19072
- return false;
19073
- }
19074
- const baseUrl = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
19075
- return addKeyForProvider(pid, deps, {
19076
- type: pid,
19077
- family,
19078
- ...baseUrl ? { baseUrl } : {}
19079
- });
19080
- }
19081
- async function addKeyForProvider(providerId, deps, template) {
19082
- const providers = await loadProviders(deps);
19083
- const existing = providers[providerId];
19084
- const existingKeys = existing ? normalizeKeys(existing) : [];
19085
- const usedLabels = new Set(existingKeys.map((k) => k.label));
19086
- const label = await promptForLabel(deps, usedLabels);
19087
- if (!label) return false;
19088
- const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
19089
- if (!apiKey) return false;
19090
- await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
19091
- const existingProv = all[providerId] ?? {
19092
- type: providerId,
19093
- ...template
19094
- };
19095
- if (!existingProv.type) existingProv.type = providerId;
19096
- if (!existingProv.family && template.family) {
19097
- existingProv.family = template.family;
19098
- }
19099
- if (!existingProv.baseUrl && template.baseUrl) {
19100
- existingProv.baseUrl = template.baseUrl;
19101
- }
19102
- if (!existingProv.envVars && template.envVars) {
19103
- existingProv.envVars = template.envVars;
19104
- }
19105
- const list = normalizeKeys(existingProv);
19106
- list.push({ label, apiKey, createdAt: nowIso() });
19107
- writeKeysBack(existingProv, list);
19108
- if (!existingProv.activeKey) existingProv.activeKey = label;
19109
- all[providerId] = existingProv;
19110
- });
19111
- deps.renderer.write(
19112
- ` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
19113
- `
19114
- );
19115
- deps.renderer.write(color.dim(` Launch: wstack --provider ${providerId} "<task>"
19116
- `));
19117
- return true;
19118
- }
19119
- async function promptForLabel(deps, usedLabels) {
19120
- const defaultLabel = suggestLabel(usedLabels);
19121
- const labelRaw = (await deps.reader.readLine(
19122
- ` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
19123
- )).trim();
19124
- const label = labelRaw || defaultLabel;
19125
- if (usedLabels.has(label)) {
19126
- deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
19127
- return null;
19128
- }
19129
- return label;
19130
- }
19131
-
19132
- // src/auth-menu/anthropic-oauth.ts
19133
- init_provider_config_utils();
19134
- var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
19135
- var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
19136
- var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
19137
- var REDIRECT_PORT = 53692;
19138
- var REDIRECT_HOST = "127.0.0.1";
19139
- var REDIRECT_PATH = "/callback";
19140
- var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
19141
- var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
19142
- var CLAUDE_PROVIDER_ID = "anthropic-oauth";
19143
- var CLAUDE_BASE_URL = "https://api.anthropic.com";
19144
- function base64url(buf) {
19145
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
19146
- }
19147
- function generatePkce() {
19148
- const verifier = base64url(randomBytes(32));
19149
- const challenge = base64url(createHash("sha256").update(verifier).digest());
19150
- return { verifier, challenge };
19151
- }
19152
- function buildAuthorizeUrl(challenge, verifier) {
19153
- const params = new URLSearchParams({
19154
- code: "true",
19155
- client_id: CLIENT_ID,
19156
- response_type: "code",
19157
- redirect_uri: REDIRECT_URI,
19158
- scope: SCOPES,
19159
- code_challenge: challenge,
19160
- code_challenge_method: "S256",
19161
- state: verifier
19162
- });
19163
- return `${AUTHORIZE_URL}?${params.toString()}`;
19164
- }
19165
- function parseAuthorizationInput(input) {
19166
- const value = input.trim();
19167
- if (!value) return {};
19168
- try {
19169
- const url = new URL(value);
19170
- return {
19171
- code: url.searchParams.get("code") ?? void 0,
19172
- state: url.searchParams.get("state") ?? void 0
19173
- };
19174
- } catch {
19175
- }
19176
- if (value.includes("#")) {
19177
- const [code, state] = value.split("#", 2);
19178
- return { code, state };
19179
- }
19180
- if (value.includes("code=")) {
19181
- const params = new URLSearchParams(value);
19182
- return {
19183
- code: params.get("code") ?? void 0,
19184
- state: params.get("state") ?? void 0
19185
- };
19186
- }
19187
- return { code: value };
19188
- }
19189
- async function readTokens(res, op) {
19190
- if (!res.ok) {
19191
- const text = await res.text().catch(() => "");
19192
- throw new Error(`Claude token ${op} failed (${res.status}): ${text || res.statusText}`);
19193
- }
19194
- const json = await res.json();
19195
- if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
19196
- throw new Error(`Claude token ${op} response missing fields`);
19197
- }
19198
- return {
19199
- access: json.access_token,
19200
- refresh: json.refresh_token,
19201
- expires: Date.now() + json.expires_in * 1e3
19202
- };
20395
+ return {
20396
+ access: json.access_token,
20397
+ refresh: json.refresh_token,
20398
+ expires: Date.now() + json.expires_in * 1e3
20399
+ };
19203
20400
  }
19204
20401
  async function exchangeAuthorizationCode(code, state, verifier, signal) {
19205
20402
  const res = await fetch(TOKEN_URL, {
@@ -19243,12 +20440,12 @@ function callbackHtml(ok, message) {
19243
20440
  function startLoopbackServer(expectedState) {
19244
20441
  let resolveCode = () => {
19245
20442
  };
19246
- const codePromise = new Promise((resolve11) => {
20443
+ const codePromise = new Promise((resolve12) => {
19247
20444
  let settled = false;
19248
20445
  resolveCode = (v) => {
19249
20446
  if (settled) return;
19250
20447
  settled = true;
19251
- resolve11(v);
20448
+ resolve12(v);
19252
20449
  };
19253
20450
  });
19254
20451
  const server = createServer((req2, res) => {
@@ -19291,10 +20488,10 @@ function startLoopbackServer(expectedState) {
19291
20488
  res.end(callbackHtml(true, "You can close this window and return to the terminal."));
19292
20489
  resolveCode({ code, state });
19293
20490
  });
19294
- return new Promise((resolve11) => {
20491
+ return new Promise((resolve12) => {
19295
20492
  server.on("error", () => {
19296
20493
  resolveCode(null);
19297
- resolve11({
20494
+ resolve12({
19298
20495
  bound: false,
19299
20496
  waitForCode: () => Promise.resolve(null),
19300
20497
  close: () => {
@@ -19306,7 +20503,7 @@ function startLoopbackServer(expectedState) {
19306
20503
  });
19307
20504
  });
19308
20505
  server.listen(REDIRECT_PORT, REDIRECT_HOST, () => {
19309
- resolve11({
20506
+ resolve12({
19310
20507
  bound: true,
19311
20508
  waitForCode: () => codePromise,
19312
20509
  close: () => {
@@ -19465,9 +20662,9 @@ function openBrowser2(url) {
19465
20662
  }
19466
20663
  }
19467
20664
  function sleep(ms, signal) {
19468
- return new Promise((resolve11, reject) => {
20665
+ return new Promise((resolve12, reject) => {
19469
20666
  if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
19470
- const t = setTimeout(resolve11, ms);
20667
+ const t = setTimeout(resolve12, ms);
19471
20668
  signal.addEventListener(
19472
20669
  "abort",
19473
20670
  () => {
@@ -19745,12 +20942,12 @@ function callbackHtml2(ok, message) {
19745
20942
  function startLoopbackServer2(state) {
19746
20943
  let resolveCode = () => {
19747
20944
  };
19748
- const codePromise = new Promise((resolve11) => {
20945
+ const codePromise = new Promise((resolve12) => {
19749
20946
  let settled = false;
19750
20947
  resolveCode = (v) => {
19751
20948
  if (settled) return;
19752
20949
  settled = true;
19753
- resolve11(v);
20950
+ resolve12(v);
19754
20951
  };
19755
20952
  });
19756
20953
  const server = createServer((req2, res) => {
@@ -19792,10 +20989,10 @@ function startLoopbackServer2(state) {
19792
20989
  res.end(callbackHtml2(true, "You can close this window and return to the terminal."));
19793
20990
  resolveCode(code);
19794
20991
  });
19795
- return new Promise((resolve11) => {
20992
+ return new Promise((resolve12) => {
19796
20993
  server.on("error", () => {
19797
20994
  resolveCode(null);
19798
- resolve11({
20995
+ resolve12({
19799
20996
  bound: false,
19800
20997
  waitForCode: () => Promise.resolve(null),
19801
20998
  close: () => {
@@ -19807,7 +21004,7 @@ function startLoopbackServer2(state) {
19807
21004
  });
19808
21005
  });
19809
21006
  server.listen(REDIRECT_PORT2, REDIRECT_HOST2, () => {
19810
- resolve11({
21007
+ resolve12({
19811
21008
  bound: true,
19812
21009
  waitForCode: () => codePromise,
19813
21010
  close: () => {
@@ -19967,6 +21164,316 @@ async function saveCodexTokens(deps, providerId, tokens, accountId) {
19967
21164
  }
19968
21165
  }
19969
21166
 
21167
+ // src/auth-menu/oauth-menu.ts
21168
+ function renderOAuthLoginOptions(deps, indent = " ") {
21169
+ deps.renderer.write(
21170
+ `${indent}${color.bold("OAuth login options")} ${color.dim("(subscription sign-in)")}
21171
+ ${indent}${color.bold("chatgpt")} ChatGPT Plus/Pro ${color.dim("(\u2192 openai-codex)")}
21172
+ ${indent}${color.bold("claude")} Claude Pro/Max ${color.dim("(\u2192 anthropic-oauth)")}
21173
+ ${indent}${color.bold("copilot")} GitHub Copilot ${color.dim("(\u2192 github-copilot)")}
21174
+ `
21175
+ );
21176
+ }
21177
+ async function runOAuthLoginChoice(deps, choice, opts = {}) {
21178
+ const pick = choice.trim().toLowerCase();
21179
+ const allowNumeric = opts.allowNumeric ?? true;
21180
+ if (allowNumeric && pick === "1" || pick === "chatgpt" || pick === "openai" || pick === "codex") {
21181
+ await runCodexOAuthLogin(deps);
21182
+ return true;
21183
+ }
21184
+ if (allowNumeric && pick === "2" || pick === "claude" || pick === "anthropic") {
21185
+ await runClaudeOAuthLogin(deps);
21186
+ return true;
21187
+ }
21188
+ if (allowNumeric && pick === "3" || pick === "copilot" || pick === "github" || pick === "github-copilot") {
21189
+ await runCopilotOAuthLogin(deps);
21190
+ return true;
21191
+ }
21192
+ return false;
21193
+ }
21194
+ async function runOAuthLoginMenu(deps) {
21195
+ deps.renderer.write(
21196
+ `
21197
+ ${color.bold("Login with OAuth:")}
21198
+ ` + color.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color.dim(" sanctioned path for programmatic use.\n") + ` ${color.bold("1")} ChatGPT Plus/Pro ${color.dim("(\u2192 openai-codex)")}
21199
+ ${color.bold("2")} Claude Pro/Max ${color.dim("(\u2192 anthropic-oauth)")}
21200
+ ${color.bold("3")} GitHub Copilot ${color.dim("(\u2192 github-copilot)")}
21201
+ `
21202
+ );
21203
+ const pick = await deps.reader.readLine(` ${color.amber("?")} Pick ${color.dim("(or b to go back)")}: `);
21204
+ await runOAuthLoginChoice(deps, pick);
21205
+ }
21206
+
21207
+ // src/auth-menu/add-provider.ts
21208
+ async function addFromCatalog(deps) {
21209
+ let catalog = [];
21210
+ try {
21211
+ catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
21212
+ } catch {
21213
+ deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.\n");
21214
+ }
21215
+ if (catalog.length === 0) {
21216
+ return addManualEntry(deps);
21217
+ }
21218
+ const saved = new Set(Object.keys(await loadProviders(deps)));
21219
+ deps.renderer.write(
21220
+ color.dim(
21221
+ ` Catalog: ${catalog.length} providers. Filter to narrow, "s" for unsaved-only, or enter to show all.
21222
+ `
21223
+ )
21224
+ );
21225
+ const filterRaw = (await deps.reader.readLine(
21226
+ ` ${color.amber("?")} Filter ${color.dim('(substring / "s" / q to quit)')}: `
21227
+ )).trim();
21228
+ if (filterRaw === "q") return false;
21229
+ const filterLc = filterRaw.toLowerCase();
21230
+ const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
21231
+ function matches(p) {
21232
+ if (showUnsavedOnly) return !saved.has(p.id);
21233
+ if (!filterLc) return true;
21234
+ return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
21235
+ }
21236
+ const byFamily = /* @__PURE__ */ new Map();
21237
+ let filteredCount = 0;
21238
+ for (const p of catalog) {
21239
+ if (!matches(p)) continue;
21240
+ filteredCount++;
21241
+ const list = byFamily.get(p.family) ?? [];
21242
+ list.push(p);
21243
+ byFamily.set(p.family, list);
21244
+ }
21245
+ if (filteredCount === 0) {
21246
+ deps.renderer.writeError(
21247
+ `No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\`.`
21248
+ );
21249
+ return false;
21250
+ }
21251
+ if (filterRaw && !showUnsavedOnly) {
21252
+ deps.renderer.write(
21253
+ color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
21254
+ `)
21255
+ );
21256
+ }
21257
+ renderOAuthLoginOptions(deps);
21258
+ deps.renderer.write("\n");
21259
+ const ordered = [];
21260
+ const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
21261
+ let idx = 1;
21262
+ deps.renderer.write("\n");
21263
+ for (const fam of familyOrder) {
21264
+ const list = byFamily.get(fam);
21265
+ if (!list || list.length === 0) continue;
21266
+ deps.renderer.write(` ${color.bold(fam)}
21267
+ `);
21268
+ for (const p of list) {
21269
+ const savedMark = saved.has(p.id) ? color.cyan("\u25C9") : color.dim("\u25CB");
21270
+ const env = p.envVars[0] ? color.dim(`[${p.envVars[0]}]`) : "";
21271
+ deps.renderer.write(
21272
+ ` ${color.dim(`${idx}.`.padStart(4))} ${savedMark} ${p.id.padEnd(22)} ${color.dim(p.name)} ${env}
21273
+ `
21274
+ );
21275
+ ordered.push(p);
21276
+ idx++;
21277
+ }
21278
+ }
21279
+ deps.renderer.write(`
21280
+ ${color.dim("\u25C9 already saved \u25CB no key yet")}
21281
+ `);
21282
+ const answer = (await deps.reader.readLine(
21283
+ `
21284
+ ${color.amber("?")} Pick (1-${ordered.length}), type provider id, or OAuth option ${color.dim("[chatgpt/claude/copilot, q to quit]")}: `
21285
+ )).trim();
21286
+ if (!answer || answer === "q") return false;
21287
+ if (await runOAuthLoginChoice(deps, answer, { allowNumeric: false })) {
21288
+ return true;
21289
+ }
21290
+ let chosen;
21291
+ const num = Number.parseInt(answer, 10);
21292
+ if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
21293
+ chosen = ordered[num - 1];
21294
+ } else {
21295
+ chosen = ordered.find((p) => p.id.toLowerCase() === answer.toLowerCase()) ?? catalog.find((p) => p.id.toLowerCase() === answer.toLowerCase());
21296
+ }
21297
+ if (!chosen) {
21298
+ deps.renderer.writeError(`No such provider: "${answer}"`);
21299
+ return false;
21300
+ }
21301
+ return addKeyForCatalogProvider(deps, chosen);
21302
+ }
21303
+ async function addKeyForCatalogProvider(deps, chosen) {
21304
+ deps.renderer.write(
21305
+ color.dim(`
21306
+ Defaults from models.dev \u2014 press Enter to keep, or type overrides.
21307
+ `)
21308
+ );
21309
+ const famRaw = (await deps.reader.readLine(
21310
+ ` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `
21311
+ )).trim();
21312
+ if (famRaw === "q") return false;
21313
+ let family = chosen.family;
21314
+ if (famRaw) {
21315
+ const validated = validateFamily(famRaw);
21316
+ if (!validated) {
21317
+ deps.renderer.writeError(
21318
+ `Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
21319
+ );
21320
+ return false;
21321
+ }
21322
+ family = validated;
21323
+ }
21324
+ const baseRaw = (await deps.reader.readLine(
21325
+ ` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
21326
+ )).trim();
21327
+ if (baseRaw === "q") return false;
21328
+ const baseUrl = baseRaw || chosen.apiBase;
21329
+ const providersNow = await loadProviders(deps);
21330
+ let suggestedAlias = chosen.id;
21331
+ if (family !== chosen.family) {
21332
+ let candidate = `${chosen.id}-${family}`;
21333
+ let n = 2;
21334
+ while (providersNow[candidate]) {
21335
+ candidate = `${chosen.id}-${family}-${n}`;
21336
+ n++;
21337
+ }
21338
+ suggestedAlias = candidate;
21339
+ }
21340
+ const aliasRaw = (await deps.reader.readLine(
21341
+ ` ${color.amber("?")} Save as alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used with --provider <alias>)")}: `
21342
+ )).trim();
21343
+ const alias = aliasRaw || suggestedAlias;
21344
+ const existing = providersNow[alias];
21345
+ if (existing) {
21346
+ const sameFamily = (existing.family ?? chosen.family) === family;
21347
+ const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
21348
+ if (!sameFamily || !sameBase) {
21349
+ deps.renderer.writeError(
21350
+ `Alias "${alias}" already exists with different family/baseUrl.
21351
+ Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
21352
+ New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
21353
+ Pick a different alias to keep them separate.`
21354
+ );
21355
+ return false;
21356
+ }
21357
+ }
21358
+ return addKeyForProvider(alias, deps, {
21359
+ type: chosen.id,
21360
+ family,
21361
+ baseUrl,
21362
+ envVars: chosen.envVars
21363
+ });
21364
+ }
21365
+ async function addCustomProvider(deps) {
21366
+ deps.renderer.write(
21367
+ `
21368
+ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the catalog.")}
21369
+ `
21370
+ );
21371
+ const type = (await deps.reader.readLine(
21372
+ ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
21373
+ )).trim();
21374
+ if (!type || type === "q") return false;
21375
+ const existing = (await loadProviders(deps))[type];
21376
+ if (existing) {
21377
+ deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
21378
+ return false;
21379
+ }
21380
+ const familyRaw = (await deps.reader.readLine(
21381
+ ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
21382
+ )).trim();
21383
+ if (familyRaw === "q") return false;
21384
+ const family = validateFamily(familyRaw);
21385
+ if (!family) {
21386
+ deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
21387
+ return false;
21388
+ }
21389
+ const baseUrl = (await deps.reader.readLine(
21390
+ ` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, optional)")}: `
21391
+ )).trim();
21392
+ const modelsRaw = (await deps.reader.readLine(
21393
+ ` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
21394
+ )).trim();
21395
+ const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
21396
+ const envVarsRaw = (await deps.reader.readLine(
21397
+ ` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional)")}: `
21398
+ )).trim();
21399
+ const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
21400
+ return addKeyForProvider(type, deps, {
21401
+ type,
21402
+ family,
21403
+ ...baseUrl ? { baseUrl } : {},
21404
+ ...models ? { models } : {},
21405
+ ...envVars ? { envVars } : {}
21406
+ });
21407
+ }
21408
+ async function addManualEntry(deps) {
21409
+ const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
21410
+ if (!pid || pid === "q") return false;
21411
+ const famRaw = (await deps.reader.readLine(
21412
+ ` ${color.amber("?")} Family ${color.dim("(anthropic/openai/openai-compatible/google)")}: `
21413
+ )).trim();
21414
+ const family = validateFamily(famRaw);
21415
+ if (!family) {
21416
+ deps.renderer.writeError(`Invalid family: "${famRaw}"`);
21417
+ return false;
21418
+ }
21419
+ const baseUrl = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
21420
+ return addKeyForProvider(pid, deps, {
21421
+ type: pid,
21422
+ family,
21423
+ ...baseUrl ? { baseUrl } : {}
21424
+ });
21425
+ }
21426
+ async function addKeyForProvider(providerId, deps, template) {
21427
+ const providers = await loadProviders(deps);
21428
+ const existing = providers[providerId];
21429
+ const existingKeys = existing ? normalizeKeys(existing) : [];
21430
+ const usedLabels = new Set(existingKeys.map((k) => k.label));
21431
+ const label = await promptForLabel(deps, usedLabels);
21432
+ if (!label) return false;
21433
+ const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
21434
+ if (!apiKey) return false;
21435
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
21436
+ const existingProv = all[providerId] ?? {
21437
+ type: providerId,
21438
+ ...template
21439
+ };
21440
+ if (!existingProv.type) existingProv.type = providerId;
21441
+ if (!existingProv.family && template.family) {
21442
+ existingProv.family = template.family;
21443
+ }
21444
+ if (!existingProv.baseUrl && template.baseUrl) {
21445
+ existingProv.baseUrl = template.baseUrl;
21446
+ }
21447
+ if (!existingProv.envVars && template.envVars) {
21448
+ existingProv.envVars = template.envVars;
21449
+ }
21450
+ const list = normalizeKeys(existingProv);
21451
+ list.push({ label, apiKey, createdAt: nowIso() });
21452
+ writeKeysBack(existingProv, list);
21453
+ if (!existingProv.activeKey) existingProv.activeKey = label;
21454
+ all[providerId] = existingProv;
21455
+ });
21456
+ deps.renderer.write(
21457
+ ` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
21458
+ `
21459
+ );
21460
+ deps.renderer.write(color.dim(` Launch: wstack --provider ${providerId} "<task>"
21461
+ `));
21462
+ return true;
21463
+ }
21464
+ async function promptForLabel(deps, usedLabels) {
21465
+ const defaultLabel = suggestLabel(usedLabels);
21466
+ const labelRaw = (await deps.reader.readLine(
21467
+ ` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
21468
+ )).trim();
21469
+ const label = labelRaw || defaultLabel;
21470
+ if (usedLabels.has(label)) {
21471
+ deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
21472
+ return null;
21473
+ }
21474
+ return label;
21475
+ }
21476
+
19970
21477
  // src/auth-menu/provider-menu.ts
19971
21478
  init_provider_config_utils();
19972
21479
  async function manageProvider(providerId, deps) {
@@ -20142,24 +21649,6 @@ function validKeyIndex(arg, max, deps, verb) {
20142
21649
  }
20143
21650
 
20144
21651
  // src/auth-menu/top-menu.ts
20145
- async function runSignInMenu(deps) {
20146
- deps.renderer.write(
20147
- `
20148
- ${color.bold("Sign in with a subscription:")}
20149
- ` + color.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color.dim(" sanctioned path for programmatic use.\n") + ` ${color.bold("1")} ChatGPT Plus/Pro ${color.dim("(\u2192 openai-codex)")}
20150
- ${color.bold("2")} Claude Pro/Max ${color.dim("(\u2192 anthropic-oauth)")}
20151
- ${color.bold("3")} GitHub Copilot ${color.dim("(\u2192 github-copilot)")}
20152
- `
20153
- );
20154
- const pick = (await deps.reader.readLine(` ${color.amber("?")} Pick ${color.dim("(or b to go back)")}: `)).trim().toLowerCase();
20155
- if (pick === "1" || pick === "chatgpt" || pick === "openai" || pick === "codex") {
20156
- await runCodexOAuthLogin(deps);
20157
- } else if (pick === "2" || pick === "claude" || pick === "anthropic") {
20158
- await runClaudeOAuthLogin(deps);
20159
- } else if (pick === "3" || pick === "copilot" || pick === "github") {
20160
- await runCopilotOAuthLogin(deps);
20161
- }
20162
- }
20163
21652
  async function runTopMenu(deps) {
20164
21653
  for (; ; ) {
20165
21654
  const providers = await loadProviders(deps);
@@ -20179,8 +21668,8 @@ ${color.amber("?")} Pick: `)).trim().toLowerCase();
20179
21668
  await addCustomProvider(deps);
20180
21669
  continue;
20181
21670
  }
20182
- if (choice === "s" || choice === "signin" || choice === "login") {
20183
- await runSignInMenu(deps);
21671
+ if (choice === "s" || choice === "signin" || choice === "login" || choice === "oauth") {
21672
+ await runOAuthLoginMenu(deps);
20184
21673
  continue;
20185
21674
  }
20186
21675
  const idx = Number.parseInt(choice, 10);
@@ -20843,6 +22332,262 @@ var exportCmd = async (args, deps) => {
20843
22332
  }
20844
22333
  return 0;
20845
22334
  };
22335
+ function resolveDataDir(deps) {
22336
+ const override = typeof deps.flags?.["data-dir"] === "string" ? deps.flags["data-dir"] : void 0;
22337
+ return resolveHqDataDir(override);
22338
+ }
22339
+ var hqCmd = async (args, deps) => {
22340
+ const sub = args[0];
22341
+ if (!sub || sub === "serve") {
22342
+ return startServer(deps);
22343
+ }
22344
+ if (sub === "token") {
22345
+ return hqTokenCmd(args.slice(1), deps);
22346
+ }
22347
+ if (sub === "help" || sub === "--help") {
22348
+ printHelp(deps);
22349
+ return 0;
22350
+ }
22351
+ deps.renderer.writeError(`Unknown hq subcommand: ${sub}
22352
+ `);
22353
+ printHelp(deps);
22354
+ return 1;
22355
+ };
22356
+ async function startServer(deps) {
22357
+ const { startHqServer: startHqServer2 } = await Promise.resolve().then(() => (init_hq_server(), hq_server_exports));
22358
+ const dataDir = resolveDataDir(deps);
22359
+ const flags = deps.flags ?? {};
22360
+ const host = typeof flags["host"] === "string" ? flags["host"] : "127.0.0.1";
22361
+ const port = typeof flags["port"] === "string" ? Number.parseInt(flags["port"], 10) : 3499;
22362
+ const strictPort = flags["strict-port"] === true;
22363
+ const open = flags["open"] === true;
22364
+ const handle = await startHqServer2({ host, port, strictPort, dataDir });
22365
+ if (open) {
22366
+ try {
22367
+ const { openBrowser: openBrowser5 } = await import('@wrongstack/webui/server');
22368
+ openBrowser5(handle.firstRunSetup?.browserUrl ?? `http://${handle.host}:${handle.port}`);
22369
+ } catch {
22370
+ }
22371
+ }
22372
+ writeStartupInfo(deps, handle);
22373
+ await new Promise((resolve12) => {
22374
+ const shutdown = () => {
22375
+ void handle.close().then(() => resolve12());
22376
+ };
22377
+ process.on("SIGINT", shutdown);
22378
+ process.on("SIGTERM", shutdown);
22379
+ });
22380
+ return 0;
22381
+ }
22382
+ function writeStartupInfo(deps, handle) {
22383
+ deps.renderer.write(`WrongStack HQ listening on http://${handle.host}:${handle.port}
22384
+ `);
22385
+ if (!handle.firstRunSetup) {
22386
+ deps.renderer.write(`Client endpoint: ws://${handle.host}:${handle.port}/ws/client
22387
+ `);
22388
+ deps.renderer.write(`Browser endpoint: http://${handle.host}:${handle.port}
22389
+ `);
22390
+ return;
22391
+ }
22392
+ deps.renderer.write(`Browser endpoint: ${handle.firstRunSetup.browserUrl}
22393
+ `);
22394
+ deps.renderer.write(`Client endpoint: ws://${handle.host}:${handle.port}/ws/client?token=${handle.firstRunSetup.clientEnv.WRONGSTACK_HQ_TOKEN}
22395
+ `);
22396
+ deps.renderer.write(`
22397
+ First-run HQ auth created in ${handle.firstRunSetup.dataDir}
22398
+ `);
22399
+ deps.renderer.write(`Start clients with:
22400
+ `);
22401
+ deps.renderer.write(` WRONGSTACK_HQ_URL=${handle.firstRunSetup.clientEnv.WRONGSTACK_HQ_URL}
22402
+ `);
22403
+ deps.renderer.write(` WRONGSTACK_HQ_TOKEN=${handle.firstRunSetup.clientEnv.WRONGSTACK_HQ_TOKEN}
22404
+ `);
22405
+ }
22406
+ async function hqTokenCmd(args, deps) {
22407
+ const action = args[0];
22408
+ if (action === "create") {
22409
+ return tokenCreate(args.slice(1), deps);
22410
+ }
22411
+ if (action === "list" || action === "ls" || !action) {
22412
+ return tokenList(args.slice(1), deps);
22413
+ }
22414
+ if (action === "revoke" || action === "rm" || action === "remove") {
22415
+ return tokenRevoke(args.slice(1), deps);
22416
+ }
22417
+ deps.renderer.writeError(`Unknown hq token subcommand: ${action ?? "(none)"}
22418
+ `);
22419
+ deps.renderer.write("Usage: wstack hq token <create|list|revoke>\n");
22420
+ return 1;
22421
+ }
22422
+ function resolveTokenScope(args) {
22423
+ return args.some((a) => a === "--client" || a === "-c") ? "client" : "browser";
22424
+ }
22425
+ function positionals(args) {
22426
+ return args.filter((a) => !a.startsWith("-"));
22427
+ }
22428
+ async function tokenCreate(args, deps) {
22429
+ const scope = resolveTokenScope(args);
22430
+ const pos = positionals(args);
22431
+ const label = pos[0];
22432
+ const dataDir = resolveDataDir(deps);
22433
+ const tokenField = scope === "client" ? "clientTokens" : "browserTokens";
22434
+ try {
22435
+ const next = await mutateHqAuthFile(
22436
+ dataDir,
22437
+ (current) => {
22438
+ const tokens = current[tokenField] ?? [];
22439
+ const newToken = mintHqToken(label);
22440
+ return {
22441
+ ...current,
22442
+ [tokenField]: [...tokens, newToken]
22443
+ };
22444
+ },
22445
+ { warn: (msg) => deps.renderer.writeWarning(`${msg}
22446
+ `) }
22447
+ );
22448
+ const list = next[tokenField] ?? [];
22449
+ const token = expectDefined(list[list.length - 1]);
22450
+ const endpoint = scope === "client" ? "/ws/client" : "/ws/browser";
22451
+ deps.renderer.write(`Created ${scope} token.
22452
+ `);
22453
+ deps.renderer.write(` id: ${token.id}
22454
+ `);
22455
+ if (token.label) deps.renderer.write(` label: ${token.label}
22456
+ `);
22457
+ deps.renderer.write(` token: ${token.token}
22458
+ `);
22459
+ deps.renderer.write(` createdAt: ${token.createdAt}
22460
+ `);
22461
+ deps.renderer.write(`
22462
+ `);
22463
+ deps.renderer.write(`Connect with: ws://localhost:3499${endpoint}?token=${token.token}
22464
+ `);
22465
+ deps.renderer.write(`(Copy the token now \u2014 it will not be shown again in full.)
22466
+ `);
22467
+ return 0;
22468
+ } catch (err) {
22469
+ deps.renderer.writeError(`Failed to write auth.json: ${err.message}
22470
+ `);
22471
+ return 1;
22472
+ }
22473
+ }
22474
+ async function tokenList(args, deps) {
22475
+ const scope = resolveTokenScope(args);
22476
+ const dataDir = resolveDataDir(deps);
22477
+ const tokenField = scope === "client" ? "clientTokens" : "browserTokens";
22478
+ const authFile = await readHqAuthFile(dataDir, {
22479
+ warn: (msg) => deps.renderer.writeWarning(`${msg}
22480
+ `)
22481
+ });
22482
+ const tokens = authFile[tokenField] ?? [];
22483
+ if (tokens.length === 0) {
22484
+ deps.renderer.write(`No ${scope} tokens issued. ${scope === "browser" ? "Browsers" : "Clients"} are in OPEN MODE.
22485
+ `);
22486
+ deps.renderer.write(`Run \`wstack hq token create ${scope === "client" ? "--client " : ""}[label]\` to enter TOKEN MODE.
22487
+ `);
22488
+ return 0;
22489
+ }
22490
+ deps.renderer.write(`${scope === "client" ? "Client" : "Browser"} tokens (${tokens.length}) \u2014 TOKEN MODE:
22491
+ `);
22492
+ deps.renderer.write("\n");
22493
+ for (const t of tokens) {
22494
+ const masked = `${t.token.slice(0, 6)}\u2026${t.token.slice(-4)} (${t.token.length} chars)`;
22495
+ deps.renderer.write(` ${t.id} ${masked} ${t.createdAt}${t.label ? ` "${t.label}"` : ""}${t.lastUsedAt ? ` lastUsed ${t.lastUsedAt}` : ""}
22496
+ `);
22497
+ }
22498
+ deps.renderer.write("\n");
22499
+ deps.renderer.write(`${scope === "client" ? "Clients" : "Browsers"} must append ?token=<full-token> to /ws/${scope}.
22500
+ `);
22501
+ return 0;
22502
+ }
22503
+ async function tokenRevoke(args, deps) {
22504
+ const scope = resolveTokenScope(args);
22505
+ const pos = positionals(args);
22506
+ const idPrefix = pos[0];
22507
+ if (!idPrefix) {
22508
+ deps.renderer.writeError(`Usage: wstack hq token revoke ${scope === "client" ? "--client " : ""}<id-prefix>
22509
+ `);
22510
+ return 1;
22511
+ }
22512
+ const dataDir = resolveDataDir(deps);
22513
+ const tokenField = scope === "client" ? "clientTokens" : "browserTokens";
22514
+ let revoked;
22515
+ try {
22516
+ await mutateHqAuthFile(
22517
+ dataDir,
22518
+ (current) => {
22519
+ const tokens = current[tokenField] ?? [];
22520
+ const matches = tokens.filter((t) => t.id.startsWith(idPrefix));
22521
+ if (matches.length === 0) {
22522
+ revoked = void 0;
22523
+ return current;
22524
+ }
22525
+ if (matches.length > 1) {
22526
+ revoked = matches[0];
22527
+ return current;
22528
+ }
22529
+ revoked = matches[0];
22530
+ return {
22531
+ ...current,
22532
+ [tokenField]: tokens.filter((t) => t.id !== revoked.id)
22533
+ };
22534
+ },
22535
+ { warn: (msg) => deps.renderer.writeWarning(`${msg}
22536
+ `) }
22537
+ );
22538
+ } catch (err) {
22539
+ deps.renderer.writeError(`Failed to write auth.json: ${err.message}
22540
+ `);
22541
+ return 1;
22542
+ }
22543
+ if (!revoked) {
22544
+ deps.renderer.writeError(`No ${scope} token found matching id-prefix "${idPrefix}".
22545
+ `);
22546
+ return 1;
22547
+ }
22548
+ deps.renderer.write(`Revoked ${scope} token ${revoked.id}${revoked.label ? ` ("${revoked.label}")` : ""}.
22549
+ `);
22550
+ return 0;
22551
+ }
22552
+ function printHelp(deps) {
22553
+ deps.renderer.write(`Usage: wstack hq <serve | token>
22554
+ `);
22555
+ deps.renderer.write("\n");
22556
+ deps.renderer.write(` wstack hq Start the HQ command center server.
22557
+ `);
22558
+ deps.renderer.write(` wstack hq serve Same as above (explicit form).
22559
+ `);
22560
+ deps.renderer.write(` wstack hq token create [label] Mint a browser token, enter token mode.
22561
+ `);
22562
+ deps.renderer.write(` wstack hq token create --client [label] Mint a client token (/ws/client).
22563
+ `);
22564
+ deps.renderer.write(` wstack hq token list List issued browser tokens.
22565
+ `);
22566
+ deps.renderer.write(` wstack hq token list --client List issued client tokens.
22567
+ `);
22568
+ deps.renderer.write(` wstack hq token revoke <id> Revoke a browser token (id prefix match).
22569
+ `);
22570
+ deps.renderer.write(` wstack hq token revoke --client <id> Revoke a client token.
22571
+ `);
22572
+ deps.renderer.write("\n");
22573
+ deps.renderer.write(`Flags (apply to all subcommands):
22574
+ `);
22575
+ deps.renderer.write(` --data-dir <path> Override HQ data directory (default ~/.wrongstack/hq).
22576
+ `);
22577
+ deps.renderer.write(` --host <ip> Bind host (default 127.0.0.1).
22578
+ `);
22579
+ deps.renderer.write(` --port <n> Bind port (default 3499).
22580
+ `);
22581
+ deps.renderer.write(` --strict-port Fail if port is in use.
22582
+ `);
22583
+ deps.renderer.write(` --open Open the dashboard in the default browser.
22584
+ `);
22585
+ deps.renderer.write(` --client, -c Operate on client tokens instead of browser tokens.
22586
+ `);
22587
+ deps.renderer.write("\n");
22588
+ deps.renderer.write(`auth.json schema version: ${HQ_AUTH_FILE_VERSION}.
22589
+ `);
22590
+ }
20846
22591
  var initCmd = async (_args, deps) => {
20847
22592
  deps.renderer.write(color.bold("WrongStack init (deprecated)\n"));
20848
22593
  deps.renderer.write(
@@ -20999,8 +22744,8 @@ async function serveMcpStdio(deps) {
20999
22744
  log(
21000
22745
  `wrongstack MCP server ready at ${handle2.url} \u2014 exposing ${allowed.length} tool(s) (${mode})${token ? " [token auth]" : ""}.`
21001
22746
  );
21002
- await new Promise((resolve11) => {
21003
- const stop = () => resolve11();
22747
+ await new Promise((resolve12) => {
22748
+ const stop = () => resolve12();
21004
22749
  process.once("SIGINT", stop);
21005
22750
  process.once("SIGTERM", stop);
21006
22751
  });
@@ -21085,18 +22830,14 @@ async function addMcpServer(args, deps) {
21085
22830
  }
21086
22831
  const serverCfg = { ...factory };
21087
22832
  serverCfg.enabled = enable;
21088
- let existing = {};
21089
- try {
21090
- existing = JSON.parse(await fsp5.readFile(deps.paths.globalConfig, "utf8"));
21091
- } catch {
21092
- }
21093
- const mcpServers = existing.mcpServers ?? {};
22833
+ const existing = await readJsonObjectFile(deps.paths.globalConfig);
22834
+ const mcpServers = isRecord(existing.mcpServers) ? existing.mcpServers : {};
21094
22835
  if (mcpServers[name])
21095
22836
  deps.renderer.writeWarning(`Server "${name}" already in config. Updating.
21096
22837
  `);
21097
- mcpServers[name] = serverCfg;
21098
- existing.mcpServers = mcpServers;
21099
- await atomicWrite(deps.paths.globalConfig, JSON.stringify(existing, null, 2), { mode: 384 });
22838
+ await updateJsonObjectFile(deps.paths.globalConfig, (config) => {
22839
+ setJsonPath(config, ["mcpServers", name], serverCfg);
22840
+ });
21100
22841
  const verb = enable ? "Enabled" : "Added (disabled \u2014 set enabled:true to activate)";
21101
22842
  deps.renderer.writeInfo(
21102
22843
  `${verb} "${name}" (${serverCfg.transport}). Config written to ${deps.paths.globalConfig}.
@@ -21105,26 +22846,27 @@ async function addMcpServer(args, deps) {
21105
22846
  return 0;
21106
22847
  }
21107
22848
  async function removeMcpServer(name, deps) {
21108
- let existing = {};
21109
- try {
21110
- existing = JSON.parse(await fsp5.readFile(deps.paths.globalConfig, "utf8"));
21111
- } catch {
22849
+ if (!await jsonObjectFileExists(deps.paths.globalConfig)) {
21112
22850
  deps.renderer.writeError("No config file found.\n");
21113
22851
  return 1;
21114
22852
  }
21115
- const mcpServers = existing.mcpServers ?? {};
22853
+ const existing = await readJsonObjectFile(deps.paths.globalConfig);
22854
+ const mcpServers = isRecord(existing.mcpServers) ? existing.mcpServers : {};
21116
22855
  if (!mcpServers[name]) {
21117
22856
  deps.renderer.writeError(`Server "${name}" not in config.
21118
22857
  `);
21119
22858
  return 1;
21120
22859
  }
21121
- delete mcpServers[name];
21122
- existing.mcpServers = mcpServers;
21123
- await atomicWrite(deps.paths.globalConfig, JSON.stringify(existing, null, 2), { mode: 384 });
22860
+ await updateJsonObjectFile(deps.paths.globalConfig, (config) => {
22861
+ removeJsonPath(config, ["mcpServers", name]);
22862
+ });
21124
22863
  deps.renderer.writeInfo(`Removed "${name}" from config.
21125
22864
  `);
21126
22865
  return 0;
21127
22866
  }
22867
+ function isRecord(value) {
22868
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22869
+ }
21128
22870
  var MODEL_PROFILES = [
21129
22871
  {
21130
22872
  provider: "anthropic",
@@ -22130,7 +23872,7 @@ function renderConfiguredPlugins(config) {
22130
23872
  return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
22131
23873
  }).join("\n");
22132
23874
  }
22133
- async function readConfig2(file) {
23875
+ async function readConfig(file) {
22134
23876
  try {
22135
23877
  return JSON.parse(await fsp5.readFile(file, "utf8"));
22136
23878
  } catch {
@@ -22149,15 +23891,15 @@ function officialPluginState(config, spec) {
22149
23891
  return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
22150
23892
  }
22151
23893
  async function upsertPlugin(spec, opts, deps, verb) {
22152
- const existing = await readConfig2(deps.configPath);
23894
+ const existing = await readConfig(deps.configPath);
22153
23895
  const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
22154
23896
  const idx = plugins.findIndex((p) => pluginName(p) === spec);
22155
23897
  const nextEntry = pluginEntry(spec, opts.enabled);
22156
23898
  if (idx >= 0) plugins[idx] = nextEntry;
22157
23899
  else plugins.push(nextEntry);
22158
23900
  const features = {
22159
- ...isRecord(deps.config.features) ? deps.config.features : {},
22160
- ...isRecord(existing.features) ? existing.features : {},
23901
+ ...isRecord2(deps.config.features) ? deps.config.features : {},
23902
+ ...isRecord2(existing.features) ? existing.features : {},
22161
23903
  plugins: true
22162
23904
  };
22163
23905
  existing.plugins = plugins;
@@ -22172,7 +23914,7 @@ async function upsertPlugin(spec, opts, deps, verb) {
22172
23914
  };
22173
23915
  }
22174
23916
  async function removePlugin(spec, deps) {
22175
- const existing = await readConfig2(deps.configPath);
23917
+ const existing = await readConfig(deps.configPath);
22176
23918
  const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
22177
23919
  const next = plugins.filter((p) => pluginName(p) !== spec);
22178
23920
  if (next.length === plugins.length) {
@@ -22191,7 +23933,7 @@ async function removePlugin(spec, deps) {
22191
23933
  function errorResult(message) {
22192
23934
  return { code: 1, level: "error", message };
22193
23935
  }
22194
- function isRecord(value) {
23936
+ function isRecord2(value) {
22195
23937
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
22196
23938
  }
22197
23939
 
@@ -22317,7 +24059,7 @@ function parseFlags2(args) {
22317
24059
  }
22318
24060
  return flags;
22319
24061
  }
22320
- function positionals(args) {
24062
+ function positionals2(args) {
22321
24063
  const out = [];
22322
24064
  for (let i = 0; i < args.length; i++) {
22323
24065
  const a = expectDefined(args[i]);
@@ -22335,11 +24077,17 @@ function positionals(args) {
22335
24077
  return out;
22336
24078
  }
22337
24079
  var DEFAULT_PER_PAGE = 15;
24080
+ function fmtPrice3(usdPer1M) {
24081
+ if (usdPer1M === void 0) return color.dim("?");
24082
+ const value = usdPer1M >= 10 ? usdPer1M.toFixed(1) : usdPer1M.toFixed(2);
24083
+ return "$" + value;
24084
+ }
22338
24085
  var modelsCmd = async (args, deps) => {
22339
24086
  const sub = args[0];
22340
24087
  if (sub === "add") return modelsAdd(args.slice(1), deps);
22341
24088
  if (sub === "remove") return modelsRemove(args.slice(1), deps);
22342
24089
  if (sub === "list") return modelsList(args.slice(1), deps);
24090
+ if (sub === "caps" || sub === "capabilities") return modelsCaps(args.slice(1), deps);
22343
24091
  if (sub === "refresh") {
22344
24092
  deps.renderer.writeInfo("Refreshing models.dev cache\u2026");
22345
24093
  try {
@@ -22442,6 +24190,51 @@ ${navLines.join(" \xB7 ")}
22442
24190
  );
22443
24191
  return 0;
22444
24192
  };
24193
+ async function modelsCaps(args, deps) {
24194
+ const providerId = args[0] ?? deps.config.provider;
24195
+ const modelId = args[1] ?? deps.config.model;
24196
+ if (!providerId || !modelId) {
24197
+ deps.renderer.writeError("Usage: wstack models caps [provider] [model]");
24198
+ deps.renderer.write(color.dim("Defaults to current configured provider/model when omitted.\n"));
24199
+ return 1;
24200
+ }
24201
+ const resolved = await deps.modelsRegistry.getModel(providerId, modelId);
24202
+ if (!resolved) {
24203
+ deps.renderer.writeError("Model not found in catalog: " + providerId + "/" + modelId);
24204
+ deps.renderer.write(color.dim("Run `wstack models refresh` or add a custom model if this is expected.\n"));
24205
+ return 1;
24206
+ }
24207
+ const caps = resolved.capabilities;
24208
+ const flags = [
24209
+ caps.tools ? "tools" : void 0,
24210
+ caps.vision ? "vision" : void 0,
24211
+ caps.reasoning ? "reasoning" : void 0
24212
+ ].filter((v) => v !== void 0);
24213
+ const rc = caps.reasoningConfig;
24214
+ const cost = resolved.cost;
24215
+ deps.renderer.write(color.bold("Model capabilities") + " " + color.dim(providerId + "/" + modelId) + "\n");
24216
+ deps.renderer.write(" context: " + (caps.maxContext ? color.yellow(String(caps.maxContext)) : color.dim("?")) + "\n");
24217
+ if (caps.maxOutput !== void 0) {
24218
+ deps.renderer.write(" max output: " + color.yellow(String(caps.maxOutput)) + "\n");
24219
+ }
24220
+ if (caps.knowledge) deps.renderer.write(" knowledge: " + caps.knowledge + "\n");
24221
+ deps.renderer.write(" flags: " + (flags.length > 0 ? flags.join(", ") : color.dim("(none)")) + "\n");
24222
+ deps.renderer.write(" pricing/1M: input " + fmtPrice3(cost?.input) + " output " + fmtPrice3(cost?.output) + " cacheR " + fmtPrice3(cost?.cache_read) + "\n");
24223
+ if (cost?.cache_write !== void 0 || cost?.cache_write_5m !== void 0 || cost?.cache_write_1h !== void 0) {
24224
+ const cacheWrite1h = cost.cache_write_1h ?? (cost.input !== void 0 ? cost.input * 2 : void 0);
24225
+ deps.renderer.write(" cache write: default " + fmtPrice3(cost.cache_write) + " 5m " + fmtPrice3(cost.cache_write_5m ?? cost.cache_write) + " 1h " + fmtPrice3(cacheWrite1h) + "\n");
24226
+ }
24227
+ if (rc) {
24228
+ deps.renderer.write(" reasoning:\n");
24229
+ deps.renderer.write(" default: " + rc.default + "\n");
24230
+ deps.renderer.write(" disable: " + (rc.disableSupported ? "supported" : "unsupported") + "\n");
24231
+ deps.renderer.write(" effort: " + (rc.effortSupported ? rc.effortLevels.join(", ") : "unsupported") + "\n");
24232
+ deps.renderer.write(" preserve: " + rc.preserveThinking + "\n");
24233
+ } else if (caps.reasoning) {
24234
+ deps.renderer.write(color.dim(" reasoning: supported, but no detailed config in catalog\n"));
24235
+ }
24236
+ return 0;
24237
+ }
22445
24238
  async function mutateModelsConfig(deps, mutator) {
22446
24239
  const vault = deps.vault;
22447
24240
  const configPath2 = deps.paths.globalConfig;
@@ -22491,7 +24284,7 @@ function parseBoolFlag(flags, key) {
22491
24284
  }
22492
24285
  async function modelsAdd(args, deps) {
22493
24286
  const flags = parseFlags2(args);
22494
- const pos = positionals(args);
24287
+ const pos = positionals2(args);
22495
24288
  const modelId = pos[0];
22496
24289
  if (!modelId) {
22497
24290
  deps.renderer.writeError(
@@ -22831,13 +24624,13 @@ async function listFleetRuns(deps) {
22831
24624
  const runs = [];
22832
24625
  for (const id of entries) {
22833
24626
  const runDir = path4.join(deps.paths.projectSessions, id);
22834
- let stat7;
24627
+ let stat8;
22835
24628
  try {
22836
- stat7 = await fsp5.stat(runDir);
24629
+ stat8 = await fsp5.stat(runDir);
22837
24630
  } catch {
22838
24631
  continue;
22839
24632
  }
22840
- if (!stat7.isDirectory()) continue;
24633
+ if (!stat8.isDirectory()) continue;
22841
24634
  let manifest = false;
22842
24635
  let checkpoint = false;
22843
24636
  let subagentCount = 0;
@@ -22881,15 +24674,15 @@ async function listFleetRuns(deps) {
22881
24674
  }
22882
24675
  async function showFleetRun(runId, deps) {
22883
24676
  const runDir = path4.join(deps.paths.projectSessions, runId);
22884
- let stat7;
24677
+ let stat8;
22885
24678
  try {
22886
- stat7 = await fsp5.stat(runDir);
24679
+ stat8 = await fsp5.stat(runDir);
22887
24680
  } catch {
22888
24681
  deps.renderer.writeError(`Fleet run not found: ${runId}
22889
24682
  `);
22890
24683
  return 1;
22891
24684
  }
22892
- if (!stat7.isDirectory()) {
24685
+ if (!stat8.isDirectory()) {
22893
24686
  deps.renderer.writeError(`Not a directory: ${runId}
22894
24687
  `);
22895
24688
  return 1;
@@ -23175,7 +24968,7 @@ var updateCmd = async (args, deps) => {
23175
24968
  deps.renderer.write(`Updating wrongstack from v${info.current} to v${info.latest}...
23176
24969
  `);
23177
24970
  try {
23178
- const result = await new Promise((resolve11, reject) => {
24971
+ const result = await new Promise((resolve12, reject) => {
23179
24972
  const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
23180
24973
  const child = spawn(npmCommand, ["install", "-g", "wrongstack@latest"], {
23181
24974
  cwd,
@@ -23188,7 +24981,7 @@ var updateCmd = async (args, deps) => {
23188
24981
  _stderr += d;
23189
24982
  });
23190
24983
  child.on("error", reject);
23191
- child.on("close", (code) => resolve11({ code: code ?? 0 }));
24984
+ child.on("close", (code) => resolve12({ code: code ?? 0 }));
23192
24985
  });
23193
24986
  if (result.code === 0) {
23194
24987
  deps.renderer.write(
@@ -23300,7 +25093,8 @@ var subcommands = {
23300
25093
  projects: projectsCmd,
23301
25094
  modeldiag: modeldiagCmd,
23302
25095
  quick: quickCmd,
23303
- bench: benchCmd
25096
+ bench: benchCmd,
25097
+ hq: hqCmd
23304
25098
  };
23305
25099
 
23306
25100
  // src/boot.ts
@@ -23674,7 +25468,7 @@ async function checkGitInCwd(opts) {
23674
25468
  if (answer === "y" || answer === "yes") {
23675
25469
  try {
23676
25470
  const { spawn: spawn9 } = await import('child_process');
23677
- await new Promise((resolve11, reject) => {
25471
+ await new Promise((resolve12, reject) => {
23678
25472
  const child = spawn9("git", ["init"], {
23679
25473
  cwd,
23680
25474
  signal: AbortSignal.timeout(1e4),
@@ -23683,7 +25477,7 @@ async function checkGitInCwd(opts) {
23683
25477
  child.on("error", reject);
23684
25478
  child.on(
23685
25479
  "close",
23686
- (code) => code === 0 ? resolve11() : reject(new Error(`git init failed with ${code}`))
25480
+ (code) => code === 0 ? resolve12() : reject(new Error(`git init failed with ${code}`))
23687
25481
  );
23688
25482
  });
23689
25483
  renderer.write(` ${color.green("\u2713")} Git repository initialized
@@ -24434,7 +26228,9 @@ async function runRepl(opts) {
24434
26228
  const replProjectRoot = opts.projectRoot ?? process.cwd();
24435
26229
  const projectDir = resolveProjectDir(replProjectRoot, wstackGlobalRoot());
24436
26230
  const clientId = `repl@${crypto3.randomUUID().slice(0, 8)}`;
24437
- const clientMailbox = new GlobalMailbox(projectDir);
26231
+ const hqPublisher = createHqPublisherFromEnv({ clientKind: "repl", projectRoot: replProjectRoot, projectName: path4.basename(replProjectRoot), appConfig: opts.appConfig });
26232
+ hqPublisher?.connect();
26233
+ const clientMailbox = new GlobalMailbox(projectDir, void 0, hqPublisher);
24438
26234
  let clientHeartbeat;
24439
26235
  clientMailbox.registerClient({
24440
26236
  clientId,
@@ -24507,7 +26303,7 @@ async function runRepl(opts) {
24507
26303
  `[eternal] ${toErrorMessage(err)}`
24508
26304
  );
24509
26305
  }
24510
- await new Promise((resolve11) => setTimeout(resolve11, 250));
26306
+ await new Promise((resolve12) => setTimeout(resolve12, 250));
24511
26307
  continue;
24512
26308
  }
24513
26309
  } else if (opts.getAutonomy?.() === "eternal-parallel") {
@@ -24583,7 +26379,7 @@ async function runRepl(opts) {
24583
26379
  `[parallel] ${toErrorMessage(err)}`
24584
26380
  );
24585
26381
  }
24586
- await new Promise((resolve11) => setTimeout(resolve11, 250));
26382
+ await new Promise((resolve12) => setTimeout(resolve12, 250));
24587
26383
  continue;
24588
26384
  }
24589
26385
  }
@@ -25150,12 +26946,12 @@ ${color.cyan("\u23F3 Auto")} ${color.dim("(Ctrl+C to cancel)")}
25150
26946
  let interval;
25151
26947
  let lastTickedSecond = sec + 1;
25152
26948
  let onAbort;
25153
- return new Promise((resolve11) => {
25154
- onAbort = () => resolve11(false);
26949
+ return new Promise((resolve12) => {
26950
+ onAbort = () => resolve12(false);
25155
26951
  signal.addEventListener("abort", onAbort, { once: true });
25156
26952
  interval = setInterval(() => {
25157
26953
  if (signal.aborted) {
25158
- resolve11(false);
26954
+ resolve12(false);
25159
26955
  return;
25160
26956
  }
25161
26957
  const elapsed = Date.now() - start;
@@ -25163,7 +26959,7 @@ ${color.cyan("\u23F3 Auto")} ${color.dim("(Ctrl+C to cancel)")}
25163
26959
  if (remaining <= 0) {
25164
26960
  opts.renderer.write(color.dim(` \u21B3 ${truncated}
25165
26961
  `));
25166
- resolve11(true);
26962
+ resolve12(true);
25167
26963
  return;
25168
26964
  }
25169
26965
  if (opts.onCountdownTick && remaining !== lastTickedSecond) {
@@ -25174,7 +26970,7 @@ ${color.cyan("\u23F3 Auto")} ${color.dim("(Ctrl+C to cancel)")}
25174
26970
  opts.renderer.write(
25175
26971
  color.yellow(" \u21B3 Countdown cancelled \u2014 switching to manual mode\n")
25176
26972
  );
25177
- resolve11(false);
26973
+ resolve12(false);
25178
26974
  return;
25179
26975
  }
25180
26976
  } catch {
@@ -25271,10 +27067,10 @@ async function execute(deps) {
25271
27067
  reader,
25272
27068
  session,
25273
27069
  mcpRegistry,
25274
- recoveryLock,
25275
- wpaths,
27070
+ recoveryLock: initialRecoveryLock,
27071
+ wpaths: initialWpaths,
25276
27072
  modelsRegistry,
25277
- projectRoot,
27073
+ projectRoot: initialProjectRoot,
25278
27074
  flags,
25279
27075
  positional,
25280
27076
  effectiveMaxContext,
@@ -25288,6 +27084,8 @@ async function execute(deps) {
25288
27084
  getPickableProviders,
25289
27085
  switchProviderAndModel,
25290
27086
  director,
27087
+ getDirector,
27088
+ coordinatorController,
25291
27089
  fleetRoster,
25292
27090
  fleetStreamController,
25293
27091
  interruptController,
@@ -25324,6 +27122,11 @@ async function execute(deps) {
25324
27122
  restoredToolCalls,
25325
27123
  needsSetup
25326
27124
  } = deps;
27125
+ let wpaths = initialWpaths;
27126
+ let projectRoot = initialProjectRoot;
27127
+ let activeSessionStore = sessionStore;
27128
+ let activeRecoveryLock = initialRecoveryLock;
27129
+ let detachActiveTodosCheckpoint = detachTodosCheckpoint;
25327
27130
  const rootTraceId = context.traceId;
25328
27131
  const storageLog = (event, payload) => {
25329
27132
  const traceId = payload.traceId ?? rootTraceId;
@@ -25372,8 +27175,8 @@ async function execute(deps) {
25372
27175
  timeoutMs: 3e5
25373
27176
  };
25374
27177
  const subagentId = await dir.spawn(cfg);
25375
- const { randomUUID: randomUUID7 } = await import('crypto');
25376
- const taskId = randomUUID7();
27178
+ const { randomUUID: randomUUID8 } = await import('crypto');
27179
+ const taskId = randomUUID8();
25377
27180
  await dir.assign({
25378
27181
  id: taskId,
25379
27182
  description: taskDesc,
@@ -25588,8 +27391,11 @@ async function execute(deps) {
25588
27391
  let pendingProjectSwitch = null;
25589
27392
  const coordinatorEvents = /* @__PURE__ */ new Set();
25590
27393
  let autonomousCoordinator = null;
25591
- const onDirectorReady = (dir) => {
25592
- if (autonomousCoordinator) return;
27394
+ let coordinatorRun = null;
27395
+ const ensureAutonomousCoordinator = () => {
27396
+ if (autonomousCoordinator) return autonomousCoordinator;
27397
+ const currentDirector = getDirector?.() ?? director;
27398
+ if (!currentDirector) return null;
25593
27399
  const transcript = context.session.transcriptPath;
25594
27400
  const sessionDir = transcript ? path4.dirname(transcript) : wpaths.projectDir;
25595
27401
  const llmProvider = {
@@ -25604,7 +27410,7 @@ async function execute(deps) {
25604
27410
  type: "text",
25605
27411
  text: `Decision: ${prompt.question}
25606
27412
 
25607
- Context: ${prompt.context}
27413
+ Context: ${JSON.stringify(prompt.context)}
25608
27414
 
25609
27415
  Options:
25610
27416
  ${prompt.options.map((o, i) => ` ${i + 1}. [${o.id}] ${o.label}${o.consequence ? ` \u2014 ${o.consequence}` : ""}`).join("\n")}
@@ -25641,7 +27447,9 @@ Reply with ONLY the JSON object.`
25641
27447
  };
25642
27448
  autonomousCoordinator = new AutonomousCoordinator({
25643
27449
  sessionDir,
25644
- fleet: dir.fleet,
27450
+ fleet: currentDirector.fleet,
27451
+ fleetManager: currentDirector.fleetManager,
27452
+ director: currentDirector,
25645
27453
  mailbox,
25646
27454
  selfAgentId: `leader@${context.session.id ?? "unknown"}`,
25647
27455
  selfAgentName: "Leader",
@@ -25650,16 +27458,168 @@ Reply with ONLY the JSON object.`
25650
27458
  for (const fn of coordinatorEvents) fn(event);
25651
27459
  }
25652
27460
  });
25653
- deps.onCoordinatorStop = () => autonomousCoordinator?.stop();
27461
+ deps.onCoordinatorStop = () => autonomousCoordinator?.dispose();
27462
+ if (coordinatorController) {
27463
+ coordinatorController["onCoordinatorStart"] = (goal) => {
27464
+ const coordinator = autonomousCoordinator;
27465
+ if (!coordinator) return;
27466
+ coordinator.run({ goal: goal ?? "Improve the codebase", runUntilComplete: true }).then(() => void 0).catch((err) => console.error("[coordinator] run() failed:", err));
27467
+ };
27468
+ coordinatorController["onCoordinatorStop"] = () => autonomousCoordinator?.stop();
27469
+ coordinatorController["onCoordinatorTasks"] = async () => {
27470
+ if (!autonomousCoordinator) return null;
27471
+ await autonomousCoordinator.graph.load();
27472
+ return autonomousCoordinator.auction.getPendingTasks().map((task) => ({ id: task.id, title: task.title, priority: task.priority, tags: task.tags }));
27473
+ };
27474
+ coordinatorController["onCoordinatorClaim"] = async (taskId) => {
27475
+ if (!autonomousCoordinator) return "No coordinator is active.";
27476
+ await autonomousCoordinator.graph.load();
27477
+ const goal = autonomousCoordinator.graph.get(taskId);
27478
+ if (!goal || goal.type !== "goal") return `Task ${taskId.slice(0, 8)} not found.`;
27479
+ if (goal.status !== "pending") return `Task ${taskId.slice(0, 8)} is ${goal.status}, not claimable.`;
27480
+ const ok = await autonomousCoordinator.auction.claim(
27481
+ taskId,
27482
+ `terminal@${context.session.id ?? "unknown"}`,
27483
+ "Terminal worker"
27484
+ );
27485
+ if (!ok) return `Task ${taskId.slice(0, 8)} could not be claimed.`;
27486
+ return { description: goal.description };
27487
+ };
27488
+ coordinatorController["onCoordinatorComplete"] = async (taskId, result) => {
27489
+ if (!autonomousCoordinator) return "No coordinator is active.";
27490
+ await autonomousCoordinator.graph.load();
27491
+ const goal = autonomousCoordinator.graph.get(taskId);
27492
+ if (!goal || goal.type !== "goal") return `Task ${taskId.slice(0, 8)} not found.`;
27493
+ if (goal.status !== "in_progress") return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot complete.`;
27494
+ await autonomousCoordinator.reportTaskCompletion(taskId, result ?? "Terminal worker completed the task");
27495
+ return null;
27496
+ };
27497
+ coordinatorController["onCoordinatorFail"] = async (taskId, error) => {
27498
+ if (!autonomousCoordinator) return "No coordinator is active.";
27499
+ await autonomousCoordinator.graph.load();
27500
+ const goal = autonomousCoordinator.graph.get(taskId);
27501
+ if (!goal || goal.type !== "goal") return `Task ${taskId.slice(0, 8)} not found.`;
27502
+ if (goal.status !== "in_progress") return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot fail.`;
27503
+ await autonomousCoordinator.reportTaskFailure(taskId, error);
27504
+ return null;
27505
+ };
27506
+ coordinatorController["onCoordinatorStatus"] = async () => {
27507
+ if (!autonomousCoordinator) return null;
27508
+ await autonomousCoordinator.syncFromGraph();
27509
+ const stats2 = autonomousCoordinator.getStats();
27510
+ return {
27511
+ goals: { total: stats2.goals.total, done: stats2.goals.done, pending: stats2.goals.pending, failed: stats2.goals.failed },
27512
+ dag: { running: stats2.dag.running, ready: stats2.dag.ready, done: stats2.dag.done, failed: stats2.dag.failed },
27513
+ auction: { pending: stats2.auction.pending, inProgress: stats2.auction.in_progress }
27514
+ };
27515
+ };
27516
+ }
27517
+ return autonomousCoordinator;
25654
27518
  };
25655
- if (director) onDirectorReady(director);
27519
+ if (director) ensureAutonomousCoordinator();
25656
27520
  const offDirectorSpawned = events.onPattern("subagent.spawned", () => {
25657
- const dir = director;
25658
- if (dir) {
25659
- offDirectorSpawned();
25660
- onDirectorReady(dir);
25661
- }
27521
+ if (ensureAutonomousCoordinator()) offDirectorSpawned();
25662
27522
  });
27523
+ const switchProjectInPlace = async (targetRoot, displayName) => {
27524
+ const resolved = path4.resolve(targetRoot);
27525
+ const stat8 = await fsp5.stat(resolved).catch(() => null);
27526
+ if (!stat8?.isDirectory()) return `Cannot switch: not a directory: ${resolved}`;
27527
+ const oldWriter = context.session;
27528
+ const oldUsage = tokenCounter.total();
27529
+ const oldRecoveryLock = activeRecoveryLock;
27530
+ const oldProjectRoot = projectRoot;
27531
+ const nextWpaths = resolveWstackPaths({ projectRoot: resolved, globalRoot: wpaths.globalRoot });
27532
+ await fsp5.mkdir(nextWpaths.projectSessions, { recursive: true });
27533
+ const nextSessionStore = new DefaultSessionStore({ dir: nextWpaths.projectSessions });
27534
+ const nextWriter = await nextSessionStore.create({
27535
+ id: "",
27536
+ title: "",
27537
+ model: context.model,
27538
+ provider: context.provider.id ?? config.provider
27539
+ });
27540
+ detachActiveTodosCheckpoint?.();
27541
+ process.chdir(resolved);
27542
+ projectRoot = resolved;
27543
+ wpaths = nextWpaths;
27544
+ activeSessionStore = nextSessionStore;
27545
+ activeRecoveryLock = new RecoveryLock({ dir: nextWpaths.projectSessions, sessionStore: nextSessionStore });
27546
+ context.cwd = resolved;
27547
+ context.projectRoot = resolved;
27548
+ context.workingDir = resolved;
27549
+ context.session = nextWriter;
27550
+ context.state.replaceMessages([]);
27551
+ context.state.replaceTodos([]);
27552
+ context.clearFileTracking();
27553
+ context.tokenCounter.reset();
27554
+ context.meta["packageTrackerOpts"] = { storageDir: nextWpaths.projectDir, projectRoot: resolved };
27555
+ context.state.setMeta("plan.path", path4.join(nextWpaths.projectSessions, `${nextWriter.id}.plan.json`));
27556
+ context.state.setMeta("task.path", path4.join(nextWpaths.projectSessions, `${nextWriter.id}.tasks.json`));
27557
+ detachActiveTodosCheckpoint = attachTodosCheckpoint(
27558
+ context.state,
27559
+ path4.join(nextWpaths.projectSessions, `${nextWriter.id}.todos.json`),
27560
+ nextWriter.id,
27561
+ events,
27562
+ context.traceId
27563
+ );
27564
+ setQueuedMessagesSnapshot(context, []);
27565
+ try {
27566
+ const switchMode = modeId && modeId !== "default" && modeStore ? await modeStore.getMode(modeId) : void 0;
27567
+ const switchBuilder = new DefaultSystemPromptBuilder({
27568
+ memoryStore: memoryStore ?? void 0,
27569
+ skillLoader,
27570
+ modeStore,
27571
+ modeId: modeId ?? "default",
27572
+ modePrompt: switchMode?.prompt ?? ""
27573
+ });
27574
+ context.systemPrompt = await switchBuilder.build({
27575
+ cwd: resolved,
27576
+ projectRoot: resolved,
27577
+ tools: agent.tools.list(),
27578
+ provider: context.provider.id,
27579
+ model: context.model
27580
+ });
27581
+ } catch (err) {
27582
+ console.error(
27583
+ JSON.stringify({
27584
+ level: "warn",
27585
+ event: "execution.project_switch_prompt_rebuild_failed",
27586
+ message: err instanceof Error ? err.message : String(err),
27587
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
27588
+ })
27589
+ );
27590
+ }
27591
+ void (async () => {
27592
+ try {
27593
+ await oldWriter.append({ type: "session_end", ts: (/* @__PURE__ */ new Date()).toISOString(), usage: oldUsage });
27594
+ await oldWriter.close();
27595
+ } catch (err) {
27596
+ console.error(
27597
+ JSON.stringify({
27598
+ level: "warn",
27599
+ event: "execution.project_switch_old_session_close_failed",
27600
+ message: err instanceof Error ? err.message : String(err),
27601
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
27602
+ })
27603
+ );
27604
+ }
27605
+ await oldRecoveryLock.clear().catch(() => void 0);
27606
+ })();
27607
+ try {
27608
+ await activeRecoveryLock.write(nextWriter.id);
27609
+ } catch (err) {
27610
+ console.error(
27611
+ JSON.stringify({
27612
+ level: "error",
27613
+ event: "execution.project_switch_recovery_lock_failed",
27614
+ message: err instanceof Error ? err.message : String(err),
27615
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
27616
+ })
27617
+ );
27618
+ }
27619
+ const emitUntyped = events.emit;
27620
+ emitUntyped("project.switched", { from: oldProjectRoot, to: resolved, name: displayName });
27621
+ return null;
27622
+ };
25663
27623
  try {
25664
27624
  code = await runTui({
25665
27625
  agent,
@@ -25729,6 +27689,9 @@ Reply with ONLY the JSON object.`
25729
27689
  const autonomy = cfg.autonomy;
25730
27690
  const rawMode = autonomy?.defaultMode;
25731
27691
  const mode = rawMode === "suggest" || rawMode === "auto" ? rawMode : "off";
27692
+ const modelRuntime = cfg.modelRuntime;
27693
+ const reasoningEffortRaw = modelRuntime?.reasoning?.effort;
27694
+ const reasoningEffort = reasoningEffortRaw === "none" || reasoningEffortRaw === "minimal" || reasoningEffortRaw === "low" || reasoningEffortRaw === "medium" || reasoningEffortRaw === "high" || reasoningEffortRaw === "xhigh" || reasoningEffortRaw === "max" ? reasoningEffortRaw : "high";
25732
27695
  return {
25733
27696
  mode,
25734
27697
  delayMs: autonomy?.autoProceedDelayMs ?? 45e3,
@@ -25759,12 +27722,17 @@ Reply with ONLY the JSON object.`
25759
27722
  restrictFsToRoot: cfg.tools?.restrictToProjectRoot ?? false,
25760
27723
  autoProceedMaxIterations: cfg.autonomy?.autoProceedMaxIterations ?? 50,
25761
27724
  debugStream: cfg.debugStream ?? false,
27725
+ statuslineMode: autonomy?.statuslineMode === "minimum" ? "minimum" : "detailed",
25762
27726
  configScope: cfg.configScope ?? "global",
25763
27727
  enhanceDelayMs: cfg.autonomy?.enhanceDelayMs ?? 6e4,
25764
27728
  enhanceEnabled: cfg.autonomy?.enhance ?? true,
25765
27729
  enhanceLanguage: cfg.autonomy?.enhanceLanguage === "english" ? "english" : "original",
25766
27730
  mouseMode: autonomy?.mouseMode ?? false,
25767
27731
  autonomyNextPrompt: cfg.autonomy?.autonomyNextPrompt ?? "auto {{suggestion}}",
27732
+ reasoningMode: modelRuntime?.reasoning?.mode === "on" || modelRuntime?.reasoning?.mode === "off" ? modelRuntime.reasoning.mode : "auto",
27733
+ reasoningEffort,
27734
+ reasoningPreserve: modelRuntime?.reasoning?.preserve === true,
27735
+ cacheTtl: modelRuntime?.cache?.ttl === "5m" || modelRuntime?.cache?.ttl === "1h" ? modelRuntime.cache.ttl : "default",
25768
27736
  breakerEnabled: cfg.circuitBreaker?.enabled === true,
25769
27737
  breakerAutoKillResetMs: cfg.circuitBreaker?.autoKillResetMs ?? 6e4
25770
27738
  };
@@ -25790,6 +27758,7 @@ Reply with ONLY the JSON object.`
25790
27758
  if (s.mouseMode !== void 0) a["mouseMode"] = s.mouseMode;
25791
27759
  if (s.enhanceEnabled !== void 0) a["enhance"] = s.enhanceEnabled;
25792
27760
  if (s.enhanceLanguage !== void 0) a["enhanceLanguage"] = s.enhanceLanguage;
27761
+ if (s.statuslineMode !== void 0) a["statuslineMode"] = s.statuslineMode;
25793
27762
  if (s.autonomyNextPrompt !== void 0) a["autonomyNextPrompt"] = s.autonomyNextPrompt;
25794
27763
  if (s.autoProceedMaxIterations !== void 0)
25795
27764
  a["autoProceedMaxIterations"] = s.autoProceedMaxIterations;
@@ -25945,7 +27914,7 @@ Reply with ONLY the JSON object.`
25945
27914
  // The coordinator tracks goals, tasks, knowledge, and consensus across all
25946
27915
  // active sessions in the same project. It runs independently of the leader
25947
27916
  // agent and is accessible to any session in the project via the GlobalMailbox.
25948
- getAutonomousCoordinator: () => autonomousCoordinator,
27917
+ getAutonomousCoordinator: () => ensureAutonomousCoordinator(),
25949
27918
  subscribeCoordinatorEvents: (fn) => {
25950
27919
  coordinatorEvents.add(fn);
25951
27920
  return () => {
@@ -25953,17 +27922,105 @@ Reply with ONLY the JSON object.`
25953
27922
  };
25954
27923
  },
25955
27924
  onCoordinatorStart: (goal) => {
25956
- if (!autonomousCoordinator) {
25957
- console.error("[coordinator] not ready \u2014 no director yet (spawn a subagent first)");
27925
+ const coordinator = ensureAutonomousCoordinator();
27926
+ if (!coordinator) {
27927
+ console.error("[coordinator] not ready \u2014 no director available");
25958
27928
  return;
25959
27929
  }
25960
- autonomousCoordinator.run({ goal: goal ?? "" }).catch((err) => {
27930
+ if (coordinatorRun) return;
27931
+ coordinatorRun = coordinator.run({ goal: goal ?? "Improve the codebase", runUntilComplete: true }).then(() => void 0).catch((err) => {
25961
27932
  console.error("[coordinator] run() failed:", err);
27933
+ }).finally(() => {
27934
+ coordinatorRun = null;
25962
27935
  });
25963
27936
  },
25964
27937
  onCoordinatorStop: () => {
25965
27938
  autonomousCoordinator?.stop();
25966
27939
  },
27940
+ onCoordinatorTasks: async () => {
27941
+ const coordinator = ensureAutonomousCoordinator();
27942
+ if (!coordinator) return null;
27943
+ await coordinator.graph.load();
27944
+ return coordinator.auction.getPendingTasks().map((task) => ({
27945
+ id: task.id,
27946
+ title: task.title,
27947
+ priority: task.priority,
27948
+ tags: task.tags
27949
+ }));
27950
+ },
27951
+ onCoordinatorClaim: async (taskId) => {
27952
+ const coordinator = ensureAutonomousCoordinator();
27953
+ if (!coordinator) return "No coordinator is active.";
27954
+ await coordinator.graph.load();
27955
+ const goal = coordinator.graph.get(taskId);
27956
+ if (!goal || goal.type !== "goal") {
27957
+ return `Task ${taskId.slice(0, 8)} not found in the coordinator graph.`;
27958
+ }
27959
+ if (goal.status !== "pending") {
27960
+ return `Task ${taskId.slice(0, 8)} is ${goal.status}, not claimable.`;
27961
+ }
27962
+ const ok = await coordinator.auction.claim(
27963
+ taskId,
27964
+ `terminal@${context.session.id ?? "unknown"}`,
27965
+ "Terminal worker"
27966
+ );
27967
+ if (!ok) {
27968
+ return `Task ${taskId.slice(0, 8)} could not be claimed (status changed?).`;
27969
+ }
27970
+ return { description: goal.description };
27971
+ },
27972
+ onCoordinatorComplete: async (taskId, result) => {
27973
+ const coordinator = ensureAutonomousCoordinator();
27974
+ if (!coordinator) return "No coordinator is active.";
27975
+ await coordinator.graph.load();
27976
+ const goal = coordinator.graph.get(taskId);
27977
+ if (!goal || goal.type !== "goal") {
27978
+ return `Task ${taskId.slice(0, 8)} not found in the coordinator graph.`;
27979
+ }
27980
+ if (goal.status !== "in_progress") {
27981
+ return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot complete.`;
27982
+ }
27983
+ await coordinator.reportTaskCompletion(taskId, result ?? "Terminal worker completed the task");
27984
+ return null;
27985
+ },
27986
+ onCoordinatorFail: async (taskId, error) => {
27987
+ const coordinator = ensureAutonomousCoordinator();
27988
+ if (!coordinator) return "No coordinator is active.";
27989
+ await coordinator.graph.load();
27990
+ const goal = coordinator.graph.get(taskId);
27991
+ if (!goal || goal.type !== "goal") {
27992
+ return `Task ${taskId.slice(0, 8)} not found in the coordinator graph.`;
27993
+ }
27994
+ if (goal.status !== "in_progress") {
27995
+ return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot fail.`;
27996
+ }
27997
+ await coordinator.reportTaskFailure(taskId, error);
27998
+ return null;
27999
+ },
28000
+ onCoordinatorStatus: async () => {
28001
+ const coordinator = ensureAutonomousCoordinator();
28002
+ if (!coordinator) return null;
28003
+ await coordinator.syncFromGraph();
28004
+ const stats2 = coordinator.getStats();
28005
+ return {
28006
+ goals: {
28007
+ total: stats2.goals.total,
28008
+ done: stats2.goals.done,
28009
+ pending: stats2.goals.pending,
28010
+ failed: stats2.goals.failed
28011
+ },
28012
+ dag: {
28013
+ running: stats2.dag.running,
28014
+ ready: stats2.dag.ready,
28015
+ done: stats2.dag.done,
28016
+ failed: stats2.dag.failed
28017
+ },
28018
+ auction: {
28019
+ pending: stats2.auction.pending,
28020
+ inProgress: stats2.auction.in_progress
28021
+ }
28022
+ };
28023
+ },
25967
28024
  // /clear: signal the TUI to wipe entries and reset fleet/leader stats
25968
28025
  // AND bump the context chip version — so the display reflects a
25969
28026
  // completely fresh session after the backend has been cleared.
@@ -26013,6 +28070,7 @@ Reply with ONLY the JSON object.`
26013
28070
  initialGoal: goalFlag,
26014
28071
  initialAsk: askFlag,
26015
28072
  projectRoot,
28073
+ appConfig: config,
26016
28074
  getSDDContext: async () => {
26017
28075
  const { getActiveSDDContext: getActiveSDDContext2 } = await Promise.resolve().then(() => (init_sdd(), sdd_exports));
26018
28076
  return getActiveSDDContext2();
@@ -26091,9 +28149,9 @@ Reply with ONLY the JSON object.`
26091
28149
  restoredToolCalls,
26092
28150
  // ── Session resume support ──────────────────────────────────
26093
28151
  listSessions: async (limit = 20) => {
26094
- if (!sessionStore) return [];
26095
- const summaries = await sessionStore.list(limit);
26096
- const currentId = session.id;
28152
+ if (!activeSessionStore) return [];
28153
+ const summaries = await activeSessionStore.list(limit);
28154
+ const currentId = agent.ctx.session?.id ?? session.id;
26097
28155
  return summaries.map((s) => ({
26098
28156
  id: s.id,
26099
28157
  title: s.title ?? "",
@@ -26108,7 +28166,7 @@ Reply with ONLY the JSON object.`
26108
28166
  }));
26109
28167
  },
26110
28168
  onResumeSession: async (sessionId) => {
26111
- if (!sessionStore) return null;
28169
+ if (!activeSessionStore) return null;
26112
28170
  try {
26113
28171
  const { SessionRegistry } = await import('@wrongstack/core');
26114
28172
  const registry = new SessionRegistry(path4.dirname(wpaths.globalConfig));
@@ -26124,7 +28182,7 @@ Reply with ONLY the JSON object.`
26124
28182
  if (err instanceof Error && err.message.startsWith("Session is open")) throw err;
26125
28183
  }
26126
28184
  try {
26127
- const resumed = await sessionStore.resume(sessionId);
28185
+ const resumed = await activeSessionStore.resume(sessionId);
26128
28186
  const meta = resumed.data.metadata;
26129
28187
  agent.ctx.state.replaceMessages(resumed.data.messages);
26130
28188
  if (meta.model && meta.model !== agent.ctx.model) {
@@ -26178,7 +28236,7 @@ Reply with ONLY the JSON object.`
26178
28236
  tokenCounter.reset();
26179
28237
  void (async () => {
26180
28238
  try {
26181
- await recoveryLock.clear();
28239
+ await activeRecoveryLock.clear();
26182
28240
  } catch (err) {
26183
28241
  console.error(
26184
28242
  JSON.stringify({
@@ -26190,7 +28248,7 @@ Reply with ONLY the JSON object.`
26190
28248
  );
26191
28249
  }
26192
28250
  try {
26193
- await recoveryLock.write(resumed.writer.id);
28251
+ await activeRecoveryLock.write(resumed.writer.id);
26194
28252
  } catch (err) {
26195
28253
  console.error(
26196
28254
  JSON.stringify({
@@ -26238,60 +28296,50 @@ Reply with ONLY the JSON object.`
26238
28296
  },
26239
28297
  /**
26240
28298
  * Called when the user selects a project in the picker.
26241
- * For projects: loads manifest, validates the project, updates lastSeen,
26242
- * stores the pending switch, and returns. The TUI calls requestExit(42)
26243
- * which causes runTui to return. The host CLI then spawns wstack in
26244
- * the target directory.
26245
- * For actions: handled by the slash command path (no-op here).
28299
+ * Re-roots the live TUI process in place: new Context root, fresh
28300
+ * per-project session writer, rebuilt system prompt, and no spawned
28301
+ * replacement process.
26246
28302
  */
26247
28303
  onProjectSelect: async (slug, kind) => {
26248
- if (kind === "action") {
26249
- if (slug === "new-session") {
26250
- pendingProjectSwitch = {
26251
- root: projectRoot,
26252
- name: path4.basename(projectRoot) || projectRoot
26253
- };
26254
- }
26255
- return;
26256
- }
26257
- const { loadManifest: loadManifest2 } = await Promise.resolve().then(() => (init_project_utils(), project_utils_exports));
26258
28304
  try {
28305
+ if (kind === "action") {
28306
+ if (slug === "new-session") {
28307
+ const name = path4.basename(projectRoot) || projectRoot;
28308
+ const err2 = await switchProjectInPlace(projectRoot, name);
28309
+ if (err2) renderer.write(color.red(`Project switch failed: ${err2}
28310
+ `));
28311
+ }
28312
+ return;
28313
+ }
28314
+ const { loadManifest: loadManifest2, saveManifest: saveManifest2 } = await Promise.resolve().then(() => (init_project_utils(), project_utils_exports));
26259
28315
  const manifest = await loadManifest2(wpaths.globalConfig);
26260
28316
  const project = manifest.projects.find((p) => p.slug === slug);
26261
28317
  if (!project) return;
26262
- if (project.root === projectRoot) return;
28318
+ const targetRoot = path4.resolve(project.root);
28319
+ if (path4.resolve(projectRoot) === targetRoot) return;
26263
28320
  const fleetStatus = director?.status();
26264
28321
  const fleetRunning = fleetStatus?.subagents.filter((a) => a.status === "running").length ?? 0;
26265
- const eternalEngine = getEternalEngine?.();
26266
- const parallelEngine = getParallelEngine?.();
26267
- const eternalActive = eternalEngine?.currentState === "running";
26268
- const parallelActive = parallelEngine?.currentState === "running";
28322
+ const eternalActive = getEternalEngine?.()?.currentState === "running";
28323
+ const parallelActive = getParallelEngine?.()?.currentState === "running";
26269
28324
  const hasActiveAgents = fleetRunning > 0 || eternalActive || parallelActive;
26270
28325
  if (hasActiveAgents) {
26271
28326
  const parts = [
26272
- color.yellow(
26273
- "\u26A0 Switching projects exits this wstack \u2014 running agents will stop:"
26274
- )
28327
+ color.yellow("\u26A0 Switching project in place; active background work is still tied to the previous project:")
26275
28328
  ];
26276
- if (fleetRunning > 0) {
26277
- parts.push(color.dim(` \u2022 ${fleetRunning} subagent(s) currently running`));
26278
- }
26279
- if (eternalActive) {
26280
- parts.push(color.dim(" \u2022 Eternal engine is active"));
26281
- }
26282
- if (parallelActive) {
26283
- parts.push(color.dim(" \u2022 Parallel engine is active"));
26284
- }
28329
+ if (fleetRunning > 0) parts.push(color.dim(` \u2022 ${fleetRunning} subagent(s) currently running`));
28330
+ if (eternalActive) parts.push(color.dim(" \u2022 Eternal engine is active"));
28331
+ if (parallelActive) parts.push(color.dim(" \u2022 Parallel engine is active"));
26285
28332
  parts.push("");
26286
- parts.push(color.dim(` Opening new session in: ${project.name}`));
28333
+ parts.push(color.dim(` New project: ${project.name}`));
26287
28334
  renderer.write(`
26288
28335
  ${parts.join("\n")}
26289
28336
  `);
26290
28337
  }
26291
28338
  project.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
26292
- const { saveManifest: saveManifest2 } = await Promise.resolve().then(() => (init_project_utils(), project_utils_exports));
26293
28339
  await saveManifest2(manifest, wpaths.globalConfig);
26294
- pendingProjectSwitch = { root: project.root, name: project.name };
28340
+ const err = await switchProjectInPlace(targetRoot, project.name);
28341
+ if (err) renderer.write(color.red(`Project switch failed: ${err}
28342
+ `));
26295
28343
  } catch (err) {
26296
28344
  renderer.write(
26297
28345
  color.red(
@@ -26361,18 +28409,19 @@ ${parts.join("\n")}
26361
28409
  session,
26362
28410
  port: Number.parseInt(String(flags.port ?? "3457"), 10),
26363
28411
  projectRoot,
28412
+ appConfig: config,
26364
28413
  open: !!flags.open,
26365
28414
  modelsRegistry,
26366
28415
  globalConfigPath: wpaths.globalConfig,
26367
28416
  mcpRegistry,
26368
28417
  subscribeEternalIteration,
26369
- sessionStore,
28418
+ sessionStore: activeSessionStore,
26370
28419
  sessionsDir: wpaths.projectSessions,
26371
28420
  brain,
26372
28421
  brainSettings,
26373
28422
  getBrainLog,
26374
28423
  onSessionSwapped: (newSessionId) => {
26375
- void recoveryLock.clear().then(() => recoveryLock.write(newSessionId)).catch(() => void 0);
28424
+ void activeRecoveryLock.clear().then(() => activeRecoveryLock.write(newSessionId)).catch(() => void 0);
26376
28425
  },
26377
28426
  memoryStore,
26378
28427
  skillLoader,
@@ -26404,12 +28453,12 @@ ${parts.join("\n")}
26404
28453
  }
26405
28454
  }
26406
28455
  });
26407
- const webuiExit = new Promise((resolve11) => {
28456
+ const webuiExit = new Promise((resolve12) => {
26408
28457
  const onSigint = () => {
26409
28458
  renderer.setSilent(false);
26410
28459
  renderer.write("\n");
26411
28460
  renderer.writeInfo(color.yellow(" Shutting down WebUI server\u2026"));
26412
- resolve11(0);
28461
+ resolve12(0);
26413
28462
  };
26414
28463
  process.on("SIGINT", onSigint);
26415
28464
  process.on("SIGTERM", onSigint);
@@ -26417,13 +28466,13 @@ ${parts.join("\n")}
26417
28466
  renderer.setSilent(false);
26418
28467
  process.off("SIGINT", onSigint);
26419
28468
  process.off("SIGTERM", onSigint);
26420
- resolve11(0);
28469
+ resolve12(0);
26421
28470
  }).catch((err) => {
26422
28471
  renderer.setSilent(false);
26423
28472
  process.off("SIGINT", onSigint);
26424
28473
  process.off("SIGTERM", onSigint);
26425
28474
  console.debug(`[execution] webui error: ${err}`);
26426
- resolve11(1);
28475
+ resolve12(1);
26427
28476
  });
26428
28477
  });
26429
28478
  code = await webuiExit;
@@ -26439,6 +28488,8 @@ ${parts.join("\n")}
26439
28488
  attachments,
26440
28489
  effectiveMaxContext,
26441
28490
  projectName: path4.basename(projectRoot) || void 0,
28491
+ projectRoot,
28492
+ appConfig: config,
26442
28493
  getAutonomy,
26443
28494
  onAutonomy,
26444
28495
  getNextPredict,
@@ -26494,7 +28545,7 @@ ${parts.join("\n")}
26494
28545
  events.emit("session.ended", { id: activeSession.id, usage: tokenCounter.total() });
26495
28546
  await pendingChimeraWork;
26496
28547
  await activeSession.close();
26497
- await recoveryLock.clear().catch(() => void 0);
28548
+ await activeRecoveryLock.clear().catch(() => void 0);
26498
28549
  await reader.close();
26499
28550
  }
26500
28551
  return code;
@@ -27812,6 +29863,19 @@ function setupMetrics(params) {
27812
29863
  function setupPipelines(params) {
27813
29864
  const { events, logger } = params;
27814
29865
  const pipelines = createDefaultPipelines();
29866
+ if (params.modelRuntime) {
29867
+ const mr = params.modelRuntime;
29868
+ pipelines.request.use({
29869
+ name: "ModelRuntimeSettings",
29870
+ async handler(req2) {
29871
+ return applyModelRuntime(req2, {
29872
+ getSettings: mr.getSettings,
29873
+ getReasoningConfig: mr.getReasoningConfig,
29874
+ onWarning: mr.onWarning
29875
+ });
29876
+ }
29877
+ });
29878
+ }
27815
29879
  const installBoundary = (p) => {
27816
29880
  p.setErrorHandler((ev) => {
27817
29881
  const fromPlugin = !!ev.owner && ev.owner !== "core";
@@ -28256,6 +30320,28 @@ async function main(argv) {
28256
30320
  const handler = earlyFlags["help"] === true ? helpCmd : versionCmd;
28257
30321
  return await handler([], { renderer: stubRenderer });
28258
30322
  }
30323
+ if (earlyFlags["hq"] === true) {
30324
+ const { startHqServer: startHqServer2 } = await Promise.resolve().then(() => (init_hq_server(), hq_server_exports));
30325
+ const host = typeof earlyFlags["host"] === "string" ? earlyFlags["host"] : "127.0.0.1";
30326
+ const port = typeof earlyFlags["port"] === "string" ? Number.parseInt(earlyFlags["port"], 10) : 3499;
30327
+ const dataDir = typeof earlyFlags["data-dir"] === "string" ? earlyFlags["data-dir"] : void 0;
30328
+ const handle = await startHqServer2({ host, port, strictPort: earlyFlags["strict-port"] === true, ...dataDir !== void 0 ? { dataDir } : {} });
30329
+ if (earlyFlags["open"] === true) {
30330
+ try {
30331
+ const { openBrowser: openBrowser5 } = await import('@wrongstack/webui/server');
30332
+ openBrowser5(handle.firstRunSetup?.browserUrl ?? `http://${handle.host}:${handle.port}`);
30333
+ } catch {
30334
+ }
30335
+ }
30336
+ await new Promise((resolve12) => {
30337
+ const shutdown = () => {
30338
+ void handle.close().then(() => resolve12());
30339
+ };
30340
+ process.on("SIGINT", shutdown);
30341
+ process.on("SIGTERM", shutdown);
30342
+ });
30343
+ return 0;
30344
+ }
28259
30345
  const ctx = await boot(argv);
28260
30346
  if (typeof ctx === "number") return ctx;
28261
30347
  let {
@@ -28499,7 +30585,10 @@ async function main(argv) {
28499
30585
  const promptBuilder = container.resolve(TOKENS.SystemPromptBuilder);
28500
30586
  let onlineAgents = [];
28501
30587
  try {
28502
- const systemMailbox = new GlobalMailbox(wpaths.projectDir);
30588
+ const hqPublisher2 = createHqPublisherFromEnv({ clientKind: "cli", projectRoot, projectName: path4.basename(projectRoot), appConfig: config });
30589
+ hqPublisher2?.connect();
30590
+ if (hqPublisher2) teardownHandlers.push(() => hqPublisher2.close());
30591
+ const systemMailbox = new GlobalMailbox(wpaths.projectDir, void 0, hqPublisher2);
28503
30592
  onlineAgents = await systemMailbox.getAgentStatuses();
28504
30593
  } catch {
28505
30594
  }
@@ -28710,7 +30799,27 @@ async function main(argv) {
28710
30799
  }).catch(() => {
28711
30800
  });
28712
30801
  });
28713
- const pipelines = setupPipelines({ events, logger });
30802
+ let activeReasoningConfig;
30803
+ const refreshActiveReasoningConfig = async (providerId, modelId) => {
30804
+ try {
30805
+ const resolved = await modelsRegistry.getModel(providerId, modelId);
30806
+ activeReasoningConfig = resolved?.capabilities.reasoningConfig;
30807
+ } catch {
30808
+ activeReasoningConfig = void 0;
30809
+ }
30810
+ };
30811
+ void refreshActiveReasoningConfig(config.provider, config.model);
30812
+ const pipelines = setupPipelines({
30813
+ events,
30814
+ logger,
30815
+ modelRuntime: {
30816
+ getSettings: () => configStore.get().modelRuntime,
30817
+ getReasoningConfig: () => activeReasoningConfig,
30818
+ onWarning: (message) => {
30819
+ logger.warn(`model-runtime: ${message}`);
30820
+ }
30821
+ }
30822
+ });
28714
30823
  const hooksEnabled = flags["no-hooks"] !== true;
28715
30824
  const hookRegistry = new HookRegistry();
28716
30825
  if (hooksEnabled) hookRegistry.loadShellHooks(config.hooks);
@@ -28985,7 +31094,10 @@ async function main(argv) {
28985
31094
  outcome: e.intervened ? "steered the agent" : "observed (no action)"
28986
31095
  });
28987
31096
  });
28988
- const brainMailbox = new GlobalMailbox(wpaths.projectDir, events);
31097
+ const hqPublisher = createHqPublisherFromEnv({ clientKind: "cli", projectRoot, projectName: path4.basename(projectRoot), appConfig: config });
31098
+ hqPublisher?.connect();
31099
+ if (hqPublisher) teardownHandlers.push(() => hqPublisher.close());
31100
+ const brainMailbox = new GlobalMailbox(wpaths.projectDir, events, hqPublisher);
28989
31101
  const brainMonitor = new BrainMonitor({
28990
31102
  events,
28991
31103
  brain,
@@ -29213,6 +31325,7 @@ async function main(argv) {
29213
31325
  log: (line) => renderer.write(`${line}
29214
31326
  `)
29215
31327
  });
31328
+ const coordinatorController = {};
29216
31329
  const slashCmds = buildBuiltinSlashCommands({
29217
31330
  registry: slashRegistry,
29218
31331
  toolRegistry,
@@ -29254,6 +31367,7 @@ async function main(argv) {
29254
31367
  brain,
29255
31368
  brainSettings,
29256
31369
  getBrainLog: () => brainLog,
31370
+ coordinatorController,
29257
31371
  confirm: async (question, defaultYes = true) => {
29258
31372
  if (!isStdinTTY()) return false;
29259
31373
  const hint = defaultYes ? "[Y/n/q]" : "[y/N/q]";
@@ -29507,12 +31621,12 @@ ${color.dim("\u2500".repeat(40))}` : "";
29507
31621
  if (!f.endsWith(".jsonl")) continue;
29508
31622
  const full = path4.join(runDir, f);
29509
31623
  try {
29510
- const stat7 = await fsp5.stat(full);
31624
+ const stat8 = await fsp5.stat(full);
29511
31625
  found.push({
29512
31626
  runId,
29513
31627
  subagentId: f.replace(/\.jsonl$/, ""),
29514
31628
  file: full,
29515
- size: stat7.size
31629
+ size: stat8.size
29516
31630
  });
29517
31631
  } catch {
29518
31632
  }
@@ -29820,7 +31934,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
29820
31934
  onBeforeExit: async () => {
29821
31935
  const cwd2 = projectRoot;
29822
31936
  const statusResult = await new Promise(
29823
- (resolve11, reject) => {
31937
+ (resolve12, reject) => {
29824
31938
  const child = spawn("git", ["status", "--porcelain"], {
29825
31939
  cwd: cwd2,
29826
31940
  stdio: ["ignore", "pipe", "pipe"],
@@ -29832,7 +31946,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
29832
31946
  stdout += d;
29833
31947
  });
29834
31948
  child.on("error", reject);
29835
- child.on("close", (code) => resolve11({ stdout, code: code ?? 0 }));
31949
+ child.on("close", (code) => resolve12({ stdout, code: code ?? 0 }));
29836
31950
  }
29837
31951
  );
29838
31952
  if (statusResult.stdout.trim().length > 0) {
@@ -30002,6 +32116,8 @@ Restart WrongStack to load or unload plugin code in this session.`;
30002
32116
  getPickableProviders: () => buildPickableProviders(modelsRegistry, config),
30003
32117
  switchProviderAndModel,
30004
32118
  director: director ?? null,
32119
+ getDirector: () => director,
32120
+ coordinatorController,
30005
32121
  fleetRoster: FLEET_ROSTER,
30006
32122
  fleetStreamController,
30007
32123
  interruptController,