@wrongstack/cli 0.265.1 → 0.267.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,21 +1,21 @@
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
3
  import * as fsp5 from 'fs/promises';
4
+ import { decryptConfigSecrets, encryptConfigSecrets, DefaultSecretVault, isSecretField } from '@wrongstack/core/security';
3
5
  import * as path4 from 'path';
4
6
  import { join } from 'path';
5
- 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, DefaultLogger, DefaultModelsRegistry, isStdinTTY, 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, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, AutonomousCoordinator, noOpVault, decryptConfigSecrets, encryptConfigSecrets, atomicWrite, 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';
6
- import { decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, DefaultSecretVault, isSecretField } from '@wrongstack/core/security';
7
7
  import * as crypto3 from 'crypto';
8
8
  import { createHash, randomBytes, randomUUID } from 'crypto';
9
9
  import { createRequire } from 'module';
10
10
  import * as os from 'os';
11
11
  import os__default from 'os';
12
- import { findFreePort, AutoPhaseWebSocketHandler, generateAuthToken, verifyClient, handleGitInfo, handleShellOpen, handleSkillsExport, handleSkillsEdit, handleSkillsCreate, handleSkillsUpdate, handleSkillsUninstall, handleSkillsInstall, handleSkillsContent, handleMemoryForget, handleMemoryRemember, handleMemoryList, handleFilesWrite, handleFilesRead, handleFilesTree, handleFilesList, createEternalSubscription, createHttpServer, registerInstance, openBrowser, unregisterInstance, estimateTokens as estimateTokens$1, stringifyContent, messagePreview, messageTokens, createCustomModeStore } from '@wrongstack/webui/server';
13
- import { makeProviderFromConfig, capabilitiesFor, buildProviderFactoriesFromRegistry } from '@wrongstack/providers';
12
+ 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
+ import { setOAuthTokenPersister, makeProviderFromConfig, capabilitiesFor, buildProviderFactoriesFromRegistry, refreshCopilotToken, copilotBaseUrlFromToken } from '@wrongstack/providers';
14
14
  import { toErrorMessage } from '@wrongstack/core/utils';
15
15
  import { getProcessRegistry, builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes, shutdownCodebaseIndexHost, TIER2_TOOLS, TIER3_TOOLS, TIER1_TOOLS, resetIndexCircuitBreaker } from '@wrongstack/tools';
16
16
  import { DefaultSessionStore } from '@wrongstack/core/storage';
17
17
  import { probeLocalLlm } from '@wrongstack/runtime/probe';
18
- import * as fs2 from 'fs';
18
+ import * as fs3 from 'fs';
19
19
  import { watch, writeFileSync, existsSync, readFileSync } from 'fs';
20
20
  import { SkillInstaller } from '@wrongstack/core/skills';
21
21
  import { WebSocketServer, WebSocket } from 'ws';
@@ -28,6 +28,7 @@ import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, runEnsemble, renderEnsembleT
28
28
  import { parseNextSteps } from '@wrongstack/tui';
29
29
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
30
30
  import { SubagentBudget } from '@wrongstack/core/coordination';
31
+ import { createServer } from 'http';
31
32
  import { loadBenchConfig, reportHeaderLine, readSummary, renderMarkdownReport, createPolyglotSuite, createSwebenchSuite, runBenchmark, writeJsonArtifacts, collectCellPredictions, writePredictionsJsonl, gradePolyglot, gradeSwebench } from '@wrongstack/bench';
32
33
  import { allServers } from '@wrongstack/core/infrastructure';
33
34
  import { ToolExecutor } from '@wrongstack/core/execution';
@@ -53,6 +54,108 @@ var __export = (target, all) => {
53
54
  for (var name in all)
54
55
  __defProp(target, name, { get: all[name], enumerable: true });
55
56
  };
57
+ function normalizeKeys(cfg) {
58
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
59
+ return cfg.apiKeys.map((k) => ({ ...k }));
60
+ }
61
+ if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
62
+ return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
63
+ }
64
+ return [];
65
+ }
66
+ function writeKeysBack(cfg, keys) {
67
+ if (keys.length === 0) {
68
+ delete cfg.apiKeys;
69
+ delete cfg.apiKey;
70
+ delete cfg.activeKey;
71
+ return;
72
+ }
73
+ cfg.apiKeys = keys;
74
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
75
+ delete cfg.apiKey;
76
+ if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
77
+ cfg.activeKey = active.label;
78
+ }
79
+ }
80
+ function resolveActiveApiKey(cfg) {
81
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
82
+ const active = cfg.activeKey ? cfg.apiKeys.find((k) => k.label === cfg.activeKey) : void 0;
83
+ return (active ?? cfg.apiKeys[0])?.apiKey;
84
+ }
85
+ return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
86
+ }
87
+ function activeLabel(cfg, keys) {
88
+ if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
89
+ return keys[0]?.label;
90
+ }
91
+ function maskedKey(key) {
92
+ if (!key) return color.dim("\u2014");
93
+ if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
94
+ const head = key.slice(0, 4);
95
+ const tail = key.slice(-4);
96
+ return `${color.dim(head + "\u2026")}${tail}`;
97
+ }
98
+ function nowIso() {
99
+ return (/* @__PURE__ */ new Date()).toISOString();
100
+ }
101
+ async function loadConfigProviders(configPath2, vault, opts) {
102
+ const warn = opts?.warn;
103
+ let raw;
104
+ try {
105
+ raw = await fsp5.readFile(configPath2, "utf8");
106
+ } catch (err) {
107
+ if (err.code !== "ENOENT") {
108
+ warn?.(`Could not read ${configPath2}: ${err.message}. Treating as empty.`);
109
+ }
110
+ return {};
111
+ }
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(raw);
115
+ } catch (err) {
116
+ warn?.(`Config at ${configPath2} is not valid JSON: ${err.message}`);
117
+ return {};
118
+ }
119
+ const decrypted = decryptConfigSecrets(parsed, vault);
120
+ return decrypted.providers ?? {};
121
+ }
122
+ async function mutateConfigProviders(configPath2, vault, mutator) {
123
+ let raw;
124
+ let fileExists2 = true;
125
+ try {
126
+ raw = await fsp5.readFile(configPath2, "utf8");
127
+ } catch (err) {
128
+ if (err.code !== "ENOENT") {
129
+ throw new Error(`Refusing to mutate ${configPath2}: ${err.message}`, {
130
+ cause: err
131
+ });
132
+ }
133
+ fileExists2 = false;
134
+ raw = "{}";
135
+ }
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(raw);
139
+ } catch (err) {
140
+ if (fileExists2) {
141
+ throw new Error(
142
+ `Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
143
+ { cause: err }
144
+ );
145
+ }
146
+ parsed = {};
147
+ }
148
+ const decrypted = decryptConfigSecrets(parsed, vault);
149
+ const providers = decrypted.providers ?? {};
150
+ mutator(providers);
151
+ decrypted.providers = providers;
152
+ const encrypted = encryptConfigSecrets(decrypted, vault);
153
+ await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
154
+ }
155
+ var init_provider_config_utils = __esm({
156
+ "src/provider-config-utils.ts"() {
157
+ }
158
+ });
56
159
  function parseSubcommand(args) {
57
160
  const parts = args.trim().split(/\s+/);
58
161
  return { cmd: (parts[0] ?? "").toLowerCase(), rest: parts.slice(1) };
@@ -524,108 +627,6 @@ var init_helpers = __esm({
524
627
  ENTRY_BASENAMES = /* @__PURE__ */ new Set(["main", "index", "app", "cli", "server", "__main__"]);
525
628
  }
526
629
  });
527
- function normalizeKeys(cfg) {
528
- if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
529
- return cfg.apiKeys.map((k) => ({ ...k }));
530
- }
531
- if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
532
- return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
533
- }
534
- return [];
535
- }
536
- function writeKeysBack(cfg, keys) {
537
- if (keys.length === 0) {
538
- delete cfg.apiKeys;
539
- delete cfg.apiKey;
540
- delete cfg.activeKey;
541
- return;
542
- }
543
- cfg.apiKeys = keys;
544
- const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
545
- delete cfg.apiKey;
546
- if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
547
- cfg.activeKey = active.label;
548
- }
549
- }
550
- function resolveActiveApiKey(cfg) {
551
- if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
552
- const active = cfg.activeKey ? cfg.apiKeys.find((k) => k.label === cfg.activeKey) : void 0;
553
- return (active ?? cfg.apiKeys[0])?.apiKey;
554
- }
555
- return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
556
- }
557
- function activeLabel(cfg, keys) {
558
- if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
559
- return keys[0]?.label;
560
- }
561
- function maskedKey(key) {
562
- if (!key) return color.dim("\u2014");
563
- if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
564
- const head = key.slice(0, 4);
565
- const tail = key.slice(-4);
566
- return `${color.dim(head + "\u2026")}${tail}`;
567
- }
568
- function nowIso() {
569
- return (/* @__PURE__ */ new Date()).toISOString();
570
- }
571
- async function loadConfigProviders(configPath2, vault, opts) {
572
- const warn = opts?.warn;
573
- let raw;
574
- try {
575
- raw = await fsp5.readFile(configPath2, "utf8");
576
- } catch (err) {
577
- if (err.code !== "ENOENT") {
578
- warn?.(`Could not read ${configPath2}: ${err.message}. Treating as empty.`);
579
- }
580
- return {};
581
- }
582
- let parsed;
583
- try {
584
- parsed = JSON.parse(raw);
585
- } catch (err) {
586
- warn?.(`Config at ${configPath2} is not valid JSON: ${err.message}`);
587
- return {};
588
- }
589
- const decrypted = decryptConfigSecrets$1(parsed, vault);
590
- return decrypted.providers ?? {};
591
- }
592
- async function mutateConfigProviders(configPath2, vault, mutator) {
593
- let raw;
594
- let fileExists2 = true;
595
- try {
596
- raw = await fsp5.readFile(configPath2, "utf8");
597
- } catch (err) {
598
- if (err.code !== "ENOENT") {
599
- throw new Error(`Refusing to mutate ${configPath2}: ${err.message}`, {
600
- cause: err
601
- });
602
- }
603
- fileExists2 = false;
604
- raw = "{}";
605
- }
606
- let parsed;
607
- try {
608
- parsed = JSON.parse(raw);
609
- } catch (err) {
610
- if (fileExists2) {
611
- throw new Error(
612
- `Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
613
- { cause: err }
614
- );
615
- }
616
- parsed = {};
617
- }
618
- const decrypted = decryptConfigSecrets$1(parsed, vault);
619
- const providers = decrypted.providers ?? {};
620
- mutator(providers);
621
- decrypted.providers = providers;
622
- const encrypted = encryptConfigSecrets$1(decrypted, vault);
623
- await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
624
- }
625
- var init_provider_config_utils = __esm({
626
- "src/provider-config-utils.ts"() {
627
- }
628
- });
629
630
  function createApi(ownerName, base) {
630
631
  return new DefaultPluginAPI({ ownerName, ...base });
631
632
  }
@@ -654,26 +655,26 @@ function fmtDuration(ms) {
654
655
  const remMin = m - h * 60;
655
656
  return `${h}h${remMin}m`;
656
657
  }
657
- function fmtTaskResultLine(r, color74) {
658
+ function fmtTaskResultLine(r, color77) {
658
659
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
659
660
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
660
661
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
661
662
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
662
- const errKindChip = errKind ? color74.dim(` [${errKind}]`) : "";
663
- const errSnip = errMsg || errKind ? `${errKindChip}${color74.dim(errTail)}` : "";
663
+ const errKindChip = errKind ? color77.dim(` [${errKind}]`) : "";
664
+ const errSnip = errMsg || errKind ? `${errKindChip}${color77.dim(errTail)}` : "";
664
665
  switch (r.status) {
665
666
  case "success":
666
- return { mark: color74.green("\u2713"), stats, tail: "" };
667
+ return { mark: color77.green("\u2713"), stats, tail: "" };
667
668
  case "timeout":
668
669
  return {
669
- mark: color74.yellow("\u23F1"),
670
- stats: `${color74.yellow("timeout")} ${stats}`,
670
+ mark: color77.yellow("\u23F1"),
671
+ stats: `${color77.yellow("timeout")} ${stats}`,
671
672
  tail: errSnip
672
673
  };
673
674
  case "stopped":
674
- return { mark: color74.dim("\u2298"), stats: `${color74.dim("stopped")} ${stats}`, tail: errSnip };
675
+ return { mark: color77.dim("\u2298"), stats: `${color77.dim("stopped")} ${stats}`, tail: errSnip };
675
676
  case "failed":
676
- return { mark: color74.red("\u2717"), stats: `${color74.red("failed")} ${stats}`, tail: errSnip };
677
+ return { mark: color77.red("\u2717"), stats: `${color77.red("failed")} ${stats}`, tail: errSnip };
677
678
  }
678
679
  }
679
680
  var init_utils = __esm({
@@ -3028,6 +3029,23 @@ var init_update_check = __esm({
3028
3029
  CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
3029
3030
  }
3030
3031
  });
3032
+
3033
+ // src/webui-server/cost-helpers.ts
3034
+ function getCostRates(model) {
3035
+ const cost = model?.cost;
3036
+ return {
3037
+ input: cost?.input ?? 0,
3038
+ output: cost?.output ?? 0,
3039
+ cacheRead: cost?.cache_read ?? 0
3040
+ };
3041
+ }
3042
+ function computeUsageCost(usage, rates) {
3043
+ return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
3044
+ }
3045
+ var init_cost_helpers = __esm({
3046
+ "src/webui-server/cost-helpers.ts"() {
3047
+ }
3048
+ });
3031
3049
  function registerWebuiInstance(p, deps = {}) {
3032
3050
  const register = deps.registerFn ?? registerInstance;
3033
3051
  void register(
@@ -3047,7 +3065,7 @@ function registerWebuiInstance(p, deps = {}) {
3047
3065
  }
3048
3066
  function announceWebuiReady(p) {
3049
3067
  const log = p.log ?? ((m) => console.log(m));
3050
- const launch = p.openBrowserFn ?? openBrowser;
3068
+ const launch = p.openBrowserFn ?? openBrowser$1;
3051
3069
  const openUrl = p.wsToken ? `http://${p.host}:${p.httpPort}?token=${encodeURIComponent(p.wsToken)}` : `http://${p.host}:${p.httpPort}`;
3052
3070
  p.server.on("listening", () => {
3053
3071
  log(
@@ -3100,6 +3118,40 @@ var init_lifecycle = __esm({
3100
3118
  "src/webui-server/lifecycle.ts"() {
3101
3119
  }
3102
3120
  });
3121
+
3122
+ // src/webui-server/logger-shim.ts
3123
+ var structuredLine, consoleLogger;
3124
+ var init_logger_shim = __esm({
3125
+ "src/webui-server/logger-shim.ts"() {
3126
+ structuredLine = (level, message) => JSON.stringify({
3127
+ level,
3128
+ event: "webui.autophase",
3129
+ message,
3130
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3131
+ });
3132
+ consoleLogger = {
3133
+ level: "debug",
3134
+ error(msg, _ctx) {
3135
+ console.error(structuredLine("error", msg));
3136
+ },
3137
+ warn(msg, _ctx) {
3138
+ console.warn(structuredLine("warn", msg));
3139
+ },
3140
+ info(msg, _ctx) {
3141
+ console.log(structuredLine("info", msg));
3142
+ },
3143
+ debug(msg, _ctx) {
3144
+ console.debug(structuredLine("debug", msg));
3145
+ },
3146
+ trace(msg, _ctx) {
3147
+ console.debug(structuredLine("trace", msg));
3148
+ },
3149
+ child(_bindings) {
3150
+ return this;
3151
+ }
3152
+ };
3153
+ }
3154
+ });
3103
3155
  function getVault(globalConfigPath) {
3104
3156
  const keyFile = path4.join(path4.dirname(globalConfigPath ?? ""), ".key");
3105
3157
  return new DefaultSecretVault({ keyFile });
@@ -3618,23 +3670,6 @@ var init_context = __esm({
3618
3670
  init_context_breakdown();
3619
3671
  }
3620
3672
  });
3621
-
3622
- // src/webui-server/cost-helpers.ts
3623
- function getCostRates(model) {
3624
- const cost = model?.cost;
3625
- return {
3626
- input: cost?.input ?? 0,
3627
- output: cost?.output ?? 0,
3628
- cacheRead: cost?.cache_read ?? 0
3629
- };
3630
- }
3631
- function computeUsageCost(usage, rates) {
3632
- return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
3633
- }
3634
- var init_cost_helpers = __esm({
3635
- "src/webui-server/cost-helpers.ts"() {
3636
- }
3637
- });
3638
3673
  async function handleSkillsList(ctx, ws) {
3639
3674
  if (!ctx.skillLoader) {
3640
3675
  ctx.send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
@@ -4046,29 +4081,15 @@ async function handleProviderModels(ctx, ws, providerId) {
4046
4081
  return;
4047
4082
  }
4048
4083
  try {
4049
- const provider = await ctx.modelsRegistry.getProvider(providerId);
4050
- if (!provider) {
4051
- sendResult7(ctx, ws, false, `Provider "${providerId}" not found in catalog`);
4052
- return;
4053
- }
4084
+ const saved = await ctx.providerStore.load();
4085
+ const cfg = saved[providerId];
4086
+ const catalogId = cfg?.type && cfg.type !== providerId ? cfg.type : providerId;
4087
+ const provider = await ctx.modelsRegistry.getProvider(catalogId);
4054
4088
  ctx.send(ws, {
4055
4089
  type: "provider.models",
4056
4090
  payload: {
4057
4091
  provider: providerId,
4058
- models: provider.models.map((m) => ({
4059
- id: m.id,
4060
- name: m.name,
4061
- releaseDate: m.release_date,
4062
- contextWindow: m.limit?.context,
4063
- inputCost: m.cost?.input,
4064
- outputCost: m.cost?.output,
4065
- capabilities: [
4066
- ...m.tool_call ? ["tools"] : [],
4067
- ...m.reasoning ? ["reasoning"] : [],
4068
- ...m.modalities?.input?.includes("image") ? ["vision"] : [],
4069
- ...m.open_weights ? ["open_weights"] : []
4070
- ]
4071
- }))
4092
+ models: resolveProviderModelList(cfg?.models, provider)
4072
4093
  }
4073
4094
  });
4074
4095
  } catch (err) {
@@ -4665,40 +4686,6 @@ var init_ws_handlers = __esm({
4665
4686
  }
4666
4687
  });
4667
4688
 
4668
- // src/webui-server/logger-shim.ts
4669
- var structuredLine, consoleLogger;
4670
- var init_logger_shim = __esm({
4671
- "src/webui-server/logger-shim.ts"() {
4672
- structuredLine = (level, message) => JSON.stringify({
4673
- level,
4674
- event: "webui.autophase",
4675
- message,
4676
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4677
- });
4678
- consoleLogger = {
4679
- level: "debug",
4680
- error(msg, _ctx) {
4681
- console.error(structuredLine("error", msg));
4682
- },
4683
- warn(msg, _ctx) {
4684
- console.warn(structuredLine("warn", msg));
4685
- },
4686
- info(msg, _ctx) {
4687
- console.log(structuredLine("info", msg));
4688
- },
4689
- debug(msg, _ctx) {
4690
- console.debug(structuredLine("debug", msg));
4691
- },
4692
- trace(msg, _ctx) {
4693
- console.debug(structuredLine("trace", msg));
4694
- },
4695
- child(_bindings) {
4696
- return this;
4697
- }
4698
- };
4699
- }
4700
- });
4701
-
4702
4689
  // src/webui-server.ts
4703
4690
  var webui_server_exports = {};
4704
4691
  __export(webui_server_exports, {
@@ -4835,7 +4822,7 @@ async function runWebUI(opts) {
4835
4822
  const vault = new DefaultSecretVault({
4836
4823
  keyFile: path4.join(path4.dirname(configPath2), ".key")
4837
4824
  });
4838
- const decrypted = decryptConfigSecrets$1(parsed, vault);
4825
+ const decrypted = decryptConfigSecrets(parsed, vault);
4839
4826
  const autonomyCfg = decrypted.autonomy ?? {};
4840
4827
  let autonomyTouched = false;
4841
4828
  const setAutonomy = (key, val) => {
@@ -4910,13 +4897,16 @@ async function runWebUI(opts) {
4910
4897
  if (tgTouched) {
4911
4898
  const ext = decrypted.extensions ?? {};
4912
4899
  const tg = ext["telegram"] ?? {};
4913
- if (typeof payload["tgSessionEnd"] === "boolean") tg["notifyOnSessionEnd"] = payload["tgSessionEnd"];
4914
- if (typeof payload["tgDelegate"] === "boolean") tg["notifyOnDelegate"] = payload["tgDelegate"];
4915
- if (typeof payload["tgLongToolMs"] === "number") tg["longToolThresholdMs"] = payload["tgLongToolMs"];
4900
+ if (typeof payload["tgSessionEnd"] === "boolean")
4901
+ tg["notifyOnSessionEnd"] = payload["tgSessionEnd"];
4902
+ if (typeof payload["tgDelegate"] === "boolean")
4903
+ tg["notifyOnDelegate"] = payload["tgDelegate"];
4904
+ if (typeof payload["tgLongToolMs"] === "number")
4905
+ tg["longToolThresholdMs"] = payload["tgLongToolMs"];
4916
4906
  ext["telegram"] = tg;
4917
4907
  decrypted.extensions = ext;
4918
4908
  }
4919
- const encrypted = encryptConfigSecrets$1(decrypted, vault);
4909
+ const encrypted = encryptConfigSecrets(decrypted, vault);
4920
4910
  await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
4921
4911
  };
4922
4912
  const next = prefWriteLock.then(write);
@@ -5615,7 +5605,13 @@ async function runWebUI(opts) {
5615
5605
  case "key.add":
5616
5606
  case "key.update": {
5617
5607
  const m = msg;
5618
- await handleKeyUpsert(wsHandlerCtx, ws, m.payload.providerId, m.payload.label, m.payload.apiKey);
5608
+ await handleKeyUpsert(
5609
+ wsHandlerCtx,
5610
+ ws,
5611
+ m.payload.providerId,
5612
+ m.payload.label,
5613
+ m.payload.apiKey
5614
+ );
5619
5615
  break;
5620
5616
  }
5621
5617
  case "key.delete": {
@@ -5819,52 +5815,39 @@ async function runWebUI(opts) {
5819
5815
  }
5820
5816
  return handleMemoryForget(ws, msg, opts.memoryStore);
5821
5817
  }
5822
- // ── MCP operations — MCP servers are read directly from config file ──
5823
- case "mcp.list": {
5824
- const servers = [];
5825
- if (opts.globalConfigPath) {
5826
- try {
5827
- const raw = await fsp5.readFile(opts.globalConfigPath, "utf8");
5828
- const cfg = JSON.parse(raw);
5829
- const mcpServers = cfg.mcpServers;
5830
- if (mcpServers) {
5831
- for (const [name, serverCfg] of Object.entries(mcpServers)) {
5832
- servers.push({
5833
- name,
5834
- transport: serverCfg.transport ?? "stdio",
5835
- status: "stopped",
5836
- enabled: serverCfg.enabled ?? true,
5837
- // Conditional spreads keep these absent (not explicitly
5838
- // `undefined`) to satisfy exactOptionalPropertyTypes.
5839
- ...serverCfg.description !== void 0 && { description: serverCfg.description },
5840
- ...serverCfg.allowedTools !== void 0 && { tools: serverCfg.allowedTools }
5841
- });
5842
- }
5843
- }
5844
- } catch {
5845
- }
5846
- }
5847
- send(ws, { type: "mcp.list", payload: { servers } });
5818
+ // ── MCP operations — delegated to the shared handlers in
5819
+ // @wrongstack/webui/server, which run against the same on-disk config
5820
+ // and the live MCPRegistry the agent loop + `/mcp` use. ──
5821
+ case "mcp.list":
5822
+ await handleMcpList(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5848
5823
  break;
5849
- }
5850
5824
  case "mcp.add":
5825
+ await handleMcpAdd(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5826
+ break;
5851
5827
  case "mcp.remove":
5828
+ await handleMcpRemove(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5829
+ break;
5852
5830
  case "mcp.update":
5831
+ await handleMcpUpdate(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5832
+ break;
5853
5833
  case "mcp.wake":
5834
+ await handleMcpWake(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5835
+ break;
5854
5836
  case "mcp.sleep":
5837
+ await handleMcpSleep(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5838
+ break;
5855
5839
  case "mcp.discover":
5840
+ await handleMcpDiscover(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5841
+ break;
5856
5842
  case "mcp.enable":
5843
+ await handleMcpEnable(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5844
+ break;
5857
5845
  case "mcp.disable":
5858
- case "mcp.restart": {
5859
- send(ws, {
5860
- type: "mcp.operation_result",
5861
- payload: {
5862
- success: false,
5863
- message: 'MCP management operations require the standalone WebUI server. Please run "wrongstack webui" instead of "wrongstack --webui".'
5864
- }
5865
- });
5846
+ await handleMcpDisable(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5847
+ break;
5848
+ case "mcp.restart":
5849
+ await handleMcpRestart(ws, msg, opts.globalConfigPath ?? "", opts.mcpRegistry);
5866
5850
  break;
5867
- }
5868
5851
  case "skills.list": {
5869
5852
  await handleSkillsList(introspectionCtx, ws);
5870
5853
  break;
@@ -6050,6 +6033,17 @@ async function runWebUI(opts) {
6050
6033
  );
6051
6034
  break;
6052
6035
  }
6036
+ case "git.changes": {
6037
+ const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6038
+ await handleGitChanges(ws, projectRoot);
6039
+ break;
6040
+ }
6041
+ case "git.diff": {
6042
+ const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6043
+ const filePath = msg.payload?.path ?? "";
6044
+ await handleGitDiff(ws, projectRoot, filePath);
6045
+ break;
6046
+ }
6053
6047
  case "shell.open": {
6054
6048
  const result = await handleShellOpen(
6055
6049
  msg.payload,
@@ -6059,7 +6053,11 @@ async function runWebUI(opts) {
6059
6053
  break;
6060
6054
  }
6061
6055
  case "model.refine": {
6062
- await handleModelRefine(agentConfigCtx, ws, msg.payload.text);
6056
+ await handleModelRefine(
6057
+ agentConfigCtx,
6058
+ ws,
6059
+ msg.payload.text
6060
+ );
6063
6061
  break;
6064
6062
  }
6065
6063
  case "webui.shutdown":
@@ -6246,14 +6244,17 @@ async function runWebUI(opts) {
6246
6244
  }
6247
6245
  var init_webui_server = __esm({
6248
6246
  "src/webui-server.ts"() {
6247
+ init_cost_helpers();
6249
6248
  init_lifecycle();
6249
+ init_logger_shim();
6250
6250
  init_provider_config();
6251
6251
  init_static_serve();
6252
6252
  init_ws_handlers();
6253
- init_logger_shim();
6254
- init_cost_helpers();
6255
6253
  }
6256
6254
  });
6255
+
6256
+ // src/cli-main.ts
6257
+ init_provider_config_utils();
6257
6258
  var WORKTREE_PHASE_CONCURRENCY = 4;
6258
6259
  var MAX_CMD_OUTPUT = 2e5;
6259
6260
  function gitText(args, cwd) {
@@ -7157,12 +7158,12 @@ function pickGroupIndex(opts) {
7157
7158
  try {
7158
7159
  let current = 0;
7159
7160
  try {
7160
- const parsed = Number.parseInt(fs2.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7161
+ const parsed = Number.parseInt(fs3.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7161
7162
  if (Number.isFinite(parsed)) current = wrap(parsed);
7162
7163
  } catch {
7163
7164
  }
7164
- fs2.mkdirSync(path4.dirname(opts.cursorFile), { recursive: true });
7165
- fs2.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7165
+ fs3.mkdirSync(path4.dirname(opts.cursorFile), { recursive: true });
7166
+ fs3.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7166
7167
  return current;
7167
7168
  } catch {
7168
7169
  }
@@ -7649,7 +7650,19 @@ ${color.bold(theme.primary("WrongStack") + color.dim(" \u2014 Provider & Model S
7649
7650
  families.set(p.family, list);
7650
7651
  }
7651
7652
  const ordered = [];
7652
- const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
7653
+ const preferredOrder = [
7654
+ "anthropic",
7655
+ "anthropic-oauth",
7656
+ "openai",
7657
+ "openai-codex",
7658
+ "github-copilot",
7659
+ "google",
7660
+ "openai-compatible"
7661
+ ];
7662
+ const familyOrder = [
7663
+ ...preferredOrder.filter((f) => families.has(f)),
7664
+ ...[...families.keys()].filter((f) => !preferredOrder.includes(f))
7665
+ ];
7653
7666
  let idx = 1;
7654
7667
  let defaultIdx;
7655
7668
  renderer.write("\n");
@@ -9742,7 +9755,7 @@ async function persistAutonomySetting(deps, mutator) {
9742
9755
  }
9743
9756
  parsed = {};
9744
9757
  }
9745
- const decrypted = decryptConfigSecrets(parsed, deps.vault);
9758
+ const decrypted = decryptConfigSecrets$1(parsed, deps.vault);
9746
9759
  const autonomy = decrypted.autonomy ?? {};
9747
9760
  mutator(
9748
9761
  autonomy
@@ -9754,7 +9767,7 @@ async function persistAutonomySetting(deps, mutator) {
9754
9767
  await ensureProjectDir(actualTarget);
9755
9768
  }
9756
9769
  const toWrite = actualTarget === deps.globalConfigPath ? decrypted : filterSafeForProject(decrypted);
9757
- const encrypted = encryptConfigSecrets(toWrite, deps.vault);
9770
+ const encrypted = encryptConfigSecrets$1(toWrite, deps.vault);
9758
9771
  await atomicWrite(actualTarget, JSON.stringify(encrypted, null, 2), { mode: 384 });
9759
9772
  deps.configStore.update({
9760
9773
  autonomy: decrypted.autonomy
@@ -9793,7 +9806,7 @@ async function persistConfigSetting(deps, mutator) {
9793
9806
  }
9794
9807
  parsed = {};
9795
9808
  }
9796
- const decrypted = decryptConfigSecrets(parsed, deps.vault);
9809
+ const decrypted = decryptConfigSecrets$1(parsed, deps.vault);
9797
9810
  mutator(decrypted);
9798
9811
  const newScope = decrypted.configScope;
9799
9812
  const actualTarget = newScope === "project" && deps.inProjectConfigPath ? deps.inProjectConfigPath : newScope === "global" ? deps.globalConfigPath : targetPath;
@@ -9801,7 +9814,7 @@ async function persistConfigSetting(deps, mutator) {
9801
9814
  await ensureProjectDir(actualTarget);
9802
9815
  }
9803
9816
  const toWrite = actualTarget === deps.globalConfigPath ? decrypted : filterSafeForProject(decrypted);
9804
- const encrypted = encryptConfigSecrets(toWrite, deps.vault);
9817
+ const encrypted = encryptConfigSecrets$1(toWrite, deps.vault);
9805
9818
  await atomicWrite(actualTarget, JSON.stringify(encrypted, null, 2), { mode: 384 });
9806
9819
  deps.configStore.update(decrypted);
9807
9820
  }
@@ -9828,13 +9841,13 @@ async function persistTelegramConfig(deps, mutator) {
9828
9841
  }
9829
9842
  parsed = {};
9830
9843
  }
9831
- const decrypted = decryptConfigSecrets(parsed, deps.vault);
9844
+ const decrypted = decryptConfigSecrets$1(parsed, deps.vault);
9832
9845
  const extensions = decrypted.extensions ?? {};
9833
9846
  const telegram = extensions.telegram ?? {};
9834
9847
  mutator(telegram);
9835
9848
  extensions.telegram = telegram;
9836
9849
  decrypted.extensions = extensions;
9837
- const encrypted = encryptConfigSecrets(decrypted, deps.vault);
9850
+ const encrypted = encryptConfigSecrets$1(decrypted, deps.vault);
9838
9851
  await atomicWrite(deps.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
9839
9852
  deps.configStore.update({
9840
9853
  extensions
@@ -10300,9 +10313,9 @@ async function patchGlobalConfig(globalConfigPath, mutate) {
10300
10313
  throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
10301
10314
  parsed = {};
10302
10315
  }
10303
- const decrypted = decryptConfigSecrets(parsed, noOpVault);
10316
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
10304
10317
  mutate(decrypted);
10305
- const encrypted = encryptConfigSecrets(decrypted, noOpVault);
10318
+ const encrypted = encryptConfigSecrets$1(decrypted, noOpVault);
10306
10319
  await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
10307
10320
  return decrypted;
10308
10321
  }
@@ -11765,6 +11778,67 @@ function handleFleetHelp(opts) {
11765
11778
  return { message: msg };
11766
11779
  }
11767
11780
 
11781
+ // src/slash-commands/f-keys.ts
11782
+ var F_PANELS = {
11783
+ "1": { action: "projectPickerOpen", label: "project switcher" },
11784
+ "2": { action: "toggleMonitor", label: "fleet orchestration monitor" },
11785
+ "3": { action: "toggleAgentsMonitor", label: "agents live monitor" },
11786
+ "4": { action: "toggleWorktreeMonitor", label: "worktree monitor" },
11787
+ "5": { action: "togglePlanPanel", label: "autonomy settings" },
11788
+ "6": { action: "toggleTodosMonitor", label: "todos monitor overlay" },
11789
+ "7": { action: "toggleQueuePanel", label: "queue panel" },
11790
+ "8": { action: "toggleProcessList", label: "process list overlay" },
11791
+ "9": { action: "toggleGoalPanel", label: "goal panel" },
11792
+ "10": { action: "toggleSessionsPanel", label: "live sessions panel" },
11793
+ "11": { action: "toggleCoordinatorMonitor", label: "coordinator monitor" },
11794
+ "12": { action: "statuslineOpen", label: "status line picker" }
11795
+ };
11796
+ function buildFKeysCommand(opts) {
11797
+ return {
11798
+ name: "f",
11799
+ description: "Open F-key panels (F1\u2013F12). Type /f for numbered options.",
11800
+ hidden: false,
11801
+ async run(args) {
11802
+ const n = args.trim();
11803
+ if (!n) {
11804
+ const lines = ["F-key panels:"];
11805
+ for (const [num, { label }] of Object.entries(F_PANELS)) {
11806
+ lines.push(` /f ${num} \u2014 ${label}`);
11807
+ }
11808
+ lines.push("", "Or use /f1 \u2026 /f12 directly (hidden from the picker).");
11809
+ return { message: lines.join("\n") };
11810
+ }
11811
+ const entry = F_PANELS[n];
11812
+ if (!entry) {
11813
+ return { message: `Unknown F-key: ${n}. Use /f to list available panels (1\u201312).` };
11814
+ }
11815
+ if (opts.onPanelOpen.current) {
11816
+ const ok = opts.onPanelOpen.current(entry.action);
11817
+ if (ok) return {};
11818
+ }
11819
+ return { message: `Opening ${entry.label}\u2026 (REPL/headless mode \u2014 panel may not be available)` };
11820
+ }
11821
+ };
11822
+ }
11823
+ function buildFKeyAliasCommands(opts) {
11824
+ return Object.entries(F_PANELS).map(([num, { action, label }]) => {
11825
+ const cmd = {
11826
+ name: `f${num}`,
11827
+ description: `Open ${label} (same as F${num})`,
11828
+ hidden: true,
11829
+ // not shown in the main slash picker
11830
+ async run() {
11831
+ if (opts.onPanelOpen.current) {
11832
+ const ok = opts.onPanelOpen.current(action);
11833
+ if (ok) return {};
11834
+ }
11835
+ return { message: `Opening ${label}\u2026` };
11836
+ }
11837
+ };
11838
+ return cmd;
11839
+ });
11840
+ }
11841
+
11768
11842
  // src/slash-commands/goal-refiner.ts
11769
11843
  async function refineGoal(rawGoal, provider, model) {
11770
11844
  const prompt = buildRefinementPrompt(rawGoal);
@@ -13604,9 +13678,9 @@ async function patchGlobalConfig2(globalConfigPath, mutate) {
13604
13678
  }
13605
13679
  parsed = {};
13606
13680
  }
13607
- const decrypted = decryptConfigSecrets(parsed, noOpVault);
13681
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
13608
13682
  mutate(decrypted);
13609
- const encrypted = encryptConfigSecrets(decrypted, noOpVault);
13683
+ const encrypted = encryptConfigSecrets$1(decrypted, noOpVault);
13610
13684
  await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
13611
13685
  return decrypted;
13612
13686
  }
@@ -14541,9 +14615,9 @@ async function patchGlobalConfig3(globalConfigPath, mutate) {
14541
14615
  throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
14542
14616
  parsed = {};
14543
14617
  }
14544
- const decrypted = decryptConfigSecrets(parsed, noOpVault);
14618
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
14545
14619
  mutate(decrypted);
14546
- const encrypted = encryptConfigSecrets(decrypted, noOpVault);
14620
+ const encrypted = encryptConfigSecrets$1(decrypted, noOpVault);
14547
14621
  await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
14548
14622
  return decrypted;
14549
14623
  }
@@ -15708,6 +15782,12 @@ function buildSettingsCommand(opts) {
15708
15782
  " /settings semver-part patch|minor|major|auto Default part for /semver and the semver_bump tool",
15709
15783
  " /settings breaker on|off Enable/disable the process circuit breaker (gates bash/exec)",
15710
15784
  " /settings breaker-timeout <seconds> Auto kill/reset delay when the breaker trips (0 = manual)",
15785
+ " /settings context-mode balanced|frugal|deep|archival Context window policy",
15786
+ " /settings context-strategy hybrid|intelligent|selective Compactor strategy",
15787
+ " /settings context-auto-compact on|off Auto-compact context when thresholds crossed",
15788
+ " /settings token-saving off|minimal|light|medium|aggressive Token-saving mode",
15789
+ " /settings max-concurrent <n> Max concurrent subagents (0 = unlimited)",
15790
+ " /settings title-animation on|off Terminal title animation",
15711
15791
  " /settings defaults Show built-in default values",
15712
15792
  "",
15713
15793
  "Settings are persisted to ~/.wrongstack/config.json."
@@ -15727,6 +15807,13 @@ function buildSettingsCommand(opts) {
15727
15807
  const cb = opts.configStore.get().circuitBreaker;
15728
15808
  const breakerEnabled = cb?.enabled === true;
15729
15809
  const breakerTimeout = cb?.autoKillResetMs ?? 6e4;
15810
+ const context = opts.configStore.get().context;
15811
+ const contextMode = context?.mode ?? "balanced";
15812
+ const contextStrategy = context?.strategy ?? "hybrid";
15813
+ const contextAutoCompact = context?.autoCompact !== false;
15814
+ const features = opts.configStore.get().features;
15815
+ const tokenSavingTier = features?.tokenSavingMode ?? "off";
15816
+ const maxConcurrent = opts.configStore.get().maxConcurrent ?? 0;
15730
15817
  return [
15731
15818
  `${color.bold("WrongStack")} ${color.dim("\u2014 Settings")}`,
15732
15819
  "",
@@ -15741,6 +15828,11 @@ function buildSettingsCommand(opts) {
15741
15828
  ` refine-language: ${color.cyan(enhanceLanguage)} ${color.dim("change: /settings refine-language original|english")}`,
15742
15829
  ` semver default part: ${color.cyan(semverPart)} ${color.dim("change: /settings semver-part patch|minor|major|auto")}`,
15743
15830
  ` circuit breaker: ${breakerEnabled ? color.cyan("on") : color.dim("off")} (kill/reset ${breakerTimeout > 0 ? formatDelay(breakerTimeout) : color.dim("manual")}) ${color.dim("change: /settings breaker on|off")}`,
15831
+ ` context mode: ${color.cyan(contextMode)} ${color.dim("change: /settings context-mode balanced|frugal|deep|archival")}`,
15832
+ ` context strategy: ${color.cyan(contextStrategy)} ${color.dim("change: /settings context-strategy hybrid|intelligent|selective")}`,
15833
+ ` context auto-compact: ${contextAutoCompact ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings context-auto-compact on|off")}`,
15834
+ ` token-saving: ${color.cyan(tokenSavingTier)} ${color.dim("change: /settings token-saving off|minimal|light|medium|aggressive")}`,
15835
+ ` max-concurrent: ${color.cyan(maxConcurrent === 0 ? "unlimited" : String(maxConcurrent))} ${color.dim("change: /settings max-concurrent <n>")}`,
15744
15836
  "",
15745
15837
  color.dim(" Persisted to ~/.wrongstack/config.json \xB7 /settings help for more")
15746
15838
  ].join("\n");
@@ -15748,7 +15840,7 @@ function buildSettingsCommand(opts) {
15748
15840
  return {
15749
15841
  name: "settings",
15750
15842
  category: "Config",
15751
- description: "View or change settings (auto-proceed delay, default autonomy mode, launch hints).",
15843
+ description: "View or change settings (auto-proceed, autonomy, context, features, token-saving).",
15752
15844
  help,
15753
15845
  async run(args) {
15754
15846
  const { cmd, rest } = parseSubcommand(args);
@@ -15970,8 +16062,101 @@ function buildSettingsCommand(opts) {
15970
16062
  message: `${color.green("\u2713")} breaker kill/reset timeout \u2192 ${ms > 0 ? formatDelay(ms) : color.dim("manual")} ${color.dim(ms > 0 ? "statusline shows a countdown when the breaker trips" : "breaker trips require /kill reset")}`
15971
16063
  };
15972
16064
  }
16065
+ if (sub === "context-mode") {
16066
+ const raw = (rest[0] ?? "").toLowerCase();
16067
+ const modes = ["balanced", "frugal", "deep", "archival"];
16068
+ if (!modes.includes(raw)) {
16069
+ return { message: `${color.amber("Usage:")} /settings context-mode balanced|frugal|deep|archival` };
16070
+ }
16071
+ await persistConfigSetting(persistDeps, (cfg) => {
16072
+ const ctx = cfg.context ?? {};
16073
+ ctx.mode = raw;
16074
+ cfg.context = ctx;
16075
+ });
16076
+ return {
16077
+ message: `${color.green("\u2713")} context mode \u2192 ${color.cyan(raw)} ${color.dim("context window policy")}`
16078
+ };
16079
+ }
16080
+ if (sub === "context-strategy") {
16081
+ const raw = (rest[0] ?? "").toLowerCase();
16082
+ const strategies = ["hybrid", "intelligent", "selective"];
16083
+ if (!strategies.includes(raw)) {
16084
+ return { message: `${color.amber("Usage:")} /settings context-strategy hybrid|intelligent|selective` };
16085
+ }
16086
+ await persistConfigSetting(persistDeps, (cfg) => {
16087
+ const ctx = cfg.context ?? {};
16088
+ ctx.strategy = raw;
16089
+ cfg.context = ctx;
16090
+ });
16091
+ return {
16092
+ message: `${color.green("\u2713")} context strategy \u2192 ${color.cyan(raw)} ${color.dim("compactor strategy")}`
16093
+ };
16094
+ }
16095
+ if (sub === "context-auto-compact") {
16096
+ const raw = (rest[0] ?? "").toLowerCase();
16097
+ if (!["on", "off"].includes(raw)) {
16098
+ return { message: `${color.amber("Usage:")} /settings context-auto-compact on|off` };
16099
+ }
16100
+ const on = raw === "on";
16101
+ await persistConfigSetting(persistDeps, (cfg) => {
16102
+ const ctx = cfg.context ?? {};
16103
+ ctx.autoCompact = on;
16104
+ cfg.context = ctx;
16105
+ });
16106
+ return {
16107
+ message: `${color.green("\u2713")} context auto-compact \u2192 ${on ? color.cyan("on") : color.dim("off")} ${color.dim("auto-compact context when thresholds crossed")}`
16108
+ };
16109
+ }
16110
+ if (sub === "token-saving") {
16111
+ const raw = (rest[0] ?? "").toLowerCase();
16112
+ const tiers = ["off", "minimal", "light", "medium", "aggressive"];
16113
+ if (!tiers.includes(raw)) {
16114
+ return { message: `${color.amber("Usage:")} /settings token-saving off|minimal|light|medium|aggressive` };
16115
+ }
16116
+ await persistConfigSetting(persistDeps, (cfg) => {
16117
+ const feat = cfg.features ?? {};
16118
+ feat.tokenSavingMode = raw;
16119
+ cfg.features = feat;
16120
+ });
16121
+ return {
16122
+ message: `${color.green("\u2713")} token-saving \u2192 ${color.cyan(raw)} ${color.dim("token-saving mode")}`
16123
+ };
16124
+ }
16125
+ if (sub === "max-concurrent") {
16126
+ const raw = rest[0];
16127
+ if (raw === void 0) {
16128
+ return {
16129
+ message: `${color.amber("Usage:")} /settings max-concurrent <n> ${color.dim("(0 = unlimited)")}`
16130
+ };
16131
+ }
16132
+ const n = Number.parseInt(raw, 10);
16133
+ if (Number.isNaN(n) || n < 0) {
16134
+ return {
16135
+ message: `${color.red("Invalid number")}: "${raw}". Enter a non-negative integer (0 = unlimited)`
16136
+ };
16137
+ }
16138
+ await persistConfigSetting(persistDeps, (cfg) => {
16139
+ cfg.maxConcurrent = n;
16140
+ });
16141
+ return {
16142
+ message: `${color.green("\u2713")} max-concurrent \u2192 ${color.cyan(n === 0 ? "unlimited" : String(n))} ${color.dim("max concurrent subagents")}`
16143
+ };
16144
+ }
16145
+ if (sub === "title-animation") {
16146
+ const raw = (rest[0] ?? "").toLowerCase();
16147
+ if (!["on", "off"].includes(raw)) {
16148
+ return { message: `${color.amber("Usage:")} /settings title-animation on|off` };
16149
+ }
16150
+ const on = raw === "on";
16151
+ await persistConfigSetting(persistDeps, (cfg) => {
16152
+ cfg.titleAnimation = on;
16153
+ });
16154
+ return {
16155
+ message: `${color.green("\u2713")} title animation \u2192 ${on ? color.cyan("on") : color.dim("off")} ${color.dim("terminal title animation")}`
16156
+ };
16157
+ }
15973
16158
  return {
15974
- 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", "defaults"], "settings")}`
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")}`
15975
16160
  };
15976
16161
  } catch (err) {
15977
16162
  return {
@@ -16609,8 +16794,10 @@ function buildTechStackTask(opts) {
16609
16794
  "1. **Read** each package.json and extract ALL dependencies (dependencies +",
16610
16795
  " devDependencies + peerDependencies). Include the workspace root.",
16611
16796
  "",
16612
- "2. **For every dependency**, look up its latest version from the npm registry:",
16613
- ' - `fetch("https://registry.npmjs.org/<package>/latest")`',
16797
+ "2. **For every dependency**, look up its latest version from the npm registry",
16798
+ " using the `fetch` tool (NOT shell `curl`/`wget`, and NOT a Node script \u2014",
16799
+ " you run under a director and only the `fetch` tool is permitted for network):",
16800
+ ' - Call the `fetch` tool with `url: "https://registry.npmjs.org/<package>/latest"`',
16614
16801
  " - Extract the `version` field from the JSON response",
16615
16802
  " - Also check `description`, `license`, and `time` fields for age/dead checks",
16616
16803
  "",
@@ -16625,7 +16812,7 @@ function buildTechStackTask(opts) {
16625
16812
  " - Flag dead packages (no release >2 years + critical issues)",
16626
16813
  " - Prefer Node.js built-ins over third-party packages",
16627
16814
  "",
16628
- `5. **Write the report** to \`${outputPath}\` in the project root:`,
16815
+ `5. **Write the report** to \`${outputPath}\` in the project root using the \`write\` tool:`,
16629
16816
  " - Markdown format: grouped by category, with version tables and warnings",
16630
16817
  " - JSON format: structured array with name, current, latest, status, notes",
16631
16818
  "",
@@ -16658,11 +16845,15 @@ function buildTechStackTask(opts) {
16658
16845
  "",
16659
16846
  "### Guardrails",
16660
16847
  "",
16661
- "- Use `fetch()` with `AbortSignal.timeout(10000)` on every npm registry call.",
16662
- "- Skip packages that return 404 (private/internal packages).",
16848
+ "- Network access is ONLY via the `fetch` tool. The `bash`, `exec`, and other",
16849
+ " shell tools are denied for this subagent \u2014 do NOT try `curl`, `wget`, or a",
16850
+ " Node/`fetch()` script as a fallback; they will be blocked. If a `fetch`",
16851
+ " call fails, retry the `fetch` tool, do not switch transports.",
16852
+ "- File writes are ONLY via the `write` tool (it is permitted for this run).",
16853
+ "- Skip packages whose `fetch` returns HTTP 404 (private/internal packages).",
16663
16854
  "- Deduplicate: same package in multiple package.json files = one row.",
16664
16855
  "- Do NOT modify any files except writing the report.",
16665
- "- Run in 2-3 iterations max. Parallel fetch where possible.",
16856
+ "- Run in 2-3 iterations max. Issue several `fetch` calls per turn where possible.",
16666
16857
  "- **IMPORTANT**: Output the chat summary FIRST, then write the file. I need to see results."
16667
16858
  ].join("\n");
16668
16859
  }
@@ -16717,6 +16908,15 @@ function buildTechStackCommand(opts) {
16717
16908
  opts.renderer.writeWarning(msg);
16718
16909
  return { message: msg };
16719
16910
  }
16911
+ const hasFetchTool = opts.toolRegistry.list().some((t) => t.name === "fetch");
16912
+ if (!hasFetchTool) {
16913
+ opts.renderer.writeWarning(
16914
+ "The `fetch` tool is not registered in this session \u2014 a token-saving tier (minimal/light) omits it. The techstack subagent cannot query the npm registry without it. Raise the tier to `medium` or higher (/settings \u2192 token saving) and re-run /techstack."
16915
+ );
16916
+ return {
16917
+ message: "techstack aborted: `fetch` tool unavailable in the current token-saving tier."
16918
+ };
16919
+ }
16720
16920
  const header = isInit ? "Tech Stack Init Audit" : "Tech Stack Audit";
16721
16921
  const label = `${color.cyan("\u{1F50D}")} ${color.bold(header)} ${color.dim(`(${packageFiles.length} package files)`)}`;
16722
16922
  opts.renderer.write(label);
@@ -16728,7 +16928,11 @@ function buildTechStackCommand(opts) {
16728
16928
  );
16729
16929
  try {
16730
16930
  const name = isInit ? "techstack-init" : "techstack-audit";
16731
- const summary = await opts.onSpawnAndWait(task, { name });
16931
+ const summary = await opts.onSpawnAndWait(task, {
16932
+ name,
16933
+ tools: ["read", "glob", "grep", "tree", "fetch", "write"],
16934
+ allowedCapabilities: ["fs.read", "net.outbound", "fs.write"]
16935
+ });
16732
16936
  return { message: summary };
16733
16937
  } catch (err) {
16734
16938
  const msg = `Tech stack scan failed: ${toErrorMessage(err)}`;
@@ -17346,6 +17550,8 @@ function buildBuiltinSlashCommands(opts) {
17346
17550
  buildAgentsCommand(opts),
17347
17551
  buildDirectorCommand(opts),
17348
17552
  buildFleetCommand(opts),
17553
+ buildFKeysCommand(opts),
17554
+ ...buildFKeyAliasCommands(opts),
17349
17555
  buildEnhanceCommand(opts),
17350
17556
  buildEnsembleCommand(),
17351
17557
  buildMemoryCommand(opts),
@@ -17387,6 +17593,8 @@ function buildBuiltinSlashCommands(opts) {
17387
17593
  }),
17388
17594
  getConfig: opts.statuslineConfig?.get ?? (async () => ({})),
17389
17595
  setConfig: opts.statuslineConfig?.set ?? (async () => {
17596
+ }),
17597
+ saveStatuslineHiddenItems: opts.saveStatuslineHiddenItems ?? (async () => {
17390
17598
  })
17391
17599
  })
17392
17600
  ];
@@ -17486,9 +17694,9 @@ async function runProjectCheck(opts) {
17486
17694
  }
17487
17695
  if (answer2 === "y" || answer2 === "yes") {
17488
17696
  try {
17489
- const { spawn: spawn6 } = await import('child_process');
17697
+ const { spawn: spawn9 } = await import('child_process');
17490
17698
  await new Promise((resolve11, reject) => {
17491
- const child = spawn6("git", ["init"], {
17699
+ const child = spawn9("git", ["init"], {
17492
17700
  cwd,
17493
17701
  signal: AbortSignal.timeout(1e4),
17494
17702
  windowsHide: true
@@ -18560,6 +18768,10 @@ ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
18560
18768
  `);
18561
18769
  renderer.write(` ${color.bold("c")} Add a custom provider
18562
18770
  `);
18771
+ renderer.write(
18772
+ ` ${color.bold("s")} Sign in with a subscription ${color.dim("(ChatGPT / Claude / Copilot)")}
18773
+ `
18774
+ );
18563
18775
  if (ids.length > 0) {
18564
18776
  renderer.write(
18565
18777
  ` ${color.dim("1-")}${color.dim(String(ids.length))} ${color.bold("Manage a provider")}
@@ -18798,123 +19010,961 @@ async function addKeyForCatalogProvider(deps, chosen) {
18798
19010
  return false;
18799
19011
  }
18800
19012
  }
18801
- return addKeyForProvider(alias, deps, {
18802
- type: chosen.id,
18803
- family,
18804
- baseUrl,
18805
- envVars: chosen.envVars
18806
- });
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
+ };
19203
+ }
19204
+ async function exchangeAuthorizationCode(code, state, verifier, signal) {
19205
+ const res = await fetch(TOKEN_URL, {
19206
+ method: "POST",
19207
+ headers: { "content-type": "application/json", accept: "application/json" },
19208
+ body: JSON.stringify({
19209
+ grant_type: "authorization_code",
19210
+ client_id: CLIENT_ID,
19211
+ code,
19212
+ state,
19213
+ redirect_uri: REDIRECT_URI,
19214
+ code_verifier: verifier
19215
+ }),
19216
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
19217
+ });
19218
+ return readTokens(res, "exchange");
19219
+ }
19220
+ async function fetchClaudeModels(accessToken, signal) {
19221
+ try {
19222
+ const res = await fetch(`${CLAUDE_BASE_URL}/v1/models?limit=100`, {
19223
+ headers: {
19224
+ accept: "application/json",
19225
+ authorization: `Bearer ${accessToken}`,
19226
+ "anthropic-version": "2023-06-01",
19227
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20"
19228
+ },
19229
+ signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
19230
+ });
19231
+ if (!res.ok) return [];
19232
+ const json = await res.json();
19233
+ const ids = (json?.data ?? []).map((m) => m.id).filter((id) => typeof id === "string" && id.startsWith("claude-"));
19234
+ return ids;
19235
+ } catch {
19236
+ return [];
19237
+ }
19238
+ }
19239
+ function callbackHtml(ok, message) {
19240
+ const heading = ok ? "Authentication successful" : "Authentication failed";
19241
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>${heading}</title><style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#09090b;color:#fafafa;font-family:ui-sans-serif,system-ui,sans-serif;text-align:center}h1{font-size:26px;margin:0 0 8px}p{color:#a1a1aa}</style></head><body><main><h1>${heading}</h1><p>${message}</p></main></body></html>`;
19242
+ }
19243
+ function startLoopbackServer(expectedState) {
19244
+ let resolveCode = () => {
19245
+ };
19246
+ const codePromise = new Promise((resolve11) => {
19247
+ let settled = false;
19248
+ resolveCode = (v) => {
19249
+ if (settled) return;
19250
+ settled = true;
19251
+ resolve11(v);
19252
+ };
19253
+ });
19254
+ const server = createServer((req2, res) => {
19255
+ let url;
19256
+ try {
19257
+ url = new URL(req2.url ?? "", `http://${REDIRECT_HOST}`);
19258
+ } catch {
19259
+ res.statusCode = 400;
19260
+ res.end();
19261
+ return;
19262
+ }
19263
+ if (url.pathname !== REDIRECT_PATH) {
19264
+ res.statusCode = 404;
19265
+ res.setHeader("content-type", "text/html; charset=utf-8");
19266
+ res.end(callbackHtml(false, "Callback route not found."));
19267
+ return;
19268
+ }
19269
+ res.setHeader("content-type", "text/html; charset=utf-8");
19270
+ const err = url.searchParams.get("error");
19271
+ if (err) {
19272
+ res.statusCode = 400;
19273
+ res.end(callbackHtml(false, `Authorization error: ${err}`));
19274
+ resolveCode(null);
19275
+ return;
19276
+ }
19277
+ const code = url.searchParams.get("code");
19278
+ const state = url.searchParams.get("state");
19279
+ if (!code || !state) {
19280
+ res.statusCode = 400;
19281
+ res.end(callbackHtml(false, "Missing code or state."));
19282
+ return;
19283
+ }
19284
+ if (state !== expectedState) {
19285
+ res.statusCode = 400;
19286
+ res.end(callbackHtml(false, "State mismatch \u2014 please restart the login."));
19287
+ resolveCode(null);
19288
+ return;
19289
+ }
19290
+ res.statusCode = 200;
19291
+ res.end(callbackHtml(true, "You can close this window and return to the terminal."));
19292
+ resolveCode({ code, state });
19293
+ });
19294
+ return new Promise((resolve11) => {
19295
+ server.on("error", () => {
19296
+ resolveCode(null);
19297
+ resolve11({
19298
+ bound: false,
19299
+ waitForCode: () => Promise.resolve(null),
19300
+ close: () => {
19301
+ try {
19302
+ server.close();
19303
+ } catch {
19304
+ }
19305
+ }
19306
+ });
19307
+ });
19308
+ server.listen(REDIRECT_PORT, REDIRECT_HOST, () => {
19309
+ resolve11({
19310
+ bound: true,
19311
+ waitForCode: () => codePromise,
19312
+ close: () => {
19313
+ resolveCode(null);
19314
+ try {
19315
+ server.close();
19316
+ } catch {
19317
+ }
19318
+ }
19319
+ });
19320
+ });
19321
+ });
19322
+ }
19323
+ function openBrowser(url) {
19324
+ try {
19325
+ const platform3 = process.platform;
19326
+ const { command, args } = platform3 === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : platform3 === "darwin" ? { command: "open", args: [url] } : { command: "xdg-open", args: [url] };
19327
+ const child = spawn(command, args, { stdio: "ignore", windowsHide: true });
19328
+ child.on("error", () => {
19329
+ });
19330
+ child.unref();
19331
+ } catch {
19332
+ }
19333
+ }
19334
+ async function runClaudeOAuthLogin(deps, opts = {}) {
19335
+ const providerId = opts.providerId ?? CLAUDE_PROVIDER_ID;
19336
+ const { verifier, challenge } = generatePkce();
19337
+ const state = verifier;
19338
+ const authorizeUrl = buildAuthorizeUrl(challenge, verifier);
19339
+ const ac = new AbortController();
19340
+ const onSig = () => ac.abort();
19341
+ process.on("SIGINT", onSig);
19342
+ const server = await startLoopbackServer(state);
19343
+ deps.renderer.write(
19344
+ color.bold(`
19345
+ Sign in with Claude \u2014 ${color.cyan(providerId)}
19346
+ `) + color.dim(" Uses your Claude Pro/Max subscription (not an API key).\n") + color.amber(" \u26A0 Using a subscription outside the official Claude Code client is against\n") + color.amber(" Anthropic\u2019s Terms \u2014 your account could be rate-limited or banned.\n") + color.dim(" Sanctioned programmatic use = an API key: ") + color.bold("wstack auth anthropic") + color.dim("\n\n") + color.bold(` ${"\u2500".repeat(56)}
19347
+ `) + color.bold(" Open this URL in your browser to sign in:\n") + color.cyan(` ${authorizeUrl}
19348
+ `) + color.bold(` ${"\u2500".repeat(56)}
19349
+
19350
+ `)
19351
+ );
19352
+ if (server.bound) {
19353
+ openBrowser(authorizeUrl);
19354
+ deps.renderer.write(
19355
+ color.dim(" A browser window should open. Waiting for you to finish signing in...\n") + color.dim(" (Listening on http://localhost:53692 \u2014 press Ctrl+C to cancel.)\n")
19356
+ );
19357
+ } else {
19358
+ deps.renderer.write(
19359
+ color.amber(" \u26A0 Could not start the local callback listener (port 53692 in use).\n") + color.dim(" After signing in, paste the full redirect URL (or the code) below.\n")
19360
+ );
19361
+ }
19362
+ let code;
19363
+ try {
19364
+ if (server.bound) {
19365
+ const got = await server.waitForCode();
19366
+ if (got) code = got.code;
19367
+ }
19368
+ if (!code) {
19369
+ const input = (await deps.reader.readLine(
19370
+ `
19371
+ ${color.amber("?")} Paste the redirect URL or code ${color.dim("(or q to cancel)")}: `
19372
+ )).trim();
19373
+ if (input.toLowerCase() === "q" || input === "") {
19374
+ deps.renderer.write(color.dim(" Cancelled.\n"));
19375
+ return 1;
19376
+ }
19377
+ const parsed = parseAuthorizationInput(input);
19378
+ if (parsed.state && parsed.state !== state) {
19379
+ deps.renderer.writeError(" State mismatch \u2014 please restart the login flow.");
19380
+ return 1;
19381
+ }
19382
+ code = parsed.code;
19383
+ }
19384
+ if (!code) {
19385
+ deps.renderer.writeError(" No authorization code received.");
19386
+ return 1;
19387
+ }
19388
+ deps.renderer.write(color.dim("\n Exchanging authorization code for tokens...\n"));
19389
+ const tokens = await exchangeAuthorizationCode(code, state, verifier, ac.signal);
19390
+ const models = await fetchClaudeModels(tokens.access, ac.signal);
19391
+ const saved = await saveClaudeTokens(deps, providerId, tokens, models);
19392
+ if (!saved) return 1;
19393
+ deps.renderer.write(color.green("\n \u2713 Signed in with Claude!\n"));
19394
+ const modelHint = models.find((m) => m.includes("sonnet")) ?? models[0] ?? "claude-sonnet-4-6";
19395
+ deps.renderer.writeInfo(
19396
+ ` Saved as provider ${color.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
19397
+ Use: ${color.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
19398
+ ` + color.dim(" Tokens refresh automatically before they expire.\n")
19399
+ );
19400
+ return 0;
19401
+ } catch (err) {
19402
+ const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
19403
+ deps.renderer.writeError(` Login failed: ${msg}`);
19404
+ return 1;
19405
+ } finally {
19406
+ server.close();
19407
+ process.off("SIGINT", onSig);
19408
+ }
19409
+ }
19410
+ async function saveClaudeTokens(deps, providerId, tokens, models) {
19411
+ const entry = {
19412
+ label: "oauth-default",
19413
+ apiKey: tokens.access,
19414
+ createdAt: nowIso(),
19415
+ authMethod: "oauth",
19416
+ expiresAt: new Date(tokens.expires).toISOString(),
19417
+ refreshToken: tokens.refresh,
19418
+ tokenType: "bearer",
19419
+ scope: SCOPES
19420
+ };
19421
+ try {
19422
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
19423
+ const existing = all[providerId];
19424
+ const p = existing ? { ...existing } : { type: providerId };
19425
+ p.family = "anthropic-oauth";
19426
+ if (!p.baseUrl) p.baseUrl = CLAUDE_BASE_URL;
19427
+ if (models.length > 0) p.models = models;
19428
+ else if (!p.models || p.models.length === 0)
19429
+ p.models = ["claude-sonnet-4-6", "claude-opus-4-8"];
19430
+ const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
19431
+ keys.push(entry);
19432
+ writeKeysBack(p, keys);
19433
+ p.activeKey = entry.label;
19434
+ all[providerId] = p;
19435
+ });
19436
+ return true;
19437
+ } catch (err) {
19438
+ deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
19439
+ return false;
19440
+ }
19441
+ }
19442
+
19443
+ // src/auth-menu/github-copilot-oauth.ts
19444
+ init_provider_config_utils();
19445
+ var CLIENT_ID2 = "Iv1.b507a08c87ecfe98";
19446
+ var DEVICE_CODE_URL = "https://github.com/login/device/code";
19447
+ var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
19448
+ var COPILOT_HEADERS = {
19449
+ "User-Agent": "GitHubCopilotChat/0.35.0",
19450
+ "Editor-Version": "vscode/1.107.0",
19451
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
19452
+ "Copilot-Integration-Id": "vscode-chat"
19453
+ };
19454
+ var COPILOT_API_VERSION = "2026-06-01";
19455
+ var COPILOT_PROVIDER_ID = "github-copilot";
19456
+ function openBrowser2(url) {
19457
+ try {
19458
+ const platform3 = process.platform;
19459
+ const { command, args } = platform3 === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : platform3 === "darwin" ? { command: "open", args: [url] } : { command: "xdg-open", args: [url] };
19460
+ const child = spawn(command, args, { stdio: "ignore", windowsHide: true });
19461
+ child.on("error", () => {
19462
+ });
19463
+ child.unref();
19464
+ } catch {
19465
+ }
19466
+ }
19467
+ function sleep(ms, signal) {
19468
+ return new Promise((resolve11, reject) => {
19469
+ if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
19470
+ const t = setTimeout(resolve11, ms);
19471
+ signal.addEventListener(
19472
+ "abort",
19473
+ () => {
19474
+ clearTimeout(t);
19475
+ reject(new DOMException("Aborted", "AbortError"));
19476
+ },
19477
+ { once: true }
19478
+ );
19479
+ });
19480
+ }
19481
+ async function startDeviceFlow(signal) {
19482
+ const res = await fetch(DEVICE_CODE_URL, {
19483
+ method: "POST",
19484
+ headers: {
19485
+ accept: "application/json",
19486
+ "content-type": "application/x-www-form-urlencoded",
19487
+ "user-agent": COPILOT_HEADERS["User-Agent"]
19488
+ },
19489
+ body: new URLSearchParams({ client_id: CLIENT_ID2, scope: "read:user" }).toString(),
19490
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3)
19491
+ });
19492
+ if (!res.ok) throw new Error(`GitHub device-code request failed (${res.status})`);
19493
+ const json = await res.json();
19494
+ if (!json?.device_code || !json.user_code || !json.verification_uri || typeof json.expires_in !== "number") {
19495
+ throw new Error("Invalid device-code response");
19496
+ }
19497
+ return {
19498
+ device_code: json.device_code,
19499
+ user_code: json.user_code,
19500
+ verification_uri: json.verification_uri,
19501
+ interval: json.interval ?? 5,
19502
+ expires_in: json.expires_in
19503
+ };
19504
+ }
19505
+ async function pollForGitHubToken(device, signal) {
19506
+ let intervalMs = device.interval * 1e3;
19507
+ const expiresAt = Date.now() + device.expires_in * 1e3;
19508
+ while (Date.now() < expiresAt) {
19509
+ await sleep(intervalMs, signal);
19510
+ const res = await fetch(ACCESS_TOKEN_URL, {
19511
+ method: "POST",
19512
+ headers: {
19513
+ accept: "application/json",
19514
+ "content-type": "application/x-www-form-urlencoded",
19515
+ "user-agent": COPILOT_HEADERS["User-Agent"]
19516
+ },
19517
+ body: new URLSearchParams({
19518
+ client_id: CLIENT_ID2,
19519
+ device_code: device.device_code,
19520
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
19521
+ }).toString(),
19522
+ signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)])
19523
+ });
19524
+ const json = await res.json().catch(() => ({}));
19525
+ if (json.access_token) return json.access_token;
19526
+ if (json.error === "authorization_pending") continue;
19527
+ if (json.error === "slow_down") {
19528
+ intervalMs += 5e3;
19529
+ continue;
19530
+ }
19531
+ throw new Error(`Device flow failed: ${json.error ?? "unknown error"}`);
19532
+ }
19533
+ throw new Error("Device code expired \u2014 please restart the login.");
19534
+ }
19535
+ function isUsableCopilotChatModel(item) {
19536
+ if (typeof item.id !== "string" || item.id.length === 0) return false;
19537
+ const cap = item.capabilities;
19538
+ if (cap?.type !== "chat") return false;
19539
+ if (cap.supports?.tool_calls !== true) return false;
19540
+ const eps = item.supported_endpoints;
19541
+ if (Array.isArray(eps) && !eps.includes("/chat/completions")) return false;
19542
+ if (item.policy?.state === "disabled") return false;
19543
+ if (item.vendor === "Experimental") return false;
19544
+ return true;
19545
+ }
19546
+ async function fetchCopilotModels(copilotToken, signal) {
19547
+ try {
19548
+ const base = copilotBaseUrlFromToken(copilotToken);
19549
+ const res = await fetch(`${base}/models`, {
19550
+ headers: {
19551
+ accept: "application/json",
19552
+ authorization: `Bearer ${copilotToken}`,
19553
+ "X-GitHub-Api-Version": COPILOT_API_VERSION,
19554
+ ...COPILOT_HEADERS
19555
+ },
19556
+ signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
19557
+ });
19558
+ if (!res.ok) return [];
19559
+ const json = await res.json();
19560
+ const data = json?.data;
19561
+ if (!Array.isArray(data)) return [];
19562
+ const usable = data.filter(isUsableCopilotChatModel);
19563
+ usable.sort((a, b) => copilotModelRank(a) - copilotModelRank(b));
19564
+ return usable.map((m) => m.id);
19565
+ } catch {
19566
+ return [];
19567
+ }
19568
+ }
19569
+ function copilotModelRank(item) {
19570
+ if (item.is_chat_default === true) return 0;
19571
+ if (item.is_chat_fallback === true) return 1;
19572
+ return 2;
19573
+ }
19574
+ async function runCopilotOAuthLogin(deps, opts = {}) {
19575
+ const providerId = opts.providerId ?? COPILOT_PROVIDER_ID;
19576
+ const ac = new AbortController();
19577
+ const onSig = () => ac.abort();
19578
+ process.on("SIGINT", onSig);
19579
+ try {
19580
+ deps.renderer.write(
19581
+ color.bold(`
19582
+ Sign in with GitHub Copilot \u2014 ${color.cyan(providerId)}
19583
+ `) + color.dim(" Uses your GitHub Copilot subscription (not an API key).\n") + color.amber(" \u26A0 Using Copilot outside its official editor integrations may violate\n") + color.amber(" GitHub\u2019s Terms \u2014 your account could be rate-limited or banned.\n\n")
19584
+ );
19585
+ const device = await startDeviceFlow(ac.signal);
19586
+ deps.renderer.write(
19587
+ color.bold(` ${"\u2500".repeat(56)}
19588
+ `) + color.bold(" Open this URL and enter the code:\n") + color.cyan(` ${device.verification_uri}
19589
+ `) + color.bold(" Code: ") + color.green(color.bold(device.user_code)) + "\n" + color.bold(` ${"\u2500".repeat(56)}
19590
+
19591
+ `)
19592
+ );
19593
+ openBrowser2(device.verification_uri);
19594
+ deps.renderer.write(color.dim(" Waiting for you to authorize in the browser...\n"));
19595
+ const githubToken = await pollForGitHubToken(device, ac.signal);
19596
+ deps.renderer.write(color.dim(" Authorized. Fetching your Copilot token...\n"));
19597
+ const copilot = await refreshCopilotToken(githubToken, ac.signal);
19598
+ const models = await fetchCopilotModels(copilot.token, ac.signal);
19599
+ const saved = await saveCopilotTokens(
19600
+ deps,
19601
+ providerId,
19602
+ copilot.token,
19603
+ githubToken,
19604
+ copilot.expires,
19605
+ models
19606
+ );
19607
+ if (!saved) return 1;
19608
+ deps.renderer.write(color.green("\n \u2713 Signed in with GitHub Copilot!\n"));
19609
+ const modelHint = models[0] ?? "gpt-4o";
19610
+ deps.renderer.writeInfo(
19611
+ ` Saved as provider ${color.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
19612
+ Use: ${color.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
19613
+ ` + color.dim(" The Copilot token refreshes automatically.\n")
19614
+ );
19615
+ return 0;
19616
+ } catch (err) {
19617
+ const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
19618
+ deps.renderer.writeError(` Login failed: ${msg}`);
19619
+ return 1;
19620
+ } finally {
19621
+ process.off("SIGINT", onSig);
19622
+ }
18807
19623
  }
18808
- async function addCustomProvider(deps) {
18809
- deps.renderer.write(
18810
- `
18811
- ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the catalog.")}
18812
- `
18813
- );
18814
- const type = (await deps.reader.readLine(
18815
- ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
18816
- )).trim();
18817
- if (!type || type === "q") return false;
18818
- const existing = (await loadProviders(deps))[type];
18819
- if (existing) {
18820
- deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
19624
+ async function saveCopilotTokens(deps, providerId, copilotToken, githubToken, expires, models) {
19625
+ const entry = {
19626
+ label: "oauth-default",
19627
+ apiKey: copilotToken,
19628
+ createdAt: nowIso(),
19629
+ authMethod: "oauth",
19630
+ expiresAt: new Date(expires).toISOString(),
19631
+ refreshToken: githubToken,
19632
+ tokenType: "bearer"
19633
+ };
19634
+ try {
19635
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
19636
+ const existing = all[providerId];
19637
+ const p = existing ? { ...existing } : { type: providerId };
19638
+ p.family = "github-copilot";
19639
+ if (!p.baseUrl) p.baseUrl = copilotBaseUrlFromToken(copilotToken);
19640
+ if (models.length > 0) p.models = models;
19641
+ else if (!p.models || p.models.length === 0) p.models = ["gpt-4o"];
19642
+ const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
19643
+ keys.push(entry);
19644
+ writeKeysBack(p, keys);
19645
+ p.activeKey = entry.label;
19646
+ all[providerId] = p;
19647
+ });
19648
+ return true;
19649
+ } catch (err) {
19650
+ deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
18821
19651
  return false;
18822
19652
  }
18823
- const familyRaw = (await deps.reader.readLine(
18824
- ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
18825
- )).trim();
18826
- if (familyRaw === "q") return false;
18827
- const family = validateFamily(familyRaw);
18828
- if (!family) {
18829
- deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
18830
- return false;
19653
+ }
19654
+
19655
+ // src/auth-menu/openai-codex-oauth.ts
19656
+ init_provider_config_utils();
19657
+ var CLIENT_ID3 = "app_EMoamEEZ73f0CkXaXp7hrann";
19658
+ var AUTH_BASE_URL = "https://auth.openai.com";
19659
+ var AUTHORIZE_URL2 = `${AUTH_BASE_URL}/oauth/authorize`;
19660
+ var TOKEN_URL2 = `${AUTH_BASE_URL}/oauth/token`;
19661
+ var REDIRECT_PORT2 = 1455;
19662
+ var REDIRECT_HOST2 = "127.0.0.1";
19663
+ var REDIRECT_PATH2 = "/auth/callback";
19664
+ var REDIRECT_URI2 = `http://localhost:${REDIRECT_PORT2}${REDIRECT_PATH2}`;
19665
+ var SCOPE = "openid profile email offline_access";
19666
+ var JWT_CLAIM_PATH = "https://api.openai.com/auth";
19667
+ var ORIGINATOR = "wrongstack";
19668
+ var CODEX_PROVIDER_ID = "openai-codex";
19669
+ var CODEX_BASE_URL = "https://chatgpt.com/backend-api";
19670
+ function base64url2(buf) {
19671
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
19672
+ }
19673
+ function generatePkce2() {
19674
+ const verifier = base64url2(randomBytes(32));
19675
+ const challenge = base64url2(createHash("sha256").update(verifier).digest());
19676
+ return { verifier, challenge };
19677
+ }
19678
+ function createState() {
19679
+ return randomBytes(16).toString("hex");
19680
+ }
19681
+ function buildAuthorizeUrl2(challenge, state) {
19682
+ const url = new URL(AUTHORIZE_URL2);
19683
+ url.searchParams.set("response_type", "code");
19684
+ url.searchParams.set("client_id", CLIENT_ID3);
19685
+ url.searchParams.set("redirect_uri", REDIRECT_URI2);
19686
+ url.searchParams.set("scope", SCOPE);
19687
+ url.searchParams.set("code_challenge", challenge);
19688
+ url.searchParams.set("code_challenge_method", "S256");
19689
+ url.searchParams.set("state", state);
19690
+ url.searchParams.set("id_token_add_organizations", "true");
19691
+ url.searchParams.set("codex_cli_simplified_flow", "true");
19692
+ url.searchParams.set("originator", ORIGINATOR);
19693
+ return url.toString();
19694
+ }
19695
+ function decodeJwtPayload(token) {
19696
+ try {
19697
+ const parts = token.split(".");
19698
+ if (parts.length !== 3) return null;
19699
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
19700
+ return JSON.parse(json);
19701
+ } catch {
19702
+ return null;
18831
19703
  }
18832
- const baseUrl = (await deps.reader.readLine(
18833
- ` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, optional)")}: `
18834
- )).trim();
18835
- const modelsRaw = (await deps.reader.readLine(
18836
- ` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
18837
- )).trim();
18838
- const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
18839
- const envVarsRaw = (await deps.reader.readLine(
18840
- ` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional)")}: `
18841
- )).trim();
18842
- const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
18843
- return addKeyForProvider(type, deps, {
18844
- type,
18845
- family,
18846
- ...baseUrl ? { baseUrl } : {},
18847
- ...models ? { models } : {},
18848
- ...envVars ? { envVars } : {}
18849
- });
18850
19704
  }
18851
- async function addManualEntry(deps) {
18852
- const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
18853
- if (!pid || pid === "q") return false;
18854
- const famRaw = (await deps.reader.readLine(
18855
- ` ${color.amber("?")} Family ${color.dim("(anthropic/openai/openai-compatible/google)")}: `
18856
- )).trim();
18857
- const family = validateFamily(famRaw);
18858
- if (!family) {
18859
- deps.renderer.writeError(`Invalid family: "${famRaw}"`);
18860
- return false;
19705
+ function extractAccountId(token) {
19706
+ const payload = decodeJwtPayload(token);
19707
+ const auth = payload?.[JWT_CLAIM_PATH];
19708
+ const id = auth?.chatgpt_account_id;
19709
+ return typeof id === "string" && id.length > 0 ? id : null;
19710
+ }
19711
+ async function readTokens2(res, op) {
19712
+ if (!res.ok) {
19713
+ const text = await res.text().catch(() => "");
19714
+ throw new Error(`Codex token ${op} failed (${res.status}): ${text || res.statusText}`);
18861
19715
  }
18862
- const baseUrl = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
18863
- return addKeyForProvider(pid, deps, {
18864
- type: pid,
18865
- family,
18866
- ...baseUrl ? { baseUrl } : {}
19716
+ const json = await res.json();
19717
+ if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
19718
+ throw new Error(`Codex token ${op} response missing fields`);
19719
+ }
19720
+ return {
19721
+ access: json.access_token,
19722
+ refresh: json.refresh_token,
19723
+ expires: Date.now() + json.expires_in * 1e3
19724
+ };
19725
+ }
19726
+ async function exchangeAuthorizationCode2(code, verifier, signal) {
19727
+ const res = await fetch(TOKEN_URL2, {
19728
+ method: "POST",
19729
+ headers: { "content-type": "application/x-www-form-urlencoded" },
19730
+ body: new URLSearchParams({
19731
+ grant_type: "authorization_code",
19732
+ client_id: CLIENT_ID3,
19733
+ code,
19734
+ code_verifier: verifier,
19735
+ redirect_uri: REDIRECT_URI2
19736
+ }).toString(),
19737
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
18867
19738
  });
19739
+ return readTokens2(res, "exchange");
18868
19740
  }
18869
- async function addKeyForProvider(providerId, deps, template) {
18870
- const providers = await loadProviders(deps);
18871
- const existing = providers[providerId];
18872
- const existingKeys = existing ? normalizeKeys(existing) : [];
18873
- const usedLabels = new Set(existingKeys.map((k) => k.label));
18874
- const label = await promptForLabel(deps, usedLabels);
18875
- if (!label) return false;
18876
- const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
18877
- if (!apiKey) return false;
18878
- await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
18879
- const existingProv = all[providerId] ?? {
18880
- type: providerId,
18881
- ...template
19741
+ function callbackHtml2(ok, message) {
19742
+ const heading = ok ? "Authentication successful" : "Authentication failed";
19743
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>${heading}</title><style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#09090b;color:#fafafa;font-family:ui-sans-serif,system-ui,sans-serif;text-align:center}h1{font-size:26px;margin:0 0 8px}p{color:#a1a1aa}</style></head><body><main><h1>${heading}</h1><p>${message}</p></main></body></html>`;
19744
+ }
19745
+ function startLoopbackServer2(state) {
19746
+ let resolveCode = () => {
19747
+ };
19748
+ const codePromise = new Promise((resolve11) => {
19749
+ let settled = false;
19750
+ resolveCode = (v) => {
19751
+ if (settled) return;
19752
+ settled = true;
19753
+ resolve11(v);
18882
19754
  };
18883
- if (!existingProv.type) existingProv.type = providerId;
18884
- if (!existingProv.family && template.family) {
18885
- existingProv.family = template.family;
19755
+ });
19756
+ const server = createServer((req2, res) => {
19757
+ let url;
19758
+ try {
19759
+ url = new URL(req2.url ?? "", `http://${REDIRECT_HOST2}`);
19760
+ } catch {
19761
+ res.statusCode = 400;
19762
+ res.end();
19763
+ return;
18886
19764
  }
18887
- if (!existingProv.baseUrl && template.baseUrl) {
18888
- existingProv.baseUrl = template.baseUrl;
19765
+ if (url.pathname !== REDIRECT_PATH2) {
19766
+ res.statusCode = 404;
19767
+ res.setHeader("content-type", "text/html; charset=utf-8");
19768
+ res.end(callbackHtml2(false, "Callback route not found."));
19769
+ return;
18889
19770
  }
18890
- if (!existingProv.envVars && template.envVars) {
18891
- existingProv.envVars = template.envVars;
19771
+ res.setHeader("content-type", "text/html; charset=utf-8");
19772
+ const err = url.searchParams.get("error");
19773
+ if (err) {
19774
+ res.statusCode = 400;
19775
+ res.end(callbackHtml2(false, `Authorization error: ${err}`));
19776
+ resolveCode(null);
19777
+ return;
18892
19778
  }
18893
- const list = normalizeKeys(existingProv);
18894
- list.push({ label, apiKey, createdAt: nowIso() });
18895
- writeKeysBack(existingProv, list);
18896
- if (!existingProv.activeKey) existingProv.activeKey = label;
18897
- all[providerId] = existingProv;
19779
+ if (url.searchParams.get("state") !== state) {
19780
+ res.statusCode = 400;
19781
+ res.end(callbackHtml2(false, "State mismatch \u2014 please restart the login."));
19782
+ resolveCode(null);
19783
+ return;
19784
+ }
19785
+ const code = url.searchParams.get("code");
19786
+ if (!code) {
19787
+ res.statusCode = 400;
19788
+ res.end(callbackHtml2(false, "Missing authorization code."));
19789
+ return;
19790
+ }
19791
+ res.statusCode = 200;
19792
+ res.end(callbackHtml2(true, "You can close this window and return to the terminal."));
19793
+ resolveCode(code);
19794
+ });
19795
+ return new Promise((resolve11) => {
19796
+ server.on("error", () => {
19797
+ resolveCode(null);
19798
+ resolve11({
19799
+ bound: false,
19800
+ waitForCode: () => Promise.resolve(null),
19801
+ close: () => {
19802
+ try {
19803
+ server.close();
19804
+ } catch {
19805
+ }
19806
+ }
19807
+ });
19808
+ });
19809
+ server.listen(REDIRECT_PORT2, REDIRECT_HOST2, () => {
19810
+ resolve11({
19811
+ bound: true,
19812
+ waitForCode: () => codePromise,
19813
+ close: () => {
19814
+ resolveCode(null);
19815
+ try {
19816
+ server.close();
19817
+ } catch {
19818
+ }
19819
+ }
19820
+ });
19821
+ });
18898
19822
  });
19823
+ }
19824
+ function openBrowser3(url) {
19825
+ try {
19826
+ const platform3 = process.platform;
19827
+ const { command, args } = platform3 === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : platform3 === "darwin" ? { command: "open", args: [url] } : { command: "xdg-open", args: [url] };
19828
+ const child = spawn(command, args, { stdio: "ignore", windowsHide: true });
19829
+ child.on("error", () => {
19830
+ });
19831
+ child.unref();
19832
+ } catch {
19833
+ }
19834
+ }
19835
+ function parseAuthorizationInput2(input) {
19836
+ const value = input.trim();
19837
+ if (!value) return {};
19838
+ try {
19839
+ const url = new URL(value);
19840
+ return {
19841
+ code: url.searchParams.get("code") ?? void 0,
19842
+ state: url.searchParams.get("state") ?? void 0
19843
+ };
19844
+ } catch {
19845
+ }
19846
+ if (value.includes("code=")) {
19847
+ const params = new URLSearchParams(value);
19848
+ return {
19849
+ code: params.get("code") ?? void 0,
19850
+ state: params.get("state") ?? void 0
19851
+ };
19852
+ }
19853
+ return { code: value };
19854
+ }
19855
+ async function runCodexOAuthLogin(deps, opts = {}) {
19856
+ const providerId = opts.providerId ?? CODEX_PROVIDER_ID;
19857
+ const pkce = generatePkce2();
19858
+ const state = createState();
19859
+ const authorizeUrl = buildAuthorizeUrl2(pkce.challenge, state);
19860
+ const ac = new AbortController();
19861
+ const onSig = () => ac.abort();
19862
+ process.on("SIGINT", onSig);
19863
+ const server = await startLoopbackServer2(state);
18899
19864
  deps.renderer.write(
18900
- ` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
18901
- `
19865
+ color.bold(`
19866
+ Sign in with ChatGPT \u2014 ${color.cyan(providerId)}
19867
+ `) + color.dim(" Uses your ChatGPT Plus/Pro/Team subscription (not an API key).\n") + color.amber(" \u26A0 Using a subscription outside the official Codex client may violate\n") + color.amber(" OpenAI\u2019s Terms \u2014 your account could be rate-limited or banned.\n") + color.dim(" Sanctioned programmatic use = an API key: ") + color.bold("wstack auth openai") + color.dim("\n\n") + color.bold(` ${"\u2500".repeat(56)}
19868
+ `) + color.bold(" Open this URL in your browser to sign in:\n") + color.cyan(` ${authorizeUrl}
19869
+ `) + color.bold(` ${"\u2500".repeat(56)}
19870
+
19871
+ `)
18902
19872
  );
18903
- deps.renderer.write(color.dim(` Launch: wstack --provider ${providerId} "<task>"
18904
- `));
18905
- return true;
19873
+ if (server.bound) {
19874
+ openBrowser3(authorizeUrl);
19875
+ deps.renderer.write(
19876
+ color.dim(" A browser window should open. Waiting for you to finish signing in...\n") + color.dim(" (Listening on http://localhost:1455 \u2014 press Ctrl+C to cancel.)\n")
19877
+ );
19878
+ } else {
19879
+ deps.renderer.write(
19880
+ color.amber(" \u26A0 Could not start the local callback listener (port 1455 in use).\n") + color.dim(" After signing in, copy the full redirect URL from your browser\n") + color.dim(" (it starts with http://localhost:1455/auth/callback) and paste it below.\n")
19881
+ );
19882
+ }
19883
+ let code;
19884
+ try {
19885
+ if (server.bound) {
19886
+ const got = await server.waitForCode();
19887
+ if (got) code = got;
19888
+ }
19889
+ if (!code) {
19890
+ const input = (await deps.reader.readLine(
19891
+ `
19892
+ ${color.amber("?")} Paste the redirect URL or code ${color.dim("(or q to cancel)")}: `
19893
+ )).trim();
19894
+ if (input.toLowerCase() === "q" || input === "") {
19895
+ deps.renderer.write(color.dim(" Cancelled.\n"));
19896
+ return 1;
19897
+ }
19898
+ const parsed = parseAuthorizationInput2(input);
19899
+ if (parsed.state && parsed.state !== state) {
19900
+ deps.renderer.writeError(" State mismatch \u2014 please restart the login flow.");
19901
+ return 1;
19902
+ }
19903
+ code = parsed.code;
19904
+ }
19905
+ if (!code) {
19906
+ deps.renderer.writeError(" No authorization code received.");
19907
+ return 1;
19908
+ }
19909
+ deps.renderer.write(color.dim("\n Exchanging authorization code for tokens...\n"));
19910
+ const tokens = await exchangeAuthorizationCode2(code, pkce.verifier, ac.signal);
19911
+ const accountId = extractAccountId(tokens.access);
19912
+ if (!accountId) {
19913
+ deps.renderer.writeError(
19914
+ " Signed in, but the token has no ChatGPT account id.\n This account may not have Codex/ChatGPT subscription access."
19915
+ );
19916
+ return 1;
19917
+ }
19918
+ const saved = await saveCodexTokens(deps, providerId, tokens, accountId);
19919
+ if (!saved) return 1;
19920
+ deps.renderer.write(color.green("\n \u2713 Signed in with ChatGPT!\n"));
19921
+ deps.renderer.writeInfo(
19922
+ ` Saved as provider ${color.bold(providerId)}.
19923
+ Use: ${color.bold(`wstack --provider ${providerId} --model gpt-5.5`)} "<task>"
19924
+ ` + color.dim(" Tokens refresh automatically before they expire.\n")
19925
+ );
19926
+ return 0;
19927
+ } catch (err) {
19928
+ const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
19929
+ deps.renderer.writeError(` Login failed: ${msg}`);
19930
+ return 1;
19931
+ } finally {
19932
+ server.close();
19933
+ process.off("SIGINT", onSig);
19934
+ }
18906
19935
  }
18907
- async function promptForLabel(deps, usedLabels) {
18908
- const defaultLabel = suggestLabel(usedLabels);
18909
- const labelRaw = (await deps.reader.readLine(
18910
- ` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
18911
- )).trim();
18912
- const label = labelRaw || defaultLabel;
18913
- if (usedLabels.has(label)) {
18914
- deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
18915
- return null;
19936
+ async function saveCodexTokens(deps, providerId, tokens, accountId) {
19937
+ const entry = {
19938
+ label: "oauth-default",
19939
+ apiKey: tokens.access,
19940
+ createdAt: nowIso(),
19941
+ authMethod: "oauth",
19942
+ expiresAt: new Date(tokens.expires).toISOString(),
19943
+ refreshToken: tokens.refresh,
19944
+ tokenType: "bearer",
19945
+ scope: SCOPE,
19946
+ accountId
19947
+ };
19948
+ try {
19949
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
19950
+ const existing = all[providerId];
19951
+ const p = existing ? { ...existing } : { type: providerId };
19952
+ p.family = "openai-codex";
19953
+ if (!p.baseUrl) p.baseUrl = CODEX_BASE_URL;
19954
+ if (!p.models || p.models.length === 0) {
19955
+ p.models = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark"];
19956
+ }
19957
+ const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
19958
+ keys.push(entry);
19959
+ writeKeysBack(p, keys);
19960
+ p.activeKey = entry.label;
19961
+ all[providerId] = p;
19962
+ });
19963
+ return true;
19964
+ } catch (err) {
19965
+ deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
19966
+ return false;
18916
19967
  }
18917
- return label;
18918
19968
  }
18919
19969
 
18920
19970
  // src/auth-menu/provider-menu.ts
@@ -19092,6 +20142,24 @@ function validKeyIndex(arg, max, deps, verb) {
19092
20142
  }
19093
20143
 
19094
20144
  // 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
+ }
19095
20163
  async function runTopMenu(deps) {
19096
20164
  for (; ; ) {
19097
20165
  const providers = await loadProviders(deps);
@@ -19111,6 +20179,10 @@ ${color.amber("?")} Pick: `)).trim().toLowerCase();
19111
20179
  await addCustomProvider(deps);
19112
20180
  continue;
19113
20181
  }
20182
+ if (choice === "s" || choice === "signin" || choice === "login") {
20183
+ await runSignInMenu(deps);
20184
+ continue;
20185
+ }
19114
20186
  const idx = Number.parseInt(choice, 10);
19115
20187
  if (!Number.isNaN(idx) && idx >= 1 && idx <= ids.length) {
19116
20188
  const pid = ids[idx - 1];
@@ -19189,6 +20261,26 @@ var authCmd = async (args, deps) => {
19189
20261
  }
19190
20262
  return runAuthRemove(menuDeps, pid);
19191
20263
  }
20264
+ if (first === "login") {
20265
+ const pid = (flags.positional[1] ?? "").toLowerCase();
20266
+ const codexAliases = /* @__PURE__ */ new Set(["", "openai", "codex", "chatgpt", "codex-cli", CODEX_PROVIDER_ID]);
20267
+ const claudeAliases = /* @__PURE__ */ new Set(["claude", "anthropic", "claude-pro", "claude-max", "anthropic-oauth"]);
20268
+ const copilotAliases = /* @__PURE__ */ new Set(["copilot", "github", "github-copilot", "gh"]);
20269
+ if (claudeAliases.has(pid)) {
20270
+ return runClaudeOAuthLogin(menuDeps);
20271
+ }
20272
+ if (copilotAliases.has(pid)) {
20273
+ return runCopilotOAuthLogin(menuDeps);
20274
+ }
20275
+ if (codexAliases.has(pid)) {
20276
+ return runCodexOAuthLogin(menuDeps);
20277
+ }
20278
+ deps.renderer.writeError("OAuth login is only supported for ChatGPT, Claude, and GitHub Copilot.");
20279
+ deps.renderer.write(
20280
+ color.dim(" Sign in with ChatGPT: ") + color.bold("wstack auth login chatgpt") + "\n" + color.dim(" Sign in with Claude: ") + color.bold("wstack auth login claude") + "\n" + color.dim(" Sign in with Copilot: ") + color.bold("wstack auth login copilot") + "\n" + color.dim(" For an API key instead: ") + color.bold(`wstack auth ${pid}`) + "\n"
20281
+ );
20282
+ return 1;
20283
+ }
19192
20284
  return runAuthDirect(menuDeps, {
19193
20285
  providerId: first,
19194
20286
  label: flags.label,
@@ -21165,8 +22257,11 @@ var providersCmd = async (args, deps) => {
21165
22257
  const all = await deps.modelsRegistry.listProviders();
21166
22258
  const byFamily = {
21167
22259
  anthropic: [],
22260
+ "anthropic-oauth": [],
21168
22261
  openai: [],
21169
22262
  "openai-compatible": [],
22263
+ "openai-codex": [],
22264
+ "github-copilot": [],
21170
22265
  google: [],
21171
22266
  unsupported: []
21172
22267
  };
@@ -21370,11 +22465,11 @@ async function mutateModelsConfig(deps, mutator) {
21370
22465
  }
21371
22466
  parsed = {};
21372
22467
  }
21373
- const decrypted = decryptConfigSecrets(parsed, vault);
22468
+ const decrypted = decryptConfigSecrets$1(parsed, vault);
21374
22469
  const models = decrypted.models ?? {};
21375
22470
  mutator(models);
21376
22471
  decrypted.models = models;
21377
- const encrypted = encryptConfigSecrets(decrypted, vault);
22472
+ const encrypted = encryptConfigSecrets$1(decrypted, vault);
21378
22473
  await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
21379
22474
  }
21380
22475
  function parseSizeFlag(raw) {
@@ -22578,9 +23673,9 @@ async function checkGitInCwd(opts) {
22578
23673
  const answer = (await reader.readLine(` ${color.amber("?")} Initialize one here? ${color.dim("[y/N]")} `)).trim().toLowerCase();
22579
23674
  if (answer === "y" || answer === "yes") {
22580
23675
  try {
22581
- const { spawn: spawn6 } = await import('child_process');
23676
+ const { spawn: spawn9 } = await import('child_process');
22582
23677
  await new Promise((resolve11, reject) => {
22583
- const child = spawn6("git", ["init"], {
23678
+ const child = spawn9("git", ["init"], {
22584
23679
  cwd,
22585
23680
  signal: AbortSignal.timeout(1e4),
22586
23681
  windowsHide: true
@@ -22886,6 +23981,7 @@ function bindSystemPromptBuilder(deps) {
22886
23981
  modeId: deps.modeId,
22887
23982
  modePrompt: deps.modePrompt,
22888
23983
  modelCapabilities: deps.modelCapabilities,
23984
+ tokenSavingMode: deps.tokenSavingMode,
22889
23985
  planPath: () => deps.sessionRef.current ? deps.pathJoiner.join(
22890
23986
  deps.paths.projectSessions,
22891
23987
  `${deps.sessionRef.current.id}.plan.json`
@@ -22905,12 +24001,32 @@ function bindSystemPromptBuilder(deps) {
22905
24001
  })
22906
24002
  );
22907
24003
  }
24004
+ var SIBLING_CATALOG = {
24005
+ "anthropic-oauth": "anthropic",
24006
+ "openai-codex": "openai",
24007
+ "github-copilot": "openai"
24008
+ };
22908
24009
  async function resolveRuntimeMaxContext(input) {
22909
24010
  const explicitContext = positiveNumber(input.config.context?.effectiveMaxContext);
22910
24011
  if (explicitContext) return explicitContext;
22911
24012
  const providerConfig = input.runtimeProviderConfig ?? input.config.providers?.[input.providerId];
22912
24013
  const providerOverride = positiveNumber(readConfiguredMaxContext(providerConfig));
22913
24014
  if (providerOverride) return providerOverride;
24015
+ const sibling = providerConfig?.family ? SIBLING_CATALOG[providerConfig.family] : void 0;
24016
+ if (sibling && input.modelsRegistry) {
24017
+ const mergedModels = mergeCustomModelDefs(providerConfig?.customModels, input.config.models);
24018
+ const caps = await capabilitiesFor(
24019
+ input.modelsRegistry,
24020
+ sibling,
24021
+ input.modelId,
24022
+ mergedModels
24023
+ ).catch(() => void 0);
24024
+ const siblingMax = positiveNumber(caps?.maxContext);
24025
+ if (siblingMax) return siblingMax;
24026
+ const directModel = await input.modelsRegistry.getModel(sibling, input.modelId).catch(() => void 0);
24027
+ const directMax = positiveNumber(directModel?.capabilities.maxContext);
24028
+ if (directMax) return directMax;
24029
+ }
22914
24030
  const catalogId = providerConfig?.type && providerConfig.type !== input.providerId ? providerConfig.type : input.providerId;
22915
24031
  if (input.modelsRegistry) {
22916
24032
  const topLevelBaseUrlApplies = input.providerId === input.config.provider;
@@ -24178,7 +25294,9 @@ async function execute(deps) {
24178
25294
  enhanceController,
24179
25295
  statuslineHiddenItems,
24180
25296
  setStatuslineHiddenItems,
25297
+ saveStatuslineHiddenItems,
24181
25298
  agentsMonitorController,
25299
+ onPanelOpen,
24182
25300
  getYolo,
24183
25301
  getAutonomy,
24184
25302
  onAutonomy,
@@ -24629,6 +25747,11 @@ Reply with ONLY the JSON object.`
24629
25747
  allowOutsideProjectRoot: cfg.features?.allowOutsideProjectRoot ?? true,
24630
25748
  contextAutoCompact: cfg.context?.autoCompact !== false,
24631
25749
  contextStrategy: cfg.context?.strategy ?? "hybrid",
25750
+ contextMode: (() => {
25751
+ const m = cfg.context?.["mode"];
25752
+ return m === "frugal" || m === "deep" || m === "archival" ? m : "balanced";
25753
+ })(),
25754
+ maxConcurrent: cfg.maxConcurrent ?? 0,
24632
25755
  logLevel: cfg.log?.level ?? "info",
24633
25756
  auditLevel: cfg.session?.auditLevel ?? "standard",
24634
25757
  indexOnStart: cfg.indexing?.onSessionStart !== false,
@@ -24685,7 +25808,7 @@ Reply with ONLY the JSON object.`
24685
25808
  );
24686
25809
  }
24687
25810
  const parsed = JSON.parse(raw);
24688
- const decrypted = decryptConfigSecrets(parsed, noOpVault);
25811
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
24689
25812
  if (s.nextPrediction !== void 0) {
24690
25813
  decrypted.nextPrediction = s.nextPrediction;
24691
25814
  }
@@ -24755,7 +25878,7 @@ Reply with ONLY the JSON object.`
24755
25878
  decrypted.autonomy = autonomy;
24756
25879
  }
24757
25880
  const toWrite = targetPath === wpaths.globalConfig ? decrypted : filterSafeForProject(decrypted);
24758
- const encrypted = encryptConfigSecrets(toWrite, noOpVault);
25881
+ const encrypted = encryptConfigSecrets$1(toWrite, noOpVault);
24759
25882
  if (targetPath !== wpaths.globalConfig) {
24760
25883
  await fsp5.mkdir(path4.dirname(targetPath), { recursive: true });
24761
25884
  }
@@ -24855,6 +25978,7 @@ Reply with ONLY the JSON object.`
24855
25978
  enhanceController,
24856
25979
  statuslineHiddenItems,
24857
25980
  setStatuslineHiddenItems,
25981
+ saveStatuslineHiddenItems,
24858
25982
  agentsMonitorController,
24859
25983
  getLiveSessions: async () => {
24860
25984
  const { SessionRegistry } = await import('@wrongstack/core');
@@ -25180,12 +26304,13 @@ ${parts.join("\n")}
25180
26304
  // `wrongstack quick` sets flags.quick — open the F3 agents monitor by default.
25181
26305
  initialAgentsMonitorOpen: !!flags.quick,
25182
26306
  tokenSavingMode: normalizeTokenSavingTier(config.features.tokenSavingMode) !== "off",
25183
- toolCount: agent.tools.list().length
26307
+ toolCount: agent.tools.list().length,
26308
+ onPanelOpen
25184
26309
  });
25185
26310
  if (code === PROJECT_SWITCH_EXIT_CODE && pendingProjectSwitch) {
25186
26311
  const { root, name, resumeSessionId } = pendingProjectSwitch;
25187
26312
  process.stdout.write("\x1B[2J\x1B[H");
25188
- const { spawn: spawn6 } = await import('child_process');
26313
+ const { spawn: spawn9 } = await import('child_process');
25189
26314
  const { createRequire: createRequire8 } = await import('module');
25190
26315
  let cliPath;
25191
26316
  try {
@@ -25204,7 +26329,7 @@ ${parts.join("\n")}
25204
26329
  const nodeExe = process.execPath;
25205
26330
  const spawnArgs = [cliPath, "--no-interactive"];
25206
26331
  if (resumeSessionId) spawnArgs.push("--resume", resumeSessionId);
25207
- spawn6(nodeExe, spawnArgs, {
26332
+ spawn9(nodeExe, spawnArgs, {
25208
26333
  cwd: root,
25209
26334
  stdio: "ignore",
25210
26335
  detached: true
@@ -25239,6 +26364,7 @@ ${parts.join("\n")}
25239
26364
  open: !!flags.open,
25240
26365
  modelsRegistry,
25241
26366
  globalConfigPath: wpaths.globalConfig,
26367
+ mcpRegistry,
25242
26368
  subscribeEternalIteration,
25243
26369
  sessionStore,
25244
26370
  sessionsDir: wpaths.projectSessions,
@@ -25748,8 +26874,9 @@ var MultiAgentHost = class _MultiAgentHost {
25748
26874
  const baseRegistry = this.subagentToolRegistry(tools);
25749
26875
  if (injectedFleetEmit) baseRegistry.register(injectedFleetEmit);
25750
26876
  if (injectedFleetStatus) baseRegistry.register(injectedFleetStatus);
26877
+ const subAllowedCaps = this.resolveSubagentCapabilities(subCfg);
25751
26878
  const toolExecutor = new ToolExecutor(baseRegistry, {
25752
- permissionPolicy: new AutoApprovePermissionPolicy(),
26879
+ permissionPolicy: new AutoApprovePermissionPolicy(subAllowedCaps),
25753
26880
  secretScrubber: this.deps.secretScrubber,
25754
26881
  renderer: this.deps.renderer,
25755
26882
  events,
@@ -25767,9 +26894,9 @@ var MultiAgentHost = class _MultiAgentHost {
25767
26894
  context: ctx,
25768
26895
  // Subagents cannot answer interactive permission prompts — they
25769
26896
  // run under a director, not the user. Auto-approve everything
25770
- // (except tool-level hard denies); the user already authorized
25771
- // the work when they invoked the leader.
25772
- permissionPolicy: new AutoApprovePermissionPolicy(),
26897
+ // whose capability is in the (possibly widened) allowlist; the
26898
+ // user already authorized the work when they invoked the leader.
26899
+ permissionPolicy: new AutoApprovePermissionPolicy(subAllowedCaps),
25773
26900
  toolExecutor
25774
26901
  });
25775
26902
  agent.extensions.register(
@@ -25915,6 +27042,36 @@ var MultiAgentHost = class _MultiAgentHost {
25915
27042
  const allowSet = new Set(allow);
25916
27043
  return all.filter((t) => allowSet.has(t.name));
25917
27044
  }
27045
+ /**
27046
+ * Resolve the capability allowlist for a subagent's auto-approve policy.
27047
+ *
27048
+ * Precedence:
27049
+ * 1. Explicit `subCfg.allowedCapabilities` — the spawn site knows best
27050
+ * (e.g. `/techstack` grants exactly `fs.write` on top of the safe set).
27051
+ * 2. A scoped `subCfg.tools` slice — the granted tool slice IS the intended
27052
+ * capability grant. The leader/roster deliberately chose these tools, so
27053
+ * allow exactly the capabilities they declare (plus the read-only safe
27054
+ * floor). Without this, a role given the `write`/`build` tool presets
27055
+ * could *see* those tools but the policy would deny execution (`fs.write`
27056
+ * and `shell.*` are not in the read-only default), silently crippling
27057
+ * every code-writing / build role in the catalog.
27058
+ * 3. No tool restriction (full registry) → the WIDE working set
27059
+ * (`WIDE_SUBAGENT_CAPABILITIES`: read, write, net, shell, install). The
27060
+ * user authorized full developer work when they invoked the leader, so a
27061
+ * delegated agent runs the same toolchain end-to-end. The genuinely
27062
+ * blast-radius-escaping capabilities (fs.write.outside-project, mcp.proxy,
27063
+ * subagent.spawn, config.mutate) stay off and need an explicit (1) grant.
27064
+ */
27065
+ resolveSubagentCapabilities(subCfg) {
27066
+ if (subCfg.allowedCapabilities) return subCfg.allowedCapabilities;
27067
+ const allow = subCfg.tools;
27068
+ if (!allow || allow.length === 0) return WIDE_SUBAGENT_CAPABILITIES;
27069
+ const caps = new Set(WIDE_SUBAGENT_CAPABILITIES);
27070
+ for (const tool of this.filterTools([...allow])) {
27071
+ for (const c of tool.capabilities ?? []) caps.add(c);
27072
+ }
27073
+ return [...caps];
27074
+ }
25918
27075
  subagentToolRegistry(allow) {
25919
27076
  if (!allow || allow.length === 0) return this.deps.toolRegistry;
25920
27077
  const sub = new ToolRegistry();
@@ -25937,7 +27094,8 @@ var MultiAgentHost = class _MultiAgentHost {
25937
27094
  role: "general",
25938
27095
  provider: opts?.provider,
25939
27096
  model: opts?.model,
25940
- tools: opts?.tools
27097
+ tools: opts?.tools,
27098
+ allowedCapabilities: opts?.allowedCapabilities
25941
27099
  };
25942
27100
  const { subagentId, taskId } = await this._spawnAndAssign(subagentConfig, description);
25943
27101
  this.fleetManager?.addPendingTask(taskId, subagentId, description);
@@ -26563,7 +27721,7 @@ async function setupCodebaseIndexing(deps) {
26563
27721
  let watcher;
26564
27722
  if (idx.watchExternal) {
26565
27723
  try {
26566
- watcher = fs2.watch(projectRoot, { recursive: true }, (_event, filename) => {
27724
+ watcher = fs3.watch(projectRoot, { recursive: true }, (_event, filename) => {
26567
27725
  if (!filename) return;
26568
27726
  const rel = filename.toString();
26569
27727
  if (isIgnored(rel)) return;
@@ -26971,7 +28129,17 @@ async function setupProvider(params) {
26971
28129
  resolvedProvider = await modelsRegistry.getProvider(savedProviderCfg.type).catch(() => void 0);
26972
28130
  }
26973
28131
  if (!resolvedProvider) {
26974
- if (!savedProviderCfg?.family) {
28132
+ if (savedProviderCfg?.family) {
28133
+ resolvedProvider = {
28134
+ id: config.provider,
28135
+ name: config.provider,
28136
+ family: savedProviderCfg.family,
28137
+ apiBase: savedProviderCfg.baseUrl,
28138
+ envVars: savedProviderCfg.envVars ?? [],
28139
+ models: (savedProviderCfg.models ?? []).map((m) => ({ id: m, name: m })),
28140
+ npm: void 0
28141
+ };
28142
+ } else {
26975
28143
  logger.warn(
26976
28144
  `Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
26977
28145
  );
@@ -27053,11 +28221,13 @@ async function resolveModeAndCapabilities(deps) {
27053
28221
  ).catch(() => void 0),
27054
28222
  deps.modelsRegistry.getModel(deps.config.provider, deps.config.model).catch(() => void 0)
27055
28223
  ]);
27056
- const modelCapabilities = resolvedCaps ? {
27057
- maxContextTokens: resolvedCaps.maxContext,
27058
- supportsTools: resolvedCaps.tools,
27059
- supportsVision: resolvedCaps.vision,
27060
- supportsReasoning: resolvedModel?.capabilities.reasoning ?? false
28224
+ const instanceCaps = provider.capabilities;
28225
+ const useInstanceCaps = !resolvedModel && !!instanceCaps;
28226
+ const modelCapabilities = resolvedCaps || useInstanceCaps ? {
28227
+ maxContextTokens: (useInstanceCaps ? instanceCaps?.maxContext : resolvedCaps?.maxContext) || instanceCaps?.maxContext || 0,
28228
+ supportsTools: useInstanceCaps ? !!instanceCaps?.tools : resolvedCaps?.tools ?? false,
28229
+ supportsVision: useInstanceCaps ? !!instanceCaps?.vision : resolvedCaps?.vision ?? false,
28230
+ supportsReasoning: resolvedModel?.capabilities.reasoning ?? instanceCaps?.reasoning ?? false
27061
28231
  } : void 0;
27062
28232
  return {
27063
28233
  kind: "ok",
@@ -27105,6 +28275,21 @@ async function main(argv) {
27105
28275
  } = ctx;
27106
28276
  const { updateInfo: refreshedUpdateInfo } = await runPreflight(config, updateInfo);
27107
28277
  updateInfo = refreshedUpdateInfo;
28278
+ setOAuthTokenPersister((providerId, creds) => {
28279
+ void mutateConfigProviders(wpaths.globalConfig, vault, (all) => {
28280
+ const p = all[providerId];
28281
+ if (!p) return;
28282
+ const keys = normalizeKeys(p);
28283
+ const active = p.activeKey ? keys.find((k) => k.label === p.activeKey) : keys[0];
28284
+ if (!active) return;
28285
+ active.apiKey = creds.accessToken;
28286
+ active.refreshToken = creds.refreshToken;
28287
+ active.expiresAt = new Date(creds.expiresAt).toISOString();
28288
+ if (creds.accountId) active.accountId = creds.accountId;
28289
+ writeKeysBack(p, keys);
28290
+ }).catch(() => {
28291
+ });
28292
+ });
27108
28293
  const { events, container } = wireContainer({
27109
28294
  config,
27110
28295
  wpaths,
@@ -27168,6 +28353,7 @@ async function main(argv) {
27168
28353
  modePrompt,
27169
28354
  modelCapabilities,
27170
28355
  skillsEnabled: config.features.skills,
28356
+ tokenSavingMode: config.features.tokenSavingMode,
27171
28357
  paths: {
27172
28358
  projectGoal: wpaths.projectGoal,
27173
28359
  projectSessions: wpaths.projectSessions
@@ -27594,7 +28780,10 @@ async function main(argv) {
27594
28780
  toolRegistry,
27595
28781
  events,
27596
28782
  log: logger,
27597
- lazyMode: normalizeTokenSavingTier(config.features.tokenSavingMode) !== "off"
28783
+ lazyMode: normalizeTokenSavingTier(config.features.tokenSavingMode) !== "off",
28784
+ // Lazy-connect (per-server `lazy`) needs a manifest cache to register tools
28785
+ // cold; idle auto-sleep uses the default timeout.
28786
+ cacheDir: wpaths.cacheDir
27598
28787
  });
27599
28788
  if (config.features.mcp) {
27600
28789
  for (const cfg of Object.values(config.mcpServers ?? {})) {
@@ -27996,12 +29185,24 @@ async function main(argv) {
27996
29185
  const setStatuslineHiddenItems = (items) => {
27997
29186
  currentHiddenItems = items;
27998
29187
  };
29188
+ const ALL_STATUSLINE_KEYS = ["todos", "plan", "tasks", "fleet", "git", "elapsed", "context", "cost", "working_dir"];
29189
+ const saveStatuslineHiddenItems = async (items) => {
29190
+ currentHiddenItems = items;
29191
+ const cfg = { ...DEFAULTS };
29192
+ for (const k of ALL_STATUSLINE_KEYS) {
29193
+ cfg[k] = !items.includes(k);
29194
+ }
29195
+ await saveStatuslineConfig(cfg);
29196
+ };
27999
29197
  const agentsMonitorController = {
28000
29198
  visible: false,
28001
29199
  setVisible(visible) {
28002
29200
  this.visible = visible;
28003
29201
  }
28004
29202
  };
29203
+ const onPanelOpen = {
29204
+ current: null
29205
+ };
28005
29206
  const autoPhaseHost = createAutoPhaseHost({
28006
29207
  multiAgentHost,
28007
29208
  getConfig: () => config,
@@ -28045,7 +29246,9 @@ async function main(argv) {
28045
29246
  statuslineConfig: statuslineConfigDeps,
28046
29247
  statuslineHiddenItems: [...currentHiddenItems],
28047
29248
  setStatuslineHiddenItems,
29249
+ saveStatuslineHiddenItems,
28048
29250
  agentsMonitorController,
29251
+ onPanelOpen,
28049
29252
  configStore,
28050
29253
  reader,
28051
29254
  brain,
@@ -28805,6 +30008,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
28805
30008
  enhanceController,
28806
30009
  statuslineHiddenItems,
28807
30010
  setStatuslineHiddenItems,
30011
+ saveStatuslineHiddenItems,
28808
30012
  getYolo: () => {
28809
30013
  const policy = container.resolve(TOKENS.PermissionPolicy);
28810
30014
  return policy.getYolo?.() ?? config.yolo ?? false;