@wrongstack/cli 0.51.3 → 0.63.4

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
@@ -2,7 +2,7 @@
2
2
  import * as path8 from 'path';
3
3
  import { join } from 'path';
4
4
  import * as fsp3 from 'fs/promises';
5
- import { color, writeErr, DefaultTaskStore, TaskTracker, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, renderTaskGraph, SpecVersioning, atomicWrite, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, bootConfig as bootConfig$1, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, onResize, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError, ERROR_CODES } from '@wrongstack/core';
5
+ import { color, writeErr, DefaultTaskStore, TaskTracker, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, renderTaskGraph, SpecVersioning, DefaultSecretScrubber, atomicWrite, DefaultPathResolver, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, HookRegistry, HookRunner, SlashCommandRegistry, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, bootConfig as bootConfig$1, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, onResize, ERROR_CODES, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError } from '@wrongstack/core';
6
6
  import { createRequire } from 'module';
7
7
  import * as os2 from 'os';
8
8
  import os2__default from 'os';
@@ -10,21 +10,21 @@ import * as crypto2 from 'crypto';
10
10
  import { randomUUID } from 'crypto';
11
11
  import { DefaultSecretVault, decryptConfigSecrets, encryptConfigSecrets, isSecretField } from '@wrongstack/core/security';
12
12
  import { WebSocketServer, WebSocket } from 'ws';
13
- import { MCPRegistry } from '@wrongstack/mcp';
13
+ import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
14
14
  import { capabilitiesFor, buildProviderFactoriesFromRegistry, makeProviderFromConfig } from '@wrongstack/providers';
15
15
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
16
16
  import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
17
17
  import { fileURLToPath } from 'url';
18
18
  import * as readline from 'readline';
19
- import * as fs10 from 'fs';
19
+ import * as fs12 from 'fs';
20
20
  import { writeFileSync, existsSync, readFileSync } from 'fs';
21
21
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
22
22
  import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
23
23
  import { ACP_AGENTS, SubagentBudget } from '@wrongstack/core/coordination';
24
24
  import { spawn } from 'child_process';
25
25
  import { allServers } from '@wrongstack/core/infrastructure';
26
- import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
27
26
  import { ToolExecutor } from '@wrongstack/core/execution';
27
+ import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
28
28
 
29
29
  var __defProp = Object.defineProperty;
30
30
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1696,6 +1696,7 @@ __export(webui_server_exports, {
1696
1696
  async function runWebUI(opts) {
1697
1697
  const port = opts.port ?? 3457;
1698
1698
  const clients = /* @__PURE__ */ new Map();
1699
+ const secretScrubber = new DefaultSecretScrubber();
1699
1700
  let abortController = null;
1700
1701
  const authToken = crypto2.randomBytes(16).toString("hex");
1701
1702
  const wss = new WebSocketServer({ port, host: "127.0.0.1", maxPayload: 1 * 1024 * 1024 });
@@ -1735,7 +1736,7 @@ async function runWebUI(opts) {
1735
1736
  payload: {
1736
1737
  id: e.id,
1737
1738
  name: e.name,
1738
- input: e.input,
1739
+ input: secretScrubber.scrubObject(e.input),
1739
1740
  messageId: `tool_${e.id}`
1740
1741
  }
1741
1742
  });
@@ -1764,8 +1765,8 @@ async function runWebUI(opts) {
1764
1765
  name: e.name,
1765
1766
  durationMs: e.durationMs,
1766
1767
  ok: e.ok,
1767
- input: e.input,
1768
- output: e.output
1768
+ input: secretScrubber.scrubObject(e.input),
1769
+ output: secretScrubber.scrubObject(e.output)
1769
1770
  }
1770
1771
  });
1771
1772
  })
@@ -2344,19 +2345,43 @@ async function resolveRuntimeMaxContext(input) {
2344
2345
  const providerConfig = input.runtimeProviderConfig ?? input.config.providers?.[input.providerId];
2345
2346
  const providerOverride = positiveNumber(readConfiguredMaxContext(providerConfig));
2346
2347
  if (providerOverride) return providerOverride;
2347
- const topLevelBaseUrlApplies = input.providerId === input.config.provider;
2348
- const hasCustomBaseUrl = Boolean(
2349
- providerConfig?.baseUrl || topLevelBaseUrlApplies && input.config.baseUrl
2350
- );
2351
- if (input.modelsRegistry && !hasCustomBaseUrl) {
2352
- const caps = await capabilitiesFor(input.modelsRegistry, input.providerId, input.modelId).catch(
2353
- () => void 0
2354
- );
2355
- const catalogMax = positiveNumber(caps?.maxContext);
2356
- if (catalogMax) return catalogMax;
2348
+ const catalogId = providerConfig?.type && providerConfig.type !== input.providerId ? providerConfig.type : input.providerId;
2349
+ if (input.modelsRegistry) {
2350
+ const topLevelBaseUrlApplies = input.providerId === input.config.provider;
2351
+ const configuredBaseUrl = providerConfig?.baseUrl ?? (topLevelBaseUrlApplies ? input.config.baseUrl : void 0);
2352
+ let divergesFromCatalog = false;
2353
+ if (configuredBaseUrl) {
2354
+ const resolved = await safeGetProvider(input.modelsRegistry, catalogId);
2355
+ divergesFromCatalog = normalizeBaseUrl(configuredBaseUrl) !== normalizeBaseUrl(resolved?.apiBase);
2356
+ }
2357
+ if (!divergesFromCatalog) {
2358
+ const mergedModels = mergeCustomModelDefs(providerConfig?.customModels, input.config.models);
2359
+ const caps = await capabilitiesFor(
2360
+ input.modelsRegistry,
2361
+ catalogId,
2362
+ input.modelId,
2363
+ mergedModels
2364
+ ).catch(() => void 0);
2365
+ const catalogMax = positiveNumber(caps?.maxContext);
2366
+ if (catalogMax) return catalogMax;
2367
+ const directModel = await input.modelsRegistry.getModel(catalogId, input.modelId).catch(() => void 0);
2368
+ const directMax = positiveNumber(directModel?.capabilities.maxContext);
2369
+ if (directMax) return directMax;
2370
+ }
2357
2371
  }
2358
2372
  return positiveNumber(input.provider.capabilities.maxContext) ?? 0;
2359
2373
  }
2374
+ async function safeGetProvider(registry, id) {
2375
+ try {
2376
+ return await registry.getProvider(id);
2377
+ } catch {
2378
+ return void 0;
2379
+ }
2380
+ }
2381
+ function normalizeBaseUrl(url) {
2382
+ if (!url) return "";
2383
+ return url.trim().toLowerCase().replace(/\/+$/, "");
2384
+ }
2360
2385
  function readConfiguredMaxContext(providerConfig) {
2361
2386
  if (!providerConfig || typeof providerConfig !== "object") return void 0;
2362
2387
  const capabilities = providerConfig.capabilities;
@@ -2370,6 +2395,7 @@ function positiveNumber(value) {
2370
2395
  // src/arg-parser.ts
2371
2396
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2372
2397
  "yolo",
2398
+ "yolo-destructive",
2373
2399
  "force-all-yolo",
2374
2400
  "verbose",
2375
2401
  "trace",
@@ -2389,13 +2415,15 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2389
2415
  "metrics",
2390
2416
  "webui",
2391
2417
  "no-check",
2418
+ "no-models-refresh",
2392
2419
  "director",
2393
2420
  "no-director",
2394
2421
  "no-autonomy",
2395
2422
  "autonomy",
2396
2423
  "eternal",
2397
2424
  "no-hints",
2398
- "hints"
2425
+ "hints",
2426
+ "no-hooks"
2399
2427
  ]);
2400
2428
  function parseArgs(argv) {
2401
2429
  const flags = {};
@@ -2723,10 +2751,12 @@ function buildAutonomyCommand(opts) {
2723
2751
  " auto \u2014 After each turn, agent picks the best next step and continues.",
2724
2752
  " Runs indefinitely until you press Esc or Ctrl+C.",
2725
2753
  " eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
2726
- " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
2754
+ " Force-enables regular YOLO; destructive-gated calls still use",
2755
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2727
2756
  " parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
2728
2757
  " spawns N agents, awaits results, aggregates. Requires /goal.",
2729
- " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
2758
+ " Force-enables regular YOLO; destructive-gated calls still use",
2759
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2730
2760
  "",
2731
2761
  "Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
2732
2762
  "Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
@@ -2884,7 +2914,7 @@ function buildAutonomyCommand(opts) {
2884
2914
  opts.onEternalStart(newMode);
2885
2915
  const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
2886
2916
  const msg2 = `Autonomy mode: ${modeLabel} \u2014 engine launching against goal: ${color.bold(goal.goal)}
2887
- ${color.dim("YOLO forced ON. Use /autonomy stop to end. Journal at /goal journal.")}`;
2917
+ ${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
2888
2918
  opts.renderer.write(msg2);
2889
2919
  return { message: msg2 };
2890
2920
  }
@@ -3361,6 +3391,11 @@ function buildContextCommand(opts) {
3361
3391
  " /context Show counts: messages, est. tokens, tool calls, todos, read files.",
3362
3392
  " /context detail As above, plus model, cwd, projectRoot, and the file list.",
3363
3393
  " /context repair Repair orphan tool_use/tool_result blocks after manual compaction.",
3394
+ " /context limit Show effective context window for this session.",
3395
+ " /context limit <tokens> Set effective context window for this session (e.g. 220k).",
3396
+ " /context limit <tokens> --persist Persist effective context window to config.",
3397
+ " /context thresholds <warn> <soft> <hard> Set compaction thresholds (percent or decimal).",
3398
+ " /context thresholds <warn> <soft> <hard> --persist Persist thresholds to config.",
3364
3399
  " /context mode List context-window modes.",
3365
3400
  " /context mode <id> Switch context-window mode for this session."
3366
3401
  ].join("\n"),
@@ -3388,6 +3423,83 @@ ${formatContextWindowModeList(active)}`;
3388
3423
  ` empty msgs: removed ${repaired.report.removedMessages}`
3389
3424
  ].join("\n") : "Context repair: no orphan tool_use/tool_result blocks found.";
3390
3425
  opts.renderer.write(`${msg2}
3426
+ `);
3427
+ return { message: msg2 };
3428
+ }
3429
+ if (trimmed === "limit") {
3430
+ const limit = readEffectiveLimit(ctx, opts);
3431
+ const msg2 = limit > 0 ? `Effective context window: ${limit.toLocaleString()} tokens` : "Effective context window: unknown (auto-compaction may be disabled).";
3432
+ opts.renderer.write(`${msg2}
3433
+ `);
3434
+ return { message: msg2 };
3435
+ }
3436
+ if (trimmed.startsWith("limit ")) {
3437
+ const persist = hasPersistFlag(trimmed);
3438
+ const raw = stripPersistFlag(trimmed.slice("limit ".length)).trim();
3439
+ const limit = parseTokenCount(raw);
3440
+ if (!limit) {
3441
+ const msg3 = `Invalid context limit "${raw}". Use a positive token count, e.g. 220k or 220000.`;
3442
+ opts.renderer.write(`${color.red(msg3)}
3443
+ `);
3444
+ return { message: msg3 };
3445
+ }
3446
+ ctx.meta["effectiveMaxContext"] = limit;
3447
+ const effective = opts.onContextLimit?.(limit) ?? limit;
3448
+ if (persist) {
3449
+ const error = await persistContextConfig(opts, { effectiveMaxContext: limit });
3450
+ if (error) {
3451
+ opts.renderer.write(`${color.red(error)}
3452
+ `);
3453
+ return { message: error };
3454
+ }
3455
+ }
3456
+ const msg2 = `${color.green("Effective context window set:")} ${effective.toLocaleString()} tokens${persist ? " (persisted)" : ""}`;
3457
+ opts.renderer.write(`${msg2}
3458
+ `);
3459
+ return { message: msg2 };
3460
+ }
3461
+ if (trimmed.startsWith("thresholds ")) {
3462
+ const persist = hasPersistFlag(trimmed);
3463
+ const thresholdArgs = stripPersistFlag(trimmed.slice("thresholds ".length)).trim();
3464
+ const parts = thresholdArgs.split(/\s+/).filter(Boolean);
3465
+ if (parts.length !== 3) {
3466
+ const msg3 = "Usage: /context thresholds <warn> <soft> <hard> (examples: 60% 75% 90% or 0.6 0.75 0.9)";
3467
+ opts.renderer.write(`${color.red(msg3)}
3468
+ `);
3469
+ return { message: msg3 };
3470
+ }
3471
+ const thresholds = parts.map(parseThreshold);
3472
+ if (thresholds.some((v) => v === null)) {
3473
+ const msg3 = "Invalid thresholds. Use percentages (60%) or decimals between 0 and 1.";
3474
+ opts.renderer.write(`${color.red(msg3)}
3475
+ `);
3476
+ return { message: msg3 };
3477
+ }
3478
+ const [warn, soft, hard] = thresholds;
3479
+ if (!(warn < soft && soft < hard)) {
3480
+ const msg3 = "Invalid thresholds: require warn < soft < hard.";
3481
+ opts.renderer.write(`${color.red(msg3)}
3482
+ `);
3483
+ return { message: msg3 };
3484
+ }
3485
+ const base = readPolicy(ctx) ?? resolveContextWindowPolicy({});
3486
+ const policy2 = { ...base, thresholds: { warn, soft, hard } };
3487
+ ctx.meta["contextWindowMode"] = policy2.id;
3488
+ ctx.meta["contextWindowPolicy"] = policy2;
3489
+ if (persist) {
3490
+ const error = await persistContextConfig(opts, {
3491
+ warnThreshold: warn,
3492
+ softThreshold: soft,
3493
+ hardThreshold: hard
3494
+ });
3495
+ if (error) {
3496
+ opts.renderer.write(`${color.red(error)}
3497
+ `);
3498
+ return { message: error };
3499
+ }
3500
+ }
3501
+ const msg2 = `${color.green("Context thresholds set:")} warn ${pct(warn)}, soft ${pct(soft)}, hard ${pct(hard)}${persist ? " (persisted)" : ""}`;
3502
+ opts.renderer.write(`${msg2}
3391
3503
  `);
3392
3504
  return { message: msg2 };
3393
3505
  }
@@ -3421,6 +3533,7 @@ ${formatContextWindowModeList(active)}`;
3421
3533
  ` messages: ${messages.length} total (${countTurnPairs(messages)} user+assistant pairs)`,
3422
3534
  ` tokens (est): ${estimateTokens(messages).toLocaleString()} (chars / 4 estimate)`,
3423
3535
  ` mode: ${policy ? `${policy.id} (${policy.name})` : "balanced"}`,
3536
+ ` limit: ${formatLimit(readEffectiveLimit(ctx, opts))}`,
3424
3537
  ` system prompt: ${ctx.systemPrompt.length} block${ctx.systemPrompt.length !== 1 ? "s" : ""}`,
3425
3538
  ` tools: ${countToolUses(messages)} calls made, ${countToolResults(messages)} results in history`,
3426
3539
  ` read files: ${ctx.readFiles.size} files`,
@@ -3447,6 +3560,68 @@ function readPolicy(ctx) {
3447
3560
  const policy = ctx.meta?.["contextWindowPolicy"];
3448
3561
  return policy && typeof policy === "object" ? policy : null;
3449
3562
  }
3563
+ function hasPersistFlag(input) {
3564
+ return /(?:^|\s)--persist(?:\s|$)/.test(input);
3565
+ }
3566
+ function stripPersistFlag(input) {
3567
+ return input.replace(/(?:^|\s)--persist(?:\s|$)/g, " ").trim();
3568
+ }
3569
+ async function persistContextConfig(opts, patch) {
3570
+ if (!opts.configStore || !opts.paths) return "Cannot persist context settings: config store not available.";
3571
+ let raw = "{}";
3572
+ try {
3573
+ raw = await fsp3.readFile(opts.paths.globalConfig, "utf8");
3574
+ } catch (err) {
3575
+ if (err.code !== "ENOENT") {
3576
+ return `Could not read ${opts.paths.globalConfig}: ${err.message}`;
3577
+ }
3578
+ }
3579
+ let parsed;
3580
+ try {
3581
+ parsed = JSON.parse(raw);
3582
+ } catch (err) {
3583
+ return `Config at ${opts.paths.globalConfig} is not valid JSON: ${err.message}`;
3584
+ }
3585
+ const current = opts.configStore.get();
3586
+ const context = {
3587
+ ...current.context,
3588
+ ...parsed.context ?? {},
3589
+ ...patch
3590
+ };
3591
+ parsed.context = context;
3592
+ await atomicWrite(opts.paths.globalConfig, JSON.stringify(parsed, null, 2), { mode: 384 });
3593
+ opts.configStore.update({ context });
3594
+ return null;
3595
+ }
3596
+ function readEffectiveLimit(ctx, opts) {
3597
+ const live = opts.onContextLimit?.();
3598
+ if (typeof live === "number" && Number.isFinite(live) && live > 0) return live;
3599
+ const metaLimit = ctx.meta?.["effectiveMaxContext"];
3600
+ if (typeof metaLimit === "number" && Number.isFinite(metaLimit) && metaLimit > 0) return metaLimit;
3601
+ const providerLimit = ctx.provider?.capabilities?.maxContext;
3602
+ return typeof providerLimit === "number" && Number.isFinite(providerLimit) && providerLimit > 0 ? providerLimit : 0;
3603
+ }
3604
+ function parseTokenCount(raw) {
3605
+ const normalized = raw.trim().toLowerCase().replace(/,/g, "").replace(/_/g, "");
3606
+ const match = /^(\d+(?:\.\d+)?)([km])?$/.exec(normalized);
3607
+ if (!match) return null;
3608
+ const value = Number(match[1]);
3609
+ const unit = match[2];
3610
+ const scaled = unit === "m" ? value * 1e6 : unit === "k" ? value * 1e3 : value;
3611
+ const rounded = Math.floor(scaled);
3612
+ return Number.isFinite(rounded) && rounded > 0 ? rounded : null;
3613
+ }
3614
+ function parseThreshold(raw) {
3615
+ const s = raw.trim();
3616
+ const percent = s.endsWith("%");
3617
+ const n = Number((percent ? s.slice(0, -1) : s).trim());
3618
+ if (!Number.isFinite(n)) return null;
3619
+ const value = percent ? n / 100 : n;
3620
+ return value > 0 && value < 1 ? value : null;
3621
+ }
3622
+ function formatLimit(limit) {
3623
+ return limit > 0 ? `${limit.toLocaleString()} tokens` : "unknown";
3624
+ }
3450
3625
  function pct(n) {
3451
3626
  return `${Math.round(n * 100)}%`;
3452
3627
  }
@@ -4855,12 +5030,20 @@ function parseMcpArgs(args) {
4855
5030
  }
4856
5031
  async function runMcpManagementCommand(parsed, deps) {
4857
5032
  const { config, configPath: configPath2, mcpRegistry, allServerPresets } = deps;
4858
- const configured = config.mcpServers ?? {};
5033
+ const diskConfig = await readConfig(configPath2);
5034
+ const configured = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : config.mcpServers ?? {};
4859
5035
  switch (parsed.action) {
4860
5036
  case "list":
4861
5037
  return renderList(configured, mcpRegistry, allServerPresets);
4862
5038
  case "add":
4863
- return runAdd(parsed.name, parsed.enable ?? false, configured, configPath2, allServerPresets);
5039
+ return runAdd(
5040
+ parsed.name,
5041
+ parsed.enable ?? false,
5042
+ configured,
5043
+ configPath2,
5044
+ mcpRegistry,
5045
+ allServerPresets
5046
+ );
4864
5047
  case "remove":
4865
5048
  return runRemove(parsed.name, configured, configPath2, mcpRegistry);
4866
5049
  case "enable":
@@ -4904,34 +5087,46 @@ function renderList(configured, mcpRegistry, all) {
4904
5087
  lines.push(color.dim(" /mcp restart <name> (runtime restart)"));
4905
5088
  return lines.join("\n");
4906
5089
  }
4907
- async function runAdd(name, enable, configured, configPath2, all) {
5090
+ async function runAdd(name, enable, configured, configPath2, mcpRegistry, all) {
4908
5091
  const preset = all[name];
4909
5092
  if (!preset) {
4910
5093
  const known = Object.keys(all).join(", ");
4911
5094
  return `Unknown server "${name}". Available: ${known}`;
4912
5095
  }
4913
- if (configured[name]) {
4914
- const full2 = await readConfig(configPath2);
4915
- full2.mcpServers = {
4916
- ...full2.mcpServers ?? {},
4917
- [name]: { ...preset, ...configured[name], enabled: enable }
4918
- };
4919
- await writeConfig(configPath2, full2);
4920
- return `${color.green("Updated")} "${name}" (${enable ? "enabled" : "disabled"}). Config written.`;
4921
- }
5096
+ const existing = configured[name];
5097
+ const nextCfg = existing ? { ...preset, ...existing, enabled: enable } : { ...preset, enabled: enable };
4922
5098
  const full = await readConfig(configPath2);
4923
- const mcpServers = { ...full.mcpServers ?? {}, [name]: { ...preset, enabled: enable } };
5099
+ const mcpServers = {
5100
+ ...isMcpServerRecord(full.mcpServers) ? full.mcpServers : {},
5101
+ [name]: nextCfg
5102
+ };
4924
5103
  full.mcpServers = mcpServers;
4925
5104
  await writeConfig(configPath2, full);
4926
- const verb = enable ? "Enabled" : "Added (disabled \u2014 /mcp enable to start)";
4927
- return `${color.green(verb)} "${name}" (${preset.transport}). Config written to ${configPath2}.`;
5105
+ if (!enable) {
5106
+ const verb = existing ? "Updated" : "Added (disabled \u2014 /mcp enable to start)";
5107
+ return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
5108
+ }
5109
+ try {
5110
+ if (mcpRegistry.list().some((server) => server.name === name)) {
5111
+ await mcpRegistry.restart(name);
5112
+ } else {
5113
+ await mcpRegistry.start(nextCfg);
5114
+ }
5115
+ const verb = existing ? "Updated and started" : "Enabled and started";
5116
+ return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
5117
+ } catch (err) {
5118
+ const message = err instanceof Error ? err.message : String(err);
5119
+ return `${color.yellow("Enabled")} "${name}" in config, but failed to start: ${message}`;
5120
+ }
4928
5121
  }
4929
5122
  async function runRemove(name, configured, configPath2, mcpRegistry) {
4930
5123
  if (!configured[name]) return `Server "${name}" is not in config.`;
4931
5124
  await mcpRegistry.stop(name).catch(() => {
4932
5125
  });
4933
5126
  const full = await readConfig(configPath2);
4934
- const mcpServers = { ...full.mcpServers ?? {} };
5127
+ const mcpServers = {
5128
+ ...full.mcpServers ?? {}
5129
+ };
4935
5130
  delete mcpServers[name];
4936
5131
  full.mcpServers = mcpServers;
4937
5132
  await writeConfig(configPath2, full);
@@ -4950,7 +5145,9 @@ async function runEnable(name, configured, configPath2, mcpRegistry) {
4950
5145
  }
4951
5146
  }
4952
5147
  const full = await readConfig(configPath2);
4953
- const mcpServers = { ...full.mcpServers ?? {} };
5148
+ const mcpServers = {
5149
+ ...full.mcpServers ?? {}
5150
+ };
4954
5151
  mcpServers[name] = { ...mcpServers[name], enabled: true };
4955
5152
  full.mcpServers = mcpServers;
4956
5153
  await writeConfig(configPath2, full);
@@ -4967,7 +5164,9 @@ async function runDisable(name, configured, configPath2, mcpRegistry) {
4967
5164
  await mcpRegistry.stop(name).catch(() => {
4968
5165
  });
4969
5166
  const full = await readConfig(configPath2);
4970
- const mcpServers = { ...full.mcpServers ?? {} };
5167
+ const mcpServers = {
5168
+ ...full.mcpServers ?? {}
5169
+ };
4971
5170
  mcpServers[name] = { ...mcpServers[name], enabled: false };
4972
5171
  full.mcpServers = mcpServers;
4973
5172
  await writeConfig(configPath2, full);
@@ -5008,6 +5207,9 @@ async function readConfig(path25) {
5008
5207
  return {};
5009
5208
  }
5010
5209
  }
5210
+ function isMcpServerRecord(value) {
5211
+ return !!value && typeof value === "object" && !Array.isArray(value);
5212
+ }
5011
5213
  async function writeConfig(path25, cfg) {
5012
5214
  const raw = JSON.stringify(cfg, null, 2);
5013
5215
  const tmp = path25 + ".tmp";
@@ -5195,6 +5397,224 @@ ${targetMode.description}`
5195
5397
  }
5196
5398
  };
5197
5399
  }
5400
+ var noOpVault = {
5401
+ encrypt: (v) => v,
5402
+ decrypt: (v) => v,
5403
+ isEncrypted: () => false
5404
+ };
5405
+ async function patchGlobalConfig(globalConfigPath, mutate) {
5406
+ let raw = "{}";
5407
+ let fileExists = true;
5408
+ try {
5409
+ raw = await fsp3.readFile(globalConfigPath, "utf8");
5410
+ } catch (err) {
5411
+ if (err.code !== "ENOENT") throw err;
5412
+ fileExists = false;
5413
+ }
5414
+ let parsed;
5415
+ try {
5416
+ parsed = JSON.parse(raw);
5417
+ } catch (err) {
5418
+ if (fileExists) {
5419
+ throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
5420
+ }
5421
+ parsed = {};
5422
+ }
5423
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
5424
+ mutate(decrypted);
5425
+ const encrypted = encryptConfigSecrets$1(decrypted, noOpVault);
5426
+ await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
5427
+ return decrypted;
5428
+ }
5429
+ function fmtModel(id, def) {
5430
+ const parts = [];
5431
+ if (def.provider) parts.push(`${color.dim("provider:")} ${color.cyan(def.provider)}`);
5432
+ if (def.name) parts.push(`${color.dim("name:")} ${def.name}`);
5433
+ const caps = def.capabilities;
5434
+ if (caps) {
5435
+ if (caps.maxContext) parts.push(`${color.dim("maxContext:")} ${color.yellow(String(caps.maxContext))}`);
5436
+ const flags = [];
5437
+ if (caps.tools) flags.push("tools");
5438
+ if (caps.vision) flags.push("vision");
5439
+ if (caps.reasoning) flags.push("reasoning");
5440
+ if (caps.streaming) flags.push("streaming");
5441
+ if (caps.jsonMode) flags.push("json");
5442
+ if (flags.length) parts.push(`${color.dim("caps:")} ${flags.join(", ")}`);
5443
+ }
5444
+ if (def.maxOutput) parts.push(`${color.dim("maxOutput:")} ${color.yellow(String(def.maxOutput))}`);
5445
+ return ` ${color.amber(id)} ${parts.join(" ")}`;
5446
+ }
5447
+ function safeAt(arr, idx) {
5448
+ const v = arr[idx];
5449
+ if (v === void 0) throw new Error(`Missing value at position ${idx}`);
5450
+ return v;
5451
+ }
5452
+ function parseFlags(tokens) {
5453
+ let modelId = "";
5454
+ const caps = {};
5455
+ let provider;
5456
+ let name;
5457
+ let maxOutput;
5458
+ let i = 0;
5459
+ while (i < tokens.length) {
5460
+ const t = tokens[i];
5461
+ if (t.startsWith("--")) {
5462
+ const key = t.slice(2);
5463
+ switch (key) {
5464
+ case "provider":
5465
+ provider = safeAt(tokens, ++i);
5466
+ break;
5467
+ case "name":
5468
+ name = safeAt(tokens, ++i);
5469
+ break;
5470
+ case "max-context":
5471
+ caps.maxContext = Number(safeAt(tokens, ++i));
5472
+ break;
5473
+ case "max-output":
5474
+ maxOutput = Number(safeAt(tokens, ++i));
5475
+ break;
5476
+ case "tools":
5477
+ caps.tools = true;
5478
+ break;
5479
+ case "vision":
5480
+ caps.vision = true;
5481
+ break;
5482
+ case "streaming":
5483
+ caps.streaming = true;
5484
+ break;
5485
+ case "reasoning":
5486
+ caps.reasoning = true;
5487
+ break;
5488
+ case "json-mode":
5489
+ caps.jsonMode = true;
5490
+ break;
5491
+ default:
5492
+ return { modelId: "", error: `Unknown flag: --${key}` };
5493
+ }
5494
+ } else if (!t.startsWith("-") && !modelId) {
5495
+ modelId = t;
5496
+ }
5497
+ i++;
5498
+ }
5499
+ if (!modelId) return { modelId: "", error: "missing model id" };
5500
+ const hasCaps = Object.keys(caps).length > 0;
5501
+ const def = {};
5502
+ if (provider !== void 0) def.provider = provider;
5503
+ if (name !== void 0) def.name = name;
5504
+ if (maxOutput !== void 0) def.maxOutput = maxOutput;
5505
+ if (hasCaps) def.capabilities = caps;
5506
+ return { modelId, def: Object.keys(def).length ? def : void 0 };
5507
+ }
5508
+ function buildModelsCommand(opts) {
5509
+ const help = [
5510
+ "Usage:",
5511
+ " /models List custom model definitions",
5512
+ " /models add <id> [flags] Add or update a custom model",
5513
+ " /models remove <id> Remove a custom model",
5514
+ "",
5515
+ "Flags for add:",
5516
+ " --provider <id> Owning provider",
5517
+ ' --name "Display" Display name',
5518
+ " --max-context <N> Context window override",
5519
+ " --max-output <N> Max output tokens",
5520
+ " --tools Tool-capable",
5521
+ " --vision Vision-capable",
5522
+ " --streaming Streaming support",
5523
+ " --reasoning Reasoning support",
5524
+ " --json-mode JSON mode support",
5525
+ "",
5526
+ "Persisted to ~/.wrongstack/config.json."
5527
+ ].join("\n");
5528
+ return {
5529
+ name: "models",
5530
+ description: "Manage custom model definitions.",
5531
+ help,
5532
+ async run(args) {
5533
+ const parts = args.trim().split(/\s+/).filter(Boolean);
5534
+ const sub = (parts[0] ?? "").toLowerCase();
5535
+ if (sub === "help" || sub === "--help") return { message: help };
5536
+ if (!opts.configStore || !opts.paths) {
5537
+ return { message: `${color.red("Error")} config store not available.` };
5538
+ }
5539
+ const config = opts.configStore.get();
5540
+ const globalConfigPath = opts.paths.globalConfig;
5541
+ if (!sub) {
5542
+ const models = config.models ?? {};
5543
+ const ids = Object.keys(models);
5544
+ if (ids.length === 0) {
5545
+ return {
5546
+ message: [
5547
+ `${color.bold("Custom Models")} ${color.dim("(none defined)")}`,
5548
+ "",
5549
+ color.dim(" Add one: /models add <id> --max-context 128000 --tools")
5550
+ ].join("\n")
5551
+ };
5552
+ }
5553
+ return {
5554
+ message: [
5555
+ `${color.bold("Custom Models")} ${color.dim(`(${ids.length})`)}`,
5556
+ ...ids.sort().map((id) => fmtModel(id, models[id]))
5557
+ ].join("\n")
5558
+ };
5559
+ }
5560
+ try {
5561
+ if (sub === "add") {
5562
+ const { modelId, def, error } = parseFlags(parts.slice(1));
5563
+ if (error) {
5564
+ return { message: `${color.red("Error")}: ${error}. ${color.dim("/models help")}` };
5565
+ }
5566
+ if (!def && !error) {
5567
+ return { message: `${color.amber("Usage:")} /models add <id> [--max-context N] [--tools] ... ${color.dim("/models help")}` };
5568
+ }
5569
+ const existingModels = config.models ?? {};
5570
+ const existed = modelId in existingModels;
5571
+ const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
5572
+ const models = { ...cfg.models ?? {} };
5573
+ models[modelId] = {
5574
+ ...models[modelId],
5575
+ ...def,
5576
+ capabilities: {
5577
+ ...models[modelId]?.capabilities,
5578
+ ...def?.capabilities
5579
+ }
5580
+ };
5581
+ cfg.models = models;
5582
+ });
5583
+ opts.configStore.update({
5584
+ models: decrypted.models
5585
+ });
5586
+ return { message: `${color.green("\u2713")} ${color.amber(modelId)} ${existed ? "updated" : "added"}.` };
5587
+ }
5588
+ if (sub === "remove" || sub === "rm") {
5589
+ const modelId = parts[1];
5590
+ if (!modelId) {
5591
+ return { message: `${color.amber("Usage:")} /models remove <id>` };
5592
+ }
5593
+ const existing = config.models ?? {};
5594
+ if (!(modelId in existing)) {
5595
+ return { message: `${color.amber("Not found")}: custom model "${modelId}" is not defined.` };
5596
+ }
5597
+ const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
5598
+ const models = { ...cfg.models ?? {} };
5599
+ delete models[modelId];
5600
+ cfg.models = models;
5601
+ });
5602
+ opts.configStore.update({
5603
+ models: decrypted.models
5604
+ });
5605
+ return { message: `${color.green("\u2713")} removed ${color.amber(modelId)}` };
5606
+ }
5607
+ return {
5608
+ message: `${color.red("Unknown subcommand")} "${sub}". Try ${color.dim("/models")}, ${color.dim("/models add")}, or ${color.dim("/models help")}.`
5609
+ };
5610
+ } catch (err) {
5611
+ return {
5612
+ message: `${color.red("models error")}: ${err instanceof Error ? err.message : String(err)}`
5613
+ };
5614
+ }
5615
+ }
5616
+ };
5617
+ }
5198
5618
  function buildNextCommand(opts) {
5199
5619
  return {
5200
5620
  name: "next",
@@ -5452,7 +5872,7 @@ function summariseEvent(ev) {
5452
5872
  return color.dim("\u2026");
5453
5873
  }
5454
5874
  }
5455
- var noOpVault = {
5875
+ var noOpVault2 = {
5456
5876
  encrypt: (v) => v,
5457
5877
  decrypt: (v) => v,
5458
5878
  isEncrypted: () => false
@@ -5487,7 +5907,7 @@ function parseTarget(tokens) {
5487
5907
  function fmtEntry(e) {
5488
5908
  return e.provider ? `${e.provider}/${e.model}` : `${e.model} ${color.dim("(leader provider)")}`;
5489
5909
  }
5490
- async function patchGlobalConfig(globalConfigPath, mutate) {
5910
+ async function patchGlobalConfig2(globalConfigPath, mutate) {
5491
5911
  let raw = "{}";
5492
5912
  let fileExists = true;
5493
5913
  try {
@@ -5504,9 +5924,9 @@ async function patchGlobalConfig(globalConfigPath, mutate) {
5504
5924
  throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
5505
5925
  parsed = {};
5506
5926
  }
5507
- const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
5927
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault2);
5508
5928
  mutate(decrypted);
5509
- const encrypted = encryptConfigSecrets$1(decrypted, noOpVault);
5929
+ const encrypted = encryptConfigSecrets$1(decrypted, noOpVault2);
5510
5930
  await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
5511
5931
  return decrypted;
5512
5932
  }
@@ -5597,7 +6017,7 @@ function buildSetModelCommand(opts) {
5597
6017
  message: `${color.red("Provider not available")}: "${provider}". Keyed: ${keyed.join(", ") || "(none)"}. ${color.dim("/setmodel list")}`
5598
6018
  };
5599
6019
  }
5600
- const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
6020
+ const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
5601
6021
  cfg.provider = provider;
5602
6022
  cfg.model = model;
5603
6023
  });
@@ -5628,7 +6048,7 @@ function buildSetModelCommand(opts) {
5628
6048
  message: `${color.red("Provider not available")}: "${parsed.provider}". Keyed: ${keyed.join(", ") || "(none)"}.`
5629
6049
  };
5630
6050
  }
5631
- const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
6051
+ const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
5632
6052
  const matrix = { ...cfg.modelMatrix ?? {} };
5633
6053
  matrix[key] = parsed.provider ? { provider: parsed.provider, model: parsed.model } : { model: parsed.model };
5634
6054
  cfg.modelMatrix = matrix;
@@ -5645,7 +6065,7 @@ function buildSetModelCommand(opts) {
5645
6065
  if (!(key in existing)) {
5646
6066
  return { message: `${color.amber("No matrix entry")} for "${key}".` };
5647
6067
  }
5648
- const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
6068
+ const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
5649
6069
  const matrix = { ...cfg.modelMatrix ?? {} };
5650
6070
  delete matrix[key];
5651
6071
  cfg.modelMatrix = matrix;
@@ -5697,7 +6117,7 @@ async function persistAutonomySetting(deps, mutator) {
5697
6117
  }
5698
6118
 
5699
6119
  // src/slash-commands/settings.ts
5700
- var noOpVault2 = {
6120
+ var noOpVault3 = {
5701
6121
  encrypt: (v) => v,
5702
6122
  decrypt: (v) => v,
5703
6123
  isEncrypted: () => false
@@ -5762,7 +6182,7 @@ function buildSettingsCommand(opts) {
5762
6182
  const persistDeps = {
5763
6183
  configStore: opts.configStore,
5764
6184
  globalConfigPath: opts.paths.globalConfig,
5765
- vault: noOpVault2
6185
+ vault: noOpVault3
5766
6186
  };
5767
6187
  try {
5768
6188
  if (sub === "delay") {
@@ -6110,12 +6530,12 @@ function buildYoloCommand(opts) {
6110
6530
  help: [
6111
6531
  "Usage:",
6112
6532
  " /yolo Show current YOLO status",
6113
- " /yolo on Enable YOLO mode (auto-approve every tool call)",
6533
+ " /yolo on Enable YOLO mode (auto-approve normal project work)",
6114
6534
  " /yolo off Disable YOLO mode (restore permission prompts)",
6115
6535
  " /yolo toggle Toggle YOLO mode",
6116
6536
  "",
6117
- "YOLO mode skips all permission prompts and auto-approves every tool call.",
6118
- "Use with caution \u2014 the agent can execute any tool without asking."
6537
+ "YOLO mode auto-approves normal in-project tool calls, including simple shell commands.",
6538
+ "Clearly destructive calls may still ask unless --yolo-destructive is enabled."
6119
6539
  ].join("\n"),
6120
6540
  async run(args) {
6121
6541
  const arg = args.trim().toLowerCase();
@@ -6126,7 +6546,7 @@ function buildYoloCommand(opts) {
6126
6546
  }
6127
6547
  if (!arg) {
6128
6548
  const current = opts.onYolo();
6129
- const status = current ? `${color.yellow("ON")} ${color.dim("(auto-approving all tool calls)")}` : `${color.green("OFF")} ${color.dim("(permission prompts active)")}`;
6549
+ const status = current ? `${color.yellow("ON")} ${color.dim("(auto-approving normal project work)")}` : `${color.green("OFF")} ${color.dim("(permission prompts active)")}`;
6130
6550
  const msg2 = `YOLO mode: ${status}`;
6131
6551
  opts.renderer.write(msg2);
6132
6552
  return { message: msg2 };
@@ -6144,7 +6564,7 @@ function buildYoloCommand(opts) {
6144
6564
  return { message: msg2 };
6145
6565
  }
6146
6566
  opts.onYolo(newState);
6147
- const label = newState ? `${color.yellow("ENABLED")} \u2014 all tool calls will be auto-approved` : `${color.green("DISABLED")} \u2014 permission prompts are active`;
6567
+ const label = newState ? `${color.yellow("ENABLED")} \u2014 normal project tool calls will be auto-approved` : `${color.green("DISABLED")} \u2014 permission prompts are active`;
6148
6568
  const msg = `YOLO mode: ${label}`;
6149
6569
  opts.renderer.write(msg);
6150
6570
  return { message: msg };
@@ -6186,6 +6606,7 @@ function buildBuiltinSlashCommands(opts) {
6186
6606
  buildWorktreeCommand(opts),
6187
6607
  buildSettingsCommand(opts),
6188
6608
  buildSetModelCommand(opts),
6609
+ buildModelsCommand(opts),
6189
6610
  buildCollabCommand(opts),
6190
6611
  buildStatuslineCommand({
6191
6612
  cwd: opts.cwd,
@@ -6350,7 +6771,7 @@ async function runLaunchPrompts(opts) {
6350
6771
  yolo = yoloPinned;
6351
6772
  } else {
6352
6773
  const answer = (await reader.readLine(
6353
- ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n/q]")} `
6774
+ ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve normal project work)")} ${color.dim("[Y/n/q]")} `
6354
6775
  )).trim().toLowerCase();
6355
6776
  if (answer === "q") {
6356
6777
  renderer.write(color.dim(" Goodbye!\n"));
@@ -6907,11 +7328,11 @@ async function restoreLast(homeFn = defaultHomeDir) {
6907
7328
  var theme = { primary: color.amber };
6908
7329
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
6909
7330
  try {
6910
- const { atomicWrite: atomicWrite10 } = await import('@wrongstack/core');
6911
- const fs22 = await import('fs/promises');
7331
+ const { atomicWrite: atomicWrite13 } = await import('@wrongstack/core');
7332
+ const fs25 = await import('fs/promises');
6912
7333
  let existing = {};
6913
7334
  try {
6914
- const raw = await fs22.readFile(configPath2, "utf8");
7335
+ const raw = await fs25.readFile(configPath2, "utf8");
6915
7336
  existing = JSON.parse(raw);
6916
7337
  } catch {
6917
7338
  }
@@ -6919,7 +7340,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
6919
7340
  existing.provider = provider;
6920
7341
  existing.model = model;
6921
7342
  await backupCurrent(homeFn);
6922
- await atomicWrite10(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7343
+ await atomicWrite13(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
6923
7344
  try {
6924
7345
  await appendHistory(
6925
7346
  oldCfg,
@@ -7209,7 +7630,7 @@ var GROUPS = [
7209
7630
  items: [
7210
7631
  { key: "/mode", blurb: "switch persona: code-reviewer, debugger, architect, tester, devops, \u2026" },
7211
7632
  { key: "/model", blurb: "two-step provider \u2192 model picker, hot-swap at runtime" },
7212
- { key: "/yolo on|off|toggle", blurb: "auto-approve every tool call without restart" },
7633
+ { key: "/yolo on|off|toggle", blurb: "auto-approve normal project work without restart" },
7213
7634
  { key: "/context mode frugal|balanced|deep|archival", blurb: "pick how aggressively history is trimmed" },
7214
7635
  { key: "/compact", blurb: "manually compact the in-flight context window" },
7215
7636
  { key: "/plan show|add|start|done", blurb: "strategic roadmap, survives /resume across sessions" }
@@ -7242,12 +7663,12 @@ function pickGroupIndex(opts) {
7242
7663
  try {
7243
7664
  let current = 0;
7244
7665
  try {
7245
- const parsed = Number.parseInt(fs10.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7666
+ const parsed = Number.parseInt(fs12.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7246
7667
  if (Number.isFinite(parsed)) current = wrap(parsed);
7247
7668
  } catch {
7248
7669
  }
7249
- fs10.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7250
- fs10.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7670
+ fs12.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7671
+ fs12.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7251
7672
  return current;
7252
7673
  } catch {
7253
7674
  }
@@ -8785,9 +9206,160 @@ var initCmd = async (_args, deps) => {
8785
9206
  deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
8786
9207
  return 0;
8787
9208
  };
9209
+ var AllowAllPermissionPolicy = class extends AutoApprovePermissionPolicy {
9210
+ async evaluate() {
9211
+ return { permission: "auto", source: "default" };
9212
+ }
9213
+ };
9214
+ function parseToolsFlag(flags) {
9215
+ const raw = flags["tools"];
9216
+ if (typeof raw !== "string") return null;
9217
+ const set = new Set(
9218
+ raw.split(",").map((s) => s.trim()).filter(Boolean)
9219
+ );
9220
+ return set.size > 0 ? set : null;
9221
+ }
9222
+ function makeServeContext(cwd, projectRoot, signal) {
9223
+ const provider = {
9224
+ id: "mcp-serve",
9225
+ capabilities: { maxContext: 0 },
9226
+ complete: async () => {
9227
+ throw new Error("no model provider in `mcp serve` mode");
9228
+ },
9229
+ stream: () => {
9230
+ throw new Error("no model provider in `mcp serve` mode");
9231
+ }
9232
+ };
9233
+ const session = { append: async () => {
9234
+ } };
9235
+ const tokenCounter = {
9236
+ account: () => {
9237
+ },
9238
+ total: () => ({ input: 0, output: 0 }),
9239
+ estimateCost: () => ({ total: 0 })
9240
+ };
9241
+ return new Context({
9242
+ systemPrompt: [],
9243
+ provider,
9244
+ session,
9245
+ signal,
9246
+ tokenCounter,
9247
+ cwd,
9248
+ projectRoot,
9249
+ model: "mcp-serve",
9250
+ tools: []
9251
+ });
9252
+ }
9253
+ async function selectExposedTools(registry, ctx, policy, whitelist) {
9254
+ const allowed = [];
9255
+ for (const tool of registry.list()) {
9256
+ if (whitelist && !whitelist.has(tool.name)) continue;
9257
+ const decision = await policy.evaluate(tool, {}, ctx);
9258
+ if (decision.permission === "auto") allowed.push(tool);
9259
+ }
9260
+ return allowed;
9261
+ }
9262
+ async function serveMcpStdio(deps) {
9263
+ const flags = deps.flags ?? {};
9264
+ const yolo = flags["yolo"] === true || flags["allow-all"] === true;
9265
+ const whitelist = parseToolsFlag(flags);
9266
+ const log = (m) => process.stderr.write(`${m}
9267
+ `);
9268
+ let registry = deps.toolRegistry;
9269
+ if (!registry) {
9270
+ registry = new ToolRegistry();
9271
+ registry.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
9272
+ }
9273
+ const controller = new AbortController();
9274
+ const ctx = makeServeContext(deps.cwd, deps.projectRoot, controller.signal);
9275
+ const permissionPolicy = yolo ? new AllowAllPermissionPolicy() : new AutoApprovePermissionPolicy();
9276
+ const executor = new ToolExecutor(registry, {
9277
+ permissionPolicy,
9278
+ secretScrubber: new DefaultSecretScrubber(),
9279
+ perIterationOutputCapBytes: 1e6
9280
+ });
9281
+ const allowed = await selectExposedTools(registry, ctx, permissionPolicy, whitelist);
9282
+ const allowedNames = new Set(allowed.map((t) => t.name));
9283
+ if (allowed.length === 0) {
9284
+ log(
9285
+ "wrongstack MCP server: no tools to expose (all withheld by policy or filtered out). Pass --yolo to expose write/exec tools, or --tools <names> to whitelist."
9286
+ );
9287
+ }
9288
+ let counter = 0;
9289
+ const host = {
9290
+ listTools: () => allowed.map((t) => ({
9291
+ name: t.name,
9292
+ description: t.description,
9293
+ inputSchema: t.inputSchema ?? { type: "object" }
9294
+ })),
9295
+ callTool: async (name, callArgs) => {
9296
+ if (!allowedNames.has(name)) {
9297
+ return { content: `Tool "${name}" is not exposed by this server`, isError: true };
9298
+ }
9299
+ const use = {
9300
+ type: "tool_use",
9301
+ id: `srv_${++counter}`,
9302
+ name,
9303
+ input: callArgs
9304
+ };
9305
+ const batch = await executor.executeBatch([use], ctx, "sequential");
9306
+ const result = batch.outputs[0]?.result;
9307
+ if (!result || result.type === "tool_confirm_pending") {
9308
+ return {
9309
+ content: `Tool "${name}" requires interactive confirmation, which is unavailable over MCP`,
9310
+ isError: true
9311
+ };
9312
+ }
9313
+ return { content: result.content, isError: Boolean(result.is_error) };
9314
+ }
9315
+ };
9316
+ const server = new MCPServer({
9317
+ host,
9318
+ logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
9319
+ });
9320
+ const mode = yolo ? "yolo: all tools" : "safe: read-only tools";
9321
+ if (flags["http"] === true || typeof flags["http"] === "string" || flags["port"] || flags["host"]) {
9322
+ const port = Number(flags["port"] ?? flags["http"] ?? 0) || 0;
9323
+ const httpHost = typeof flags["host"] === "string" ? flags["host"] : "127.0.0.1";
9324
+ const token = typeof flags["token"] === "string" ? flags["token"] : void 0;
9325
+ let handle2;
9326
+ try {
9327
+ handle2 = await serveHttp(server, {
9328
+ port,
9329
+ host: httpHost,
9330
+ token,
9331
+ logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
9332
+ });
9333
+ } catch (err) {
9334
+ log(`wrongstack MCP server: ${err instanceof Error ? err.message : String(err)}`);
9335
+ return 1;
9336
+ }
9337
+ log(
9338
+ `wrongstack MCP server ready at ${handle2.url} \u2014 exposing ${allowed.length} tool(s) (${mode})${token ? " [token auth]" : ""}.`
9339
+ );
9340
+ await new Promise((resolve3) => {
9341
+ const stop = () => resolve3();
9342
+ process.once("SIGINT", stop);
9343
+ process.once("SIGTERM", stop);
9344
+ });
9345
+ await handle2.close();
9346
+ controller.abort();
9347
+ return 0;
9348
+ }
9349
+ log(`wrongstack MCP server ready on stdio \u2014 exposing ${allowed.length} tool(s) (${mode}).`);
9350
+ const handle = serveStdio(server);
9351
+ await handle.done;
9352
+ controller.abort();
9353
+ return 0;
9354
+ }
9355
+
9356
+ // src/subcommands/handlers/mcp.ts
8788
9357
  var BUILT_IN_MCP = allServers();
8789
9358
  var mcpCmd = async (args, deps) => {
8790
9359
  const sub = args[0];
9360
+ if (sub === "serve") {
9361
+ return serveMcpStdio(deps);
9362
+ }
8791
9363
  if (!sub || sub === "list") {
8792
9364
  const servers = deps.config.mcpServers ?? {};
8793
9365
  if (Object.keys(servers).length === 0) {
@@ -9154,8 +9726,49 @@ ${color.dim(`Current: ${deps.config.provider ?? "<unset>"} / ${deps.config.model
9154
9726
  return 1;
9155
9727
  }
9156
9728
  };
9729
+ function parseFlags2(args) {
9730
+ const flags = {};
9731
+ for (let i = 0; i < args.length; i++) {
9732
+ const a = args[i];
9733
+ if (a.startsWith("--")) {
9734
+ const eq = a.indexOf("=");
9735
+ if (eq !== -1) {
9736
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
9737
+ } else {
9738
+ const name = a.slice(2);
9739
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
9740
+ flags[name] = args[++i] ?? "";
9741
+ } else {
9742
+ flags[name] = true;
9743
+ }
9744
+ }
9745
+ }
9746
+ }
9747
+ return flags;
9748
+ }
9749
+ function positionals(args) {
9750
+ const out = [];
9751
+ for (let i = 0; i < args.length; i++) {
9752
+ const a = args[i];
9753
+ if (a.startsWith("--")) {
9754
+ const eq = a.indexOf("=");
9755
+ if (eq === -1) {
9756
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
9757
+ i++;
9758
+ }
9759
+ }
9760
+ continue;
9761
+ }
9762
+ out.push(a);
9763
+ }
9764
+ return out;
9765
+ }
9766
+ var DEFAULT_PER_PAGE = 15;
9157
9767
  var modelsCmd = async (args, deps) => {
9158
9768
  const sub = args[0];
9769
+ if (sub === "add") return modelsAdd(args.slice(1), deps);
9770
+ if (sub === "remove") return modelsRemove(args.slice(1), deps);
9771
+ if (sub === "list") return modelsList(args.slice(1), deps);
9159
9772
  if (sub === "refresh") {
9160
9773
  deps.renderer.writeInfo("Refreshing models.dev cache\u2026");
9161
9774
  try {
@@ -9169,9 +9782,13 @@ var modelsCmd = async (args, deps) => {
9169
9782
  return 1;
9170
9783
  }
9171
9784
  }
9172
- const providerId = sub ?? deps.config.provider;
9785
+ const flags = parseFlags2(args);
9786
+ const search = typeof flags["search"] === "string" ? flags["search"].toLowerCase() : "";
9787
+ const perPage = Number(flags["per-page"]) > 0 ? Number(flags["per-page"]) : DEFAULT_PER_PAGE;
9788
+ const page = Math.max(1, Number(flags["page"]) || 1);
9789
+ const providerId = sub ?? deps.config.provider ?? "";
9173
9790
  if (!providerId) {
9174
- deps.renderer.writeError("Usage: wstack models <provider> | refresh");
9791
+ deps.renderer.writeError("Usage: wstack models <provider> [--search <term>] [--page N] [--per-page N]");
9175
9792
  return 1;
9176
9793
  }
9177
9794
  let lookupId = providerId;
@@ -9195,34 +9812,209 @@ var modelsCmd = async (args, deps) => {
9195
9812
  `));
9196
9813
  const userModels = deps.config.providers?.[providerId]?.models;
9197
9814
  const catalogById = new Map(provider.models.map((m) => [m.id, m]));
9198
- const sorted = userModels && userModels.length > 0 ? userModels.map((id) => catalogById.get(id) ?? { id, name: id }) : [...provider.models].sort(
9815
+ const allSorted = userModels && userModels.length > 0 ? userModels.map((id) => catalogById.get(id) ?? { id, name: id }) : [...provider.models].sort(
9199
9816
  (a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
9200
9817
  );
9201
9818
  if (userModels && userModels.length > 0)
9202
9819
  deps.renderer.write(color.dim(`(${userModels.length} model(s) from your saved config)
9203
9820
  `));
9204
- for (const m of sorted) {
9205
- const caps = [];
9206
- if ("tool_call" in m && m.tool_call) caps.push("tools");
9207
- if ("reasoning" in m && m.reasoning) caps.push("reasoning");
9208
- if ("modalities" in m && m.modalities?.input?.includes("image")) caps.push("vision");
9209
- const ctx = "limit" in m && m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
9210
- const cost = "cost" in m && m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
9211
- deps.renderer.write(
9212
- ` ${m.id.padEnd(40)} ${color.dim(ctx.padStart(6))} ${color.dim(cost.padEnd(14))} ${color.dim(caps.join(","))}
9821
+ const filtered = search ? allSorted.filter((m) => m.id.toLowerCase().includes(search)) : allSorted;
9822
+ const total = filtered.length;
9823
+ const totalPages = Math.max(1, Math.ceil(total / perPage));
9824
+ const actualPage = Math.min(page, totalPages);
9825
+ const start = (actualPage - 1) * perPage;
9826
+ const pageItems = filtered.slice(start, start + perPage);
9827
+ const end = Math.min(start + pageItems.length, total);
9828
+ const pageHint = totalPages > 1 ? color.cyan(`[page ${actualPage}/${totalPages}]`) : "";
9829
+ const searchHint = search ? color.yellow(` (filtered: "${search}" \u2014 ${total} match${total === 1 ? "" : "es"})`) : color.dim(` (${total} model${total === 1 ? "" : "s"})`);
9830
+ deps.renderer.write(`${pageHint}${searchHint}
9831
+ `);
9832
+ if (pageItems.length === 0) {
9833
+ deps.renderer.write(color.dim("(no models match)\n"));
9834
+ } else {
9835
+ if (start > 0)
9836
+ deps.renderer.write(color.dim(` ${String.fromCharCode(8593)} ${start} above
9837
+ `));
9838
+ for (const m of pageItems) {
9839
+ const caps = [];
9840
+ if ("tool_call" in m && m.tool_call) caps.push("tools");
9841
+ if ("reasoning" in m && m.reasoning) caps.push("reasoning");
9842
+ if ("modalities" in m && m.modalities?.input?.includes("image")) caps.push("vision");
9843
+ const ctx = "limit" in m && m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
9844
+ const cost = "cost" in m && m.cost?.input !== void 0 ? `${m.cost.input}/${m.cost.output ?? "?"}` : "";
9845
+ deps.renderer.write(
9846
+ ` ${m.id.padEnd(40)} ${color.dim(ctx.padStart(6))} ${color.dim(cost.padEnd(14))} ${color.dim(caps.join(","))}
9213
9847
  `
9214
- );
9848
+ );
9849
+ }
9850
+ if (end < total)
9851
+ deps.renderer.write(color.dim(` ${String.fromCharCode(8595)} ${total - end} below
9852
+ `));
9215
9853
  }
9854
+ const navLines = [];
9855
+ if (totalPages > 1) {
9856
+ if (actualPage > 1) navLines.push(`--page ${actualPage - 1} (prev)`);
9857
+ if (actualPage < totalPages) navLines.push(`--page ${actualPage + 1} (next)`);
9858
+ }
9859
+ navLines.push("--search <term> (filter)");
9860
+ deps.renderer.write(color.dim(`
9861
+ ${navLines.join(" \xB7 ")}
9862
+ `));
9216
9863
  const age = await deps.modelsRegistry.ageSeconds();
9217
9864
  deps.renderer.write(
9218
9865
  color.dim(
9219
- `
9220
- Cache age: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never fetched"}. Run \`wstack models refresh\` to update.
9866
+ `Cache age: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never fetched"}. Run \`wstack models refresh\` to update.
9221
9867
  `
9222
9868
  )
9223
9869
  );
9224
9870
  return 0;
9225
9871
  };
9872
+ async function mutateModelsConfig(deps, mutator) {
9873
+ const vault = deps.vault;
9874
+ const configPath2 = deps.paths.globalConfig;
9875
+ let fileExists = true;
9876
+ let raw;
9877
+ try {
9878
+ raw = await fsp3.readFile(configPath2, "utf8");
9879
+ } catch (err) {
9880
+ if (err.code !== "ENOENT") throw err;
9881
+ fileExists = false;
9882
+ raw = "{}";
9883
+ }
9884
+ let parsed;
9885
+ try {
9886
+ parsed = JSON.parse(raw);
9887
+ } catch (err) {
9888
+ if (fileExists) {
9889
+ throw new Error(
9890
+ `Refusing to overwrite corrupt config at ${configPath2} (${err.message}).`
9891
+ );
9892
+ }
9893
+ parsed = {};
9894
+ }
9895
+ const decrypted = decryptConfigSecrets$1(parsed, vault);
9896
+ const models = decrypted.models ?? {};
9897
+ mutator(models);
9898
+ decrypted.models = models;
9899
+ const encrypted = encryptConfigSecrets$1(decrypted, vault);
9900
+ await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
9901
+ }
9902
+ function parseSizeFlag(raw) {
9903
+ if (!raw) return void 0;
9904
+ const s = raw.trim().toLowerCase();
9905
+ const match = /^(\d+(?:\.\d+)?)\s*(k|m|b)?$/.exec(s);
9906
+ if (!match) return void 0;
9907
+ const num = Number.parseFloat(match[1]);
9908
+ const unit = match[2];
9909
+ if (unit === "b") return Math.round(num * 1e9);
9910
+ if (unit === "m") return Math.round(num * 1e6);
9911
+ if (unit === "k") return Math.round(num * 1e3);
9912
+ return Math.round(num);
9913
+ }
9914
+ function parseBoolFlag(flags, key) {
9915
+ if (flags[key] === true || flags[key] === "true") return true;
9916
+ if (flags[`no-${key}`] !== void 0) return false;
9917
+ return void 0;
9918
+ }
9919
+ async function modelsAdd(args, deps) {
9920
+ const flags = parseFlags2(args);
9921
+ const pos = positionals(args);
9922
+ const modelId = pos[0];
9923
+ if (!modelId) {
9924
+ deps.renderer.writeError(
9925
+ "Usage: wstack models add <modelId> [--provider <id>] [--name <name>] [--max-context <N>] [--max-output <N>] [--tools] [--no-tools] [--vision] [--no-vision] [--reasoning] [--streaming] [--no-streaming] [--json-mode]"
9926
+ );
9927
+ return 1;
9928
+ }
9929
+ const existing = deps.config.models?.[modelId];
9930
+ if (existing) {
9931
+ deps.renderer.writeWarning(
9932
+ `Model "${modelId}" already defined. Overwriting.`
9933
+ );
9934
+ }
9935
+ const capabilities = {};
9936
+ const toolsVal = parseBoolFlag(flags, "tools");
9937
+ if (toolsVal !== void 0) capabilities.tools = toolsVal;
9938
+ const visionVal = parseBoolFlag(flags, "vision");
9939
+ if (visionVal !== void 0) capabilities.vision = visionVal;
9940
+ const streamingVal = parseBoolFlag(flags, "streaming");
9941
+ if (streamingVal !== void 0) capabilities.streaming = streamingVal;
9942
+ const reasoningVal = parseBoolFlag(flags, "reasoning");
9943
+ if (reasoningVal !== void 0) capabilities.reasoning = reasoningVal;
9944
+ const jsonModeVal = parseBoolFlag(flags, "json-mode");
9945
+ if (jsonModeVal !== void 0) capabilities.jsonMode = jsonModeVal;
9946
+ const maxContextRaw = typeof flags["max-context"] === "string" ? flags["max-context"] : void 0;
9947
+ const maxContext = parseSizeFlag(maxContextRaw);
9948
+ if (maxContext !== void 0) capabilities.maxContext = maxContext;
9949
+ const def = {};
9950
+ const nameFlag = typeof flags["name"] === "string" ? flags["name"] : void 0;
9951
+ const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
9952
+ if (nameFlag) def.name = nameFlag;
9953
+ if (providerFlag) def.provider = providerFlag;
9954
+ if (Object.keys(capabilities).length > 0) def.capabilities = capabilities;
9955
+ const maxOutputRaw = typeof flags["max-output"] === "string" ? flags["max-output"] : void 0;
9956
+ const maxOutput = parseSizeFlag(maxOutputRaw);
9957
+ if (maxOutput !== void 0) def.maxOutput = maxOutput;
9958
+ await mutateModelsConfig(deps, (models) => {
9959
+ models[modelId] = def;
9960
+ });
9961
+ deps.renderer.writeInfo(`Custom model "${modelId}" ${existing ? "updated" : "added"}.`);
9962
+ const capLines = [];
9963
+ if (def.capabilities) {
9964
+ for (const [k, v] of Object.entries(def.capabilities)) {
9965
+ capLines.push(` ${k}: ${v}`);
9966
+ }
9967
+ }
9968
+ if (def.maxOutput !== void 0) capLines.push(` maxOutput: ${def.maxOutput}`);
9969
+ if (capLines.length > 0) {
9970
+ deps.renderer.write(color.dim(capLines.join("\n") + "\n"));
9971
+ }
9972
+ return 0;
9973
+ }
9974
+ async function modelsRemove(args, deps) {
9975
+ const modelId = args[0];
9976
+ if (!modelId) {
9977
+ deps.renderer.writeError("Usage: wstack models remove <modelId>");
9978
+ return 1;
9979
+ }
9980
+ const existing = deps.config.models?.[modelId];
9981
+ if (!existing) {
9982
+ deps.renderer.writeError(`No custom model "${modelId}" found.`);
9983
+ return 1;
9984
+ }
9985
+ await mutateModelsConfig(deps, (models) => {
9986
+ delete models[modelId];
9987
+ });
9988
+ deps.renderer.writeInfo(`Removed custom model "${modelId}".`);
9989
+ return 0;
9990
+ }
9991
+ async function modelsList(_args, deps) {
9992
+ const models = deps.config.models ?? {};
9993
+ const entries = Object.entries(models);
9994
+ if (entries.length === 0) {
9995
+ deps.renderer.write(color.dim("No custom models defined.\n"));
9996
+ deps.renderer.write(color.dim("Use `wstack models add <modelId> --max-context 128k --tools`\n"));
9997
+ return 0;
9998
+ }
9999
+ deps.renderer.write(color.bold("Custom models\n"));
10000
+ for (const [id, def] of entries.sort(([a], [b]) => a.localeCompare(b))) {
10001
+ const label = def.name ?? id;
10002
+ const provider = def.provider ? ` ${color.dim(`(${def.provider})`)}` : "";
10003
+ deps.renderer.write(` ${color.bold(label)}${provider}
10004
+ `);
10005
+ if (def.capabilities) {
10006
+ for (const [k, v] of Object.entries(def.capabilities)) {
10007
+ deps.renderer.write(` ${color.dim(`${k}:`)} ${v}
10008
+ `);
10009
+ }
10010
+ }
10011
+ if (def.maxOutput !== void 0) {
10012
+ deps.renderer.write(` ${color.dim("maxOutput:")} ${def.maxOutput}
10013
+ `);
10014
+ }
10015
+ }
10016
+ return 0;
10017
+ }
9226
10018
  function redactKeys(obj) {
9227
10019
  if (!obj || typeof obj !== "object") return obj;
9228
10020
  if (Array.isArray(obj)) return obj.map(redactKeys);
@@ -9815,10 +10607,10 @@ var auditCmd = async (args, deps) => {
9815
10607
  return verify.ok ? 0 : 1;
9816
10608
  };
9817
10609
  async function listAudits(log, dir, deps) {
9818
- const fs22 = await import('fs/promises');
10610
+ const fs25 = await import('fs/promises');
9819
10611
  let entries;
9820
10612
  try {
9821
- entries = await fs22.readdir(dir);
10613
+ entries = await fs25.readdir(dir);
9822
10614
  } catch {
9823
10615
  deps.renderer.write(
9824
10616
  color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
@@ -9895,6 +10687,9 @@ var helpCmd = async (_args, deps) => {
9895
10687
  " wstack providers [--all] List providers from models.dev",
9896
10688
  " wstack models [<provider>] List models",
9897
10689
  " wstack models refresh Force-refresh cache",
10690
+ " wstack models add <mid> Add/override custom model (--max-context, --tools, --vision, \u2026)",
10691
+ " wstack models remove <mid> Remove a custom model",
10692
+ " wstack models list List all custom models",
9898
10693
  " wstack mcp [list] List MCP servers",
9899
10694
  " wstack plugin [list|status|official|install|add|remove|enable|disable] Manage plugins",
9900
10695
  " wstack projects List tracked projects",
@@ -9902,7 +10697,15 @@ var helpCmd = async (_args, deps) => {
9902
10697
  " wstack doctor Health checks",
9903
10698
  " wstack export <id> [opts] Render a session",
9904
10699
  " wstack usage Token + cost summary",
9905
- " wstack version Print version"
10700
+ " wstack version Print version",
10701
+ "",
10702
+ color.bold("Common flags"),
10703
+ " --yolo Auto-approve normal in-project tool calls",
10704
+ " --yolo-destructive Also auto-approve clearly destructive YOLO-gated calls",
10705
+ " --force-all-yolo Deprecated alias for --yolo-destructive",
10706
+ " --tui / --no-tui Force or disable TUI mode",
10707
+ ' --eternal "<mission>" Start an eternal-autonomy loop',
10708
+ " --no-hints Hide launch hints"
9906
10709
  ];
9907
10710
  deps.renderer.write(lines.join("\n") + "\n");
9908
10711
  return 0;
@@ -9955,22 +10758,22 @@ function fmtDuration(ms) {
9955
10758
  const remMin = m - h * 60;
9956
10759
  return `${h}h${remMin}m`;
9957
10760
  }
9958
- function fmtTaskResultLine(r, color45) {
10761
+ function fmtTaskResultLine(r, color46) {
9959
10762
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
9960
10763
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
9961
10764
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
9962
10765
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
9963
- const errKindChip = errKind ? color45.dim(` [${errKind}]`) : "";
9964
- const errSnip = errMsg || errKind ? `${errKindChip}${color45.dim(errTail)}` : "";
10766
+ const errKindChip = errKind ? color46.dim(` [${errKind}]`) : "";
10767
+ const errSnip = errMsg || errKind ? `${errKindChip}${color46.dim(errTail)}` : "";
9965
10768
  switch (r.status) {
9966
10769
  case "success":
9967
- return { mark: color45.green("\u2713"), stats, tail: "" };
10770
+ return { mark: color46.green("\u2713"), stats, tail: "" };
9968
10771
  case "timeout":
9969
- return { mark: color45.yellow("\u23F1"), stats: `${color45.yellow("timeout")} ${stats}`, tail: errSnip };
10772
+ return { mark: color46.yellow("\u23F1"), stats: `${color46.yellow("timeout")} ${stats}`, tail: errSnip };
9970
10773
  case "stopped":
9971
- return { mark: color45.dim("\u2298"), stats: `${color45.dim("stopped")} ${stats}`, tail: errSnip };
10774
+ return { mark: color46.dim("\u2298"), stats: `${color46.dim("stopped")} ${stats}`, tail: errSnip };
9972
10775
  case "failed":
9973
- return { mark: color45.red("\u2717"), stats: `${color45.red("failed")} ${stats}`, tail: errSnip };
10776
+ return { mark: color46.red("\u2717"), stats: `${color46.red("failed")} ${stats}`, tail: errSnip };
9974
10777
  }
9975
10778
  }
9976
10779
 
@@ -10038,6 +10841,15 @@ async function boot(argv) {
10038
10841
  }).catch(() => {
10039
10842
  });
10040
10843
  }
10844
+ if (!flags["no-models-refresh"]) {
10845
+ try {
10846
+ await modelsRegistry.refresh();
10847
+ logger.info("models.dev catalog refreshed");
10848
+ } catch (err) {
10849
+ const msg = err instanceof Error ? err.message : String(err);
10850
+ logger.warn(`models.dev refresh failed (${msg}); using cached catalog`);
10851
+ }
10852
+ }
10041
10853
  const first = positional[0];
10042
10854
  if (first && subcommands[first]) {
10043
10855
  const container = createDefaultContainer({
@@ -10066,7 +10878,8 @@ async function boot(argv) {
10066
10878
  vault,
10067
10879
  cwd,
10068
10880
  projectRoot,
10069
- userHome
10881
+ userHome,
10882
+ flags
10070
10883
  });
10071
10884
  await reader.close();
10072
10885
  return code;
@@ -10184,6 +10997,20 @@ async function boot(argv) {
10184
10997
  updateInfo
10185
10998
  };
10186
10999
  }
11000
+ var CONTEXT_OVERFLOW_RE = /context window|exceeds the context|too many tokens|context.*tokens/i;
11001
+ function contextOverflowHint(err) {
11002
+ const structured = err.code === ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW || err.code === ERROR_CODES.AGENT_CONTEXT_OVERFLOW;
11003
+ const textual = CONTEXT_OVERFLOW_RE.test(`${err.message}
11004
+ ${err.describe()}`);
11005
+ if (!structured && !textual) return null;
11006
+ return [
11007
+ "Provider rejected the request as over its effective context window.",
11008
+ "If you use a custom baseUrl/proxy, the real limit may be lower than models.dev reports.",
11009
+ "Try: /context limit 220k",
11010
+ "Then, if needed: /context thresholds 50% 70% 85%",
11011
+ "Persistent config: set context.effectiveMaxContext."
11012
+ ].join("\n");
11013
+ }
10187
11014
  function fmtElapsed(ms) {
10188
11015
  const s = Math.floor(ms / 1e3);
10189
11016
  if (s < 60) return `${s}s`;
@@ -10775,6 +11602,8 @@ ${taskList}`;
10775
11602
  if (err) {
10776
11603
  const tag = err.recoverable ? " (recoverable)" : "";
10777
11604
  opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
11605
+ const hint = contextOverflowHint(err);
11606
+ if (hint) opts.renderer.writeWarning(hint);
10778
11607
  } else {
10779
11608
  opts.renderer.writeError("Failed.");
10780
11609
  }
@@ -11139,7 +11968,17 @@ async function execute(deps) {
11139
11968
  const visionAdapters = () => createToolVisionAdapters(agent.tools);
11140
11969
  const supportsVision = async () => {
11141
11970
  try {
11142
- const caps = await capabilitiesFor(modelsRegistry, context.provider.id, context.model);
11971
+ const providerConfig = config.providers?.[context.provider.id];
11972
+ const mergedModels = mergeCustomModelDefs(
11973
+ providerConfig?.customModels,
11974
+ config.models
11975
+ );
11976
+ const caps = await capabilitiesFor(
11977
+ modelsRegistry,
11978
+ context.provider.id,
11979
+ context.model,
11980
+ mergedModels
11981
+ );
11143
11982
  return caps.vision;
11144
11983
  } catch {
11145
11984
  return context.provider.capabilities.vision;
@@ -11206,6 +12045,8 @@ async function execute(deps) {
11206
12045
  if (err) {
11207
12046
  const tag = err.recoverable ? " (recoverable)" : "";
11208
12047
  renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
12048
+ const hint = contextOverflowHint(err);
12049
+ if (hint) renderer.writeWarning(hint);
11209
12050
  } else {
11210
12051
  renderer.writeError("Failed.");
11211
12052
  }
@@ -12832,8 +13673,12 @@ async function setupCompaction(params) {
12832
13673
  providerId: config.provider ?? provider.id,
12833
13674
  modelId: config.model ?? context.model
12834
13675
  });
13676
+ const initialPolicy = resolveContextWindowPolicy(config.context);
13677
+ context.meta ??= {};
13678
+ context.meta["contextWindowMode"] = initialPolicy.id;
13679
+ context.meta["contextWindowPolicy"] = initialPolicy;
12835
13680
  let autoCompactor;
12836
- if (config.context.autoCompact !== false) {
13681
+ if (config.context.autoCompact !== false && effectiveMaxContext > 0) {
12837
13682
  const auditLevel = resolveAuditLevel(fullConfig ?? config);
12838
13683
  const sessionBridge = providedBridge ?? createSessionEventBridge(sessionWriter, auditLevel);
12839
13684
  autoCompactor = new AutoCompactionMiddleware(
@@ -12842,15 +13687,15 @@ async function setupCompaction(params) {
12842
13687
  // Calibrated estimator: recordActualUsage() is called after each API
12843
13688
  // response so this converges on real token counts for compaction decisions.
12844
13689
  (ctx) => estimateRequestTokensCalibrated(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total,
13690
+ initialPolicy.thresholds,
12845
13691
  {
12846
- warn: config.context.warnThreshold,
12847
- soft: config.context.softThreshold,
12848
- hard: config.context.hardThreshold
12849
- },
12850
- {
12851
- aggressiveOn: "soft",
13692
+ aggressiveOn: initialPolicy.aggressiveOn,
12852
13693
  failureMode: "throw_on_hard",
12853
13694
  events,
13695
+ policyProvider: (ctx) => {
13696
+ const policy = ctx.meta?.["contextWindowPolicy"];
13697
+ return policy && typeof policy === "object" ? policy : null;
13698
+ },
12854
13699
  sessionBridge
12855
13700
  }
12856
13701
  );
@@ -12869,7 +13714,8 @@ function createAgent(params) {
12869
13714
  confirmAwaiter: params.confirmAwaiter,
12870
13715
  iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
12871
13716
  perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
12872
- tracer: params.tracer
13717
+ tracer: params.tracer,
13718
+ hookRunner: params.hookRunner
12873
13719
  });
12874
13720
  return new Agent({
12875
13721
  container: params.container,
@@ -12887,6 +13733,139 @@ function createAgent(params) {
12887
13733
  tracer: params.tracer
12888
13734
  });
12889
13735
  }
13736
+ function parseModelRef(ref) {
13737
+ const trimmed = ref.trim();
13738
+ const slash = trimmed.indexOf("/");
13739
+ if (slash !== -1) {
13740
+ return {
13741
+ provider: trimmed.slice(0, slash) || void 0,
13742
+ model: trimmed.slice(slash + 1).trim()
13743
+ };
13744
+ }
13745
+ const parts = trimmed.split(/\s+/);
13746
+ if (parts.length >= 2) {
13747
+ return { provider: parts[0], model: parts.slice(1).join(" ") };
13748
+ }
13749
+ return { model: trimmed };
13750
+ }
13751
+ function overloadStatus(err) {
13752
+ if (!(err instanceof ProviderError)) return null;
13753
+ const s = err.status;
13754
+ if (s === 429 || s === 529 || s >= 500) return s;
13755
+ return null;
13756
+ }
13757
+ function createFallbackModelExtension(deps) {
13758
+ const initial = deps.getConfig().fallbackModels ?? [];
13759
+ if (initial.length === 0) return null;
13760
+ let dirty = false;
13761
+ return {
13762
+ name: "fallback-model",
13763
+ beforeRun: (ctx) => {
13764
+ if (!dirty) return;
13765
+ const cfg = deps.getConfig();
13766
+ try {
13767
+ ctx.provider = deps.buildProvider(cfg.provider);
13768
+ ctx.model = cfg.model;
13769
+ deps.onModelSwitch?.(cfg.provider, cfg.model);
13770
+ } catch (err) {
13771
+ deps.logger.warn(
13772
+ `fallback-model: could not restore primary "${cfg.provider}/${cfg.model}": ${err instanceof Error ? err.message : String(err)}`
13773
+ );
13774
+ }
13775
+ dirty = false;
13776
+ },
13777
+ wrapProviderRunner: async (ctx, request, inner) => {
13778
+ try {
13779
+ return await inner(ctx, request);
13780
+ } catch (firstErr) {
13781
+ let lastErr = firstErr;
13782
+ const cfg = deps.getConfig();
13783
+ const chain = cfg.fallbackModels ?? [];
13784
+ for (const ref of chain) {
13785
+ const status = overloadStatus(lastErr);
13786
+ if (status === null) break;
13787
+ const parsed = parseModelRef(ref);
13788
+ if (!parsed.model) continue;
13789
+ const targetProviderId = parsed.provider ?? cfg.provider;
13790
+ const from = { providerId: ctx.provider.id, model: ctx.model };
13791
+ let nextProvider;
13792
+ try {
13793
+ nextProvider = deps.buildProvider(targetProviderId);
13794
+ } catch (err) {
13795
+ deps.logger.warn(
13796
+ `fallback-model: skipping "${ref}" \u2014 cannot build provider "${targetProviderId}": ${err instanceof Error ? err.message : String(err)}`
13797
+ );
13798
+ continue;
13799
+ }
13800
+ const providerSwitched = nextProvider.id !== from.providerId;
13801
+ ctx.provider = nextProvider;
13802
+ ctx.model = parsed.model;
13803
+ request.model = parsed.model;
13804
+ dirty = true;
13805
+ deps.onModelSwitch?.(targetProviderId, parsed.model);
13806
+ deps.events.emit("provider.fallback", {
13807
+ from,
13808
+ to: { providerId: nextProvider.id, model: parsed.model },
13809
+ status,
13810
+ providerSwitched
13811
+ });
13812
+ try {
13813
+ return await inner(ctx, request);
13814
+ } catch (err) {
13815
+ lastErr = err;
13816
+ }
13817
+ }
13818
+ throw lastErr;
13819
+ }
13820
+ }
13821
+ };
13822
+ }
13823
+
13824
+ // src/hooks-wiring.ts
13825
+ var HookBlockedError = class extends Error {
13826
+ constructor(reason) {
13827
+ super(`Prompt blocked by hook: ${reason}`);
13828
+ this.name = "HookBlockedError";
13829
+ }
13830
+ };
13831
+ function createUserPromptSubmitMiddleware(hookRunner) {
13832
+ return {
13833
+ name: "UserPromptSubmitHooks",
13834
+ handler: async (payload, next) => {
13835
+ const prompt = payload.text;
13836
+ if (prompt && hookRunner.has("UserPromptSubmit")) {
13837
+ const r = await hookRunner.userPromptSubmit(prompt, payload.ctx);
13838
+ if (r.block) throw new HookBlockedError(r.reason ?? "no reason given");
13839
+ if (r.additionalContext) {
13840
+ const block = { type: "text", text: r.additionalContext };
13841
+ payload.content = [...payload.content, block];
13842
+ payload.text = `${prompt}
13843
+
13844
+ ${r.additionalContext}`;
13845
+ }
13846
+ }
13847
+ return next(payload);
13848
+ }
13849
+ };
13850
+ }
13851
+ function createLifecycleHooksExtension(hookRunner) {
13852
+ let started = false;
13853
+ return {
13854
+ name: "lifecycle-hooks",
13855
+ beforeRun: async (ctx) => {
13856
+ if (started) return;
13857
+ started = true;
13858
+ if (!hookRunner.has("SessionStart")) return;
13859
+ const r = await hookRunner.sessionStart(ctx);
13860
+ if (r.additionalContext) {
13861
+ ctx.systemPrompt.push({ type: "text", text: r.additionalContext });
13862
+ }
13863
+ },
13864
+ afterRun: async (ctx) => {
13865
+ if (hookRunner.has("Stop")) await hookRunner.stop(ctx);
13866
+ }
13867
+ };
13868
+ }
12890
13869
  function setupMetrics(params) {
12891
13870
  const { flags, wpaths, events, logger, config } = params;
12892
13871
  let metricsSink;
@@ -13002,7 +13981,8 @@ async function setupPlugins(params) {
13002
13981
  skillLoader,
13003
13982
  configStore,
13004
13983
  pipelines,
13005
- paths
13984
+ paths,
13985
+ hookRegistry
13006
13986
  } = params;
13007
13987
  const builtinPlugins = [];
13008
13988
  const disabledBuiltins = new Set(
@@ -13067,6 +14047,7 @@ async function setupPlugins(params) {
13067
14047
  config: pluginConfig,
13068
14048
  log,
13069
14049
  extensions: agent.extensions,
14050
+ hookRegistry,
13070
14051
  sessionWriter: {
13071
14052
  transcriptPath: sessionWriter.transcriptPath,
13072
14053
  append: (e) => sessionWriter.append(e)
@@ -13312,10 +14293,8 @@ async function launchEternalFromFlag(deps) {
13312
14293
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
13313
14294
  } : emptyGoal2(eternalFlag);
13314
14295
  await saveGoal2(goalPath, next);
13315
- const policy = deps.container.resolve(
13316
- TOKENS.PermissionPolicy
13317
- );
13318
- policy.setYolo(true);
14296
+ const policy = deps.container.resolve(TOKENS.PermissionPolicy);
14297
+ policy.setYolo?.(true);
13319
14298
  deps.configRef.current = patchConfig(deps.configRef.current, { yolo: true });
13320
14299
  const compactor = deps.container.resolve(TOKENS.Compactor);
13321
14300
  const engine = new EternalAutonomyEngine({
@@ -13378,7 +14357,7 @@ async function main(argv) {
13378
14357
  modelsRegistry,
13379
14358
  permission: {
13380
14359
  yolo: config.yolo,
13381
- forceAllYolo: flags["force-all-yolo"] === true,
14360
+ yoloDestructive: flags["yolo-destructive"] === true || flags["force-all-yolo"] === true,
13382
14361
  promptDelegate: makePromptDelegate(reader)
13383
14362
  },
13384
14363
  compactor: {
@@ -13424,7 +14403,12 @@ async function main(argv) {
13424
14403
  const modeId = activeMode?.id ?? "default";
13425
14404
  const modePrompt = activeMode?.prompt ?? "";
13426
14405
  const [resolvedCaps, resolvedModel] = await Promise.all([
13427
- capabilitiesFor(modelsRegistry, provider.id, config.model).catch(() => void 0),
14406
+ capabilitiesFor(
14407
+ modelsRegistry,
14408
+ provider.id,
14409
+ config.model,
14410
+ mergeCustomModelDefs(config.providers?.[provider.id]?.customModels, config.models)
14411
+ ).catch(() => void 0),
13428
14412
  modelsRegistry.getModel(config.provider, config.model).catch(() => void 0)
13429
14413
  ]);
13430
14414
  const modelCapabilities = resolvedCaps ? {
@@ -13643,6 +14627,19 @@ async function main(argv) {
13643
14627
  });
13644
14628
  });
13645
14629
  const pipelines = setupPipelines({ events, logger });
14630
+ const hooksEnabled = flags["no-hooks"] !== true;
14631
+ const hookRegistry = new HookRegistry();
14632
+ if (hooksEnabled) hookRegistry.loadShellHooks(config.hooks);
14633
+ container.bind(TOKENS.HookRegistry, () => hookRegistry);
14634
+ const hookRunner = new HookRunner({
14635
+ registry: hookRegistry,
14636
+ logger,
14637
+ allowShell: hooksEnabled,
14638
+ sessionId: () => session.id
14639
+ });
14640
+ if (hooksEnabled) {
14641
+ pipelines.userInput.use(createUserPromptSubmitMiddleware(hookRunner));
14642
+ }
13646
14643
  const compactor = container.resolve(TOKENS.Compactor);
13647
14644
  const compactionSetup = await setupCompaction({
13648
14645
  compactor,
@@ -13668,9 +14665,11 @@ async function main(argv) {
13668
14665
  providerId,
13669
14666
  modelId
13670
14667
  });
13671
- effectiveMaxContext = mc > 0 ? mc : 2e5;
14668
+ effectiveMaxContext = mc;
13672
14669
  context.provider.capabilities.maxContext = effectiveMaxContext;
13673
- autoCompactor?.setMaxContext(effectiveMaxContext);
14670
+ if (effectiveMaxContext > 0) {
14671
+ autoCompactor?.setMaxContext(effectiveMaxContext);
14672
+ }
13674
14673
  events.emit("ctx.max_context", { providerId, modelId, maxContext: effectiveMaxContext });
13675
14674
  updateSpinnerContext();
13676
14675
  };
@@ -13687,8 +14686,12 @@ async function main(argv) {
13687
14686
  pipelines,
13688
14687
  context,
13689
14688
  config,
13690
- confirmAwaiter: makeConfirmAwaiter(reader)
14689
+ confirmAwaiter: makeConfirmAwaiter(reader),
14690
+ hookRunner
13691
14691
  });
14692
+ if (hooksEnabled) {
14693
+ agent.extensions.register(createLifecycleHooksExtension(hookRunner));
14694
+ }
13692
14695
  const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger });
13693
14696
  if (config.features.mcp) {
13694
14697
  for (const cfg of Object.values(config.mcpServers ?? {})) {
@@ -13716,24 +14719,41 @@ async function main(argv) {
13716
14719
  healthRegistry,
13717
14720
  skillLoader: config.features.skills ? skillLoader : void 0,
13718
14721
  configStore,
13719
- paths: wpaths
14722
+ paths: wpaths,
14723
+ hookRegistry
13720
14724
  });
14725
+ const resolveProviderCfg = (providerId) => {
14726
+ const savedCfg = config.providers?.[providerId];
14727
+ const resolvedProviderId = savedCfg?.type ?? providerId;
14728
+ const cfgWithType = {
14729
+ ...savedCfg ?? { type: providerId, apiKey: config.apiKey, baseUrl: config.baseUrl },
14730
+ type: resolvedProviderId
14731
+ };
14732
+ return { resolvedProviderId, cfgWithType };
14733
+ };
14734
+ const buildProviderForId = (providerId) => {
14735
+ const { resolvedProviderId, cfgWithType } = resolveProviderCfg(providerId);
14736
+ return config.features.modelsRegistry && providerRegistry.has(resolvedProviderId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(resolvedProviderId, cfgWithType);
14737
+ };
14738
+ const refreshMaxContextFor = (providerId, modelId) => {
14739
+ const { resolvedProviderId, cfgWithType } = resolveProviderCfg(providerId);
14740
+ void refreshMaxContext(resolvedProviderId, modelId, cfgWithType);
14741
+ };
14742
+ const fallbackExtension = createFallbackModelExtension({
14743
+ getConfig: () => config,
14744
+ buildProvider: buildProviderForId,
14745
+ onModelSwitch: refreshMaxContextFor,
14746
+ events,
14747
+ logger
14748
+ });
14749
+ if (fallbackExtension) agent.extensions.register(fallbackExtension);
13721
14750
  const switchProviderAndModel = (providerId, modelId) => {
13722
14751
  try {
13723
- const savedCfg = config.providers?.[providerId];
13724
- const resolvedProviderId = savedCfg?.type ?? providerId;
13725
- const newCfg = savedCfg ?? {
13726
- type: providerId,
13727
- apiKey: config.apiKey,
13728
- baseUrl: config.baseUrl
13729
- };
13730
- const cfgWithType = { ...newCfg, type: resolvedProviderId };
13731
- const newProvider = config.features.modelsRegistry && providerRegistry.has(resolvedProviderId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(resolvedProviderId, cfgWithType);
13732
- context.provider = newProvider;
14752
+ context.provider = buildProviderForId(providerId);
13733
14753
  context.model = modelId;
13734
14754
  config = patchConfig(config, { provider: providerId, model: modelId });
13735
14755
  configStore.update({ provider: providerId, model: modelId });
13736
- void refreshMaxContext(resolvedProviderId, modelId, cfgWithType);
14756
+ refreshMaxContextFor(providerId, modelId);
13737
14757
  return null;
13738
14758
  } catch (err) {
13739
14759
  return err instanceof Error ? err.message : String(err);
@@ -14340,6 +15360,21 @@ Restart WrongStack to load or unload plugin code in this session.`;
14340
15360
  }
14341
15361
  return result.message;
14342
15362
  },
15363
+ onContextLimit: (tokens) => {
15364
+ if (typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0) {
15365
+ effectiveMaxContext = tokens;
15366
+ context.provider.capabilities.maxContext = tokens;
15367
+ context.meta["effectiveMaxContext"] = tokens;
15368
+ autoCompactor?.setMaxContext(tokens);
15369
+ events.emit("ctx.max_context", {
15370
+ providerId: config.provider,
15371
+ modelId: context.model,
15372
+ maxContext: tokens
15373
+ });
15374
+ updateSpinnerContext();
15375
+ }
15376
+ return effectiveMaxContext;
15377
+ },
14343
15378
  onMcp: async (args) => {
14344
15379
  const parsed = parseMcpArgs(args);
14345
15380
  if (!parsed) {
@@ -14358,11 +15393,11 @@ Restart WrongStack to load or unload plugin code in this session.`;
14358
15393
  onYolo: (setTo) => {
14359
15394
  const policy = container.resolve(TOKENS.PermissionPolicy);
14360
15395
  if (setTo !== void 0) {
14361
- policy.setYolo(setTo);
15396
+ policy.setYolo?.(setTo);
14362
15397
  config = patchConfig(config, { yolo: setTo });
14363
15398
  return setTo;
14364
15399
  }
14365
- return policy.getYolo();
15400
+ return policy.getYolo?.() ?? config.yolo ?? false;
14366
15401
  },
14367
15402
  onNextPredict: (setTo) => {
14368
15403
  if (setTo !== void 0) {
@@ -14596,7 +15631,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
14596
15631
  setStatuslineHiddenItems,
14597
15632
  getYolo: () => {
14598
15633
  const policy = container.resolve(TOKENS.PermissionPolicy);
14599
- return policy.getYolo();
15634
+ return policy.getYolo?.() ?? config.yolo ?? false;
14600
15635
  },
14601
15636
  getAutonomy: () => autonomyMode,
14602
15637
  onAutonomy: (setTo) => {