@wrongstack/cli 0.51.3 → 0.54.1

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, 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';
6
6
  import { createRequire } from 'module';
7
7
  import * as os2 from 'os';
8
8
  import os2__default from 'os';
@@ -16,7 +16,7 @@ import { createDefaultContainer, routeImagesForModel, readClipboardImage } from
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 fs11 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';
@@ -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
  })
@@ -2349,11 +2350,21 @@ async function resolveRuntimeMaxContext(input) {
2349
2350
  providerConfig?.baseUrl || topLevelBaseUrlApplies && input.config.baseUrl
2350
2351
  );
2351
2352
  if (input.modelsRegistry && !hasCustomBaseUrl) {
2352
- const caps = await capabilitiesFor(input.modelsRegistry, input.providerId, input.modelId).catch(
2353
- () => void 0
2353
+ const mergedModels = mergeCustomModelDefs(
2354
+ providerConfig?.customModels,
2355
+ input.config.models
2354
2356
  );
2357
+ const caps = await capabilitiesFor(
2358
+ input.modelsRegistry,
2359
+ input.providerId,
2360
+ input.modelId,
2361
+ mergedModels
2362
+ ).catch(() => void 0);
2355
2363
  const catalogMax = positiveNumber(caps?.maxContext);
2356
2364
  if (catalogMax) return catalogMax;
2365
+ const directModel = await input.modelsRegistry.getModel(input.providerId, input.modelId).catch(() => void 0);
2366
+ const directMax = positiveNumber(directModel?.capabilities.maxContext);
2367
+ if (directMax) return directMax;
2357
2368
  }
2358
2369
  return positiveNumber(input.provider.capabilities.maxContext) ?? 0;
2359
2370
  }
@@ -2389,6 +2400,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2389
2400
  "metrics",
2390
2401
  "webui",
2391
2402
  "no-check",
2403
+ "no-models-refresh",
2392
2404
  "director",
2393
2405
  "no-director",
2394
2406
  "no-autonomy",
@@ -5195,6 +5207,224 @@ ${targetMode.description}`
5195
5207
  }
5196
5208
  };
5197
5209
  }
5210
+ var noOpVault = {
5211
+ encrypt: (v) => v,
5212
+ decrypt: (v) => v,
5213
+ isEncrypted: () => false
5214
+ };
5215
+ async function patchGlobalConfig(globalConfigPath, mutate) {
5216
+ let raw = "{}";
5217
+ let fileExists = true;
5218
+ try {
5219
+ raw = await fsp3.readFile(globalConfigPath, "utf8");
5220
+ } catch (err) {
5221
+ if (err.code !== "ENOENT") throw err;
5222
+ fileExists = false;
5223
+ }
5224
+ let parsed;
5225
+ try {
5226
+ parsed = JSON.parse(raw);
5227
+ } catch (err) {
5228
+ if (fileExists) {
5229
+ throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
5230
+ }
5231
+ parsed = {};
5232
+ }
5233
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
5234
+ mutate(decrypted);
5235
+ const encrypted = encryptConfigSecrets$1(decrypted, noOpVault);
5236
+ await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
5237
+ return decrypted;
5238
+ }
5239
+ function fmtModel(id, def) {
5240
+ const parts = [];
5241
+ if (def.provider) parts.push(`${color.dim("provider:")} ${color.cyan(def.provider)}`);
5242
+ if (def.name) parts.push(`${color.dim("name:")} ${def.name}`);
5243
+ const caps = def.capabilities;
5244
+ if (caps) {
5245
+ if (caps.maxContext) parts.push(`${color.dim("maxContext:")} ${color.yellow(String(caps.maxContext))}`);
5246
+ const flags = [];
5247
+ if (caps.tools) flags.push("tools");
5248
+ if (caps.vision) flags.push("vision");
5249
+ if (caps.reasoning) flags.push("reasoning");
5250
+ if (caps.streaming) flags.push("streaming");
5251
+ if (caps.jsonMode) flags.push("json");
5252
+ if (flags.length) parts.push(`${color.dim("caps:")} ${flags.join(", ")}`);
5253
+ }
5254
+ if (def.maxOutput) parts.push(`${color.dim("maxOutput:")} ${color.yellow(String(def.maxOutput))}`);
5255
+ return ` ${color.amber(id)} ${parts.join(" ")}`;
5256
+ }
5257
+ function safeAt(arr, idx) {
5258
+ const v = arr[idx];
5259
+ if (v === void 0) throw new Error(`Missing value at position ${idx}`);
5260
+ return v;
5261
+ }
5262
+ function parseFlags(tokens) {
5263
+ let modelId = "";
5264
+ const caps = {};
5265
+ let provider;
5266
+ let name;
5267
+ let maxOutput;
5268
+ let i = 0;
5269
+ while (i < tokens.length) {
5270
+ const t = tokens[i];
5271
+ if (t.startsWith("--")) {
5272
+ const key = t.slice(2);
5273
+ switch (key) {
5274
+ case "provider":
5275
+ provider = safeAt(tokens, ++i);
5276
+ break;
5277
+ case "name":
5278
+ name = safeAt(tokens, ++i);
5279
+ break;
5280
+ case "max-context":
5281
+ caps.maxContext = Number(safeAt(tokens, ++i));
5282
+ break;
5283
+ case "max-output":
5284
+ maxOutput = Number(safeAt(tokens, ++i));
5285
+ break;
5286
+ case "tools":
5287
+ caps.tools = true;
5288
+ break;
5289
+ case "vision":
5290
+ caps.vision = true;
5291
+ break;
5292
+ case "streaming":
5293
+ caps.streaming = true;
5294
+ break;
5295
+ case "reasoning":
5296
+ caps.reasoning = true;
5297
+ break;
5298
+ case "json-mode":
5299
+ caps.jsonMode = true;
5300
+ break;
5301
+ default:
5302
+ return { modelId: "", error: `Unknown flag: --${key}` };
5303
+ }
5304
+ } else if (!t.startsWith("-") && !modelId) {
5305
+ modelId = t;
5306
+ }
5307
+ i++;
5308
+ }
5309
+ if (!modelId) return { modelId: "", error: "missing model id" };
5310
+ const hasCaps = Object.keys(caps).length > 0;
5311
+ const def = {};
5312
+ if (provider !== void 0) def.provider = provider;
5313
+ if (name !== void 0) def.name = name;
5314
+ if (maxOutput !== void 0) def.maxOutput = maxOutput;
5315
+ if (hasCaps) def.capabilities = caps;
5316
+ return { modelId, def: Object.keys(def).length ? def : void 0 };
5317
+ }
5318
+ function buildModelsCommand(opts) {
5319
+ const help = [
5320
+ "Usage:",
5321
+ " /models List custom model definitions",
5322
+ " /models add <id> [flags] Add or update a custom model",
5323
+ " /models remove <id> Remove a custom model",
5324
+ "",
5325
+ "Flags for add:",
5326
+ " --provider <id> Owning provider",
5327
+ ' --name "Display" Display name',
5328
+ " --max-context <N> Context window override",
5329
+ " --max-output <N> Max output tokens",
5330
+ " --tools Tool-capable",
5331
+ " --vision Vision-capable",
5332
+ " --streaming Streaming support",
5333
+ " --reasoning Reasoning support",
5334
+ " --json-mode JSON mode support",
5335
+ "",
5336
+ "Persisted to ~/.wrongstack/config.json."
5337
+ ].join("\n");
5338
+ return {
5339
+ name: "models",
5340
+ description: "Manage custom model definitions.",
5341
+ help,
5342
+ async run(args) {
5343
+ const parts = args.trim().split(/\s+/).filter(Boolean);
5344
+ const sub = (parts[0] ?? "").toLowerCase();
5345
+ if (sub === "help" || sub === "--help") return { message: help };
5346
+ if (!opts.configStore || !opts.paths) {
5347
+ return { message: `${color.red("Error")} config store not available.` };
5348
+ }
5349
+ const config = opts.configStore.get();
5350
+ const globalConfigPath = opts.paths.globalConfig;
5351
+ if (!sub) {
5352
+ const models = config.models ?? {};
5353
+ const ids = Object.keys(models);
5354
+ if (ids.length === 0) {
5355
+ return {
5356
+ message: [
5357
+ `${color.bold("Custom Models")} ${color.dim("(none defined)")}`,
5358
+ "",
5359
+ color.dim(" Add one: /models add <id> --max-context 128000 --tools")
5360
+ ].join("\n")
5361
+ };
5362
+ }
5363
+ return {
5364
+ message: [
5365
+ `${color.bold("Custom Models")} ${color.dim(`(${ids.length})`)}`,
5366
+ ...ids.sort().map((id) => fmtModel(id, models[id]))
5367
+ ].join("\n")
5368
+ };
5369
+ }
5370
+ try {
5371
+ if (sub === "add") {
5372
+ const { modelId, def, error } = parseFlags(parts.slice(1));
5373
+ if (error) {
5374
+ return { message: `${color.red("Error")}: ${error}. ${color.dim("/models help")}` };
5375
+ }
5376
+ if (!def && !error) {
5377
+ return { message: `${color.amber("Usage:")} /models add <id> [--max-context N] [--tools] ... ${color.dim("/models help")}` };
5378
+ }
5379
+ const existingModels = config.models ?? {};
5380
+ const existed = modelId in existingModels;
5381
+ const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
5382
+ const models = { ...cfg.models ?? {} };
5383
+ models[modelId] = {
5384
+ ...models[modelId],
5385
+ ...def,
5386
+ capabilities: {
5387
+ ...models[modelId]?.capabilities,
5388
+ ...def?.capabilities
5389
+ }
5390
+ };
5391
+ cfg.models = models;
5392
+ });
5393
+ opts.configStore.update({
5394
+ models: decrypted.models
5395
+ });
5396
+ return { message: `${color.green("\u2713")} ${color.amber(modelId)} ${existed ? "updated" : "added"}.` };
5397
+ }
5398
+ if (sub === "remove" || sub === "rm") {
5399
+ const modelId = parts[1];
5400
+ if (!modelId) {
5401
+ return { message: `${color.amber("Usage:")} /models remove <id>` };
5402
+ }
5403
+ const existing = config.models ?? {};
5404
+ if (!(modelId in existing)) {
5405
+ return { message: `${color.amber("Not found")}: custom model "${modelId}" is not defined.` };
5406
+ }
5407
+ const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
5408
+ const models = { ...cfg.models ?? {} };
5409
+ delete models[modelId];
5410
+ cfg.models = models;
5411
+ });
5412
+ opts.configStore.update({
5413
+ models: decrypted.models
5414
+ });
5415
+ return { message: `${color.green("\u2713")} removed ${color.amber(modelId)}` };
5416
+ }
5417
+ return {
5418
+ message: `${color.red("Unknown subcommand")} "${sub}". Try ${color.dim("/models")}, ${color.dim("/models add")}, or ${color.dim("/models help")}.`
5419
+ };
5420
+ } catch (err) {
5421
+ return {
5422
+ message: `${color.red("models error")}: ${err instanceof Error ? err.message : String(err)}`
5423
+ };
5424
+ }
5425
+ }
5426
+ };
5427
+ }
5198
5428
  function buildNextCommand(opts) {
5199
5429
  return {
5200
5430
  name: "next",
@@ -5452,7 +5682,7 @@ function summariseEvent(ev) {
5452
5682
  return color.dim("\u2026");
5453
5683
  }
5454
5684
  }
5455
- var noOpVault = {
5685
+ var noOpVault2 = {
5456
5686
  encrypt: (v) => v,
5457
5687
  decrypt: (v) => v,
5458
5688
  isEncrypted: () => false
@@ -5487,7 +5717,7 @@ function parseTarget(tokens) {
5487
5717
  function fmtEntry(e) {
5488
5718
  return e.provider ? `${e.provider}/${e.model}` : `${e.model} ${color.dim("(leader provider)")}`;
5489
5719
  }
5490
- async function patchGlobalConfig(globalConfigPath, mutate) {
5720
+ async function patchGlobalConfig2(globalConfigPath, mutate) {
5491
5721
  let raw = "{}";
5492
5722
  let fileExists = true;
5493
5723
  try {
@@ -5504,9 +5734,9 @@ async function patchGlobalConfig(globalConfigPath, mutate) {
5504
5734
  throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
5505
5735
  parsed = {};
5506
5736
  }
5507
- const decrypted = decryptConfigSecrets$1(parsed, noOpVault);
5737
+ const decrypted = decryptConfigSecrets$1(parsed, noOpVault2);
5508
5738
  mutate(decrypted);
5509
- const encrypted = encryptConfigSecrets$1(decrypted, noOpVault);
5739
+ const encrypted = encryptConfigSecrets$1(decrypted, noOpVault2);
5510
5740
  await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
5511
5741
  return decrypted;
5512
5742
  }
@@ -5597,7 +5827,7 @@ function buildSetModelCommand(opts) {
5597
5827
  message: `${color.red("Provider not available")}: "${provider}". Keyed: ${keyed.join(", ") || "(none)"}. ${color.dim("/setmodel list")}`
5598
5828
  };
5599
5829
  }
5600
- const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
5830
+ const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
5601
5831
  cfg.provider = provider;
5602
5832
  cfg.model = model;
5603
5833
  });
@@ -5628,7 +5858,7 @@ function buildSetModelCommand(opts) {
5628
5858
  message: `${color.red("Provider not available")}: "${parsed.provider}". Keyed: ${keyed.join(", ") || "(none)"}.`
5629
5859
  };
5630
5860
  }
5631
- const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
5861
+ const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
5632
5862
  const matrix = { ...cfg.modelMatrix ?? {} };
5633
5863
  matrix[key] = parsed.provider ? { provider: parsed.provider, model: parsed.model } : { model: parsed.model };
5634
5864
  cfg.modelMatrix = matrix;
@@ -5645,7 +5875,7 @@ function buildSetModelCommand(opts) {
5645
5875
  if (!(key in existing)) {
5646
5876
  return { message: `${color.amber("No matrix entry")} for "${key}".` };
5647
5877
  }
5648
- const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
5878
+ const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
5649
5879
  const matrix = { ...cfg.modelMatrix ?? {} };
5650
5880
  delete matrix[key];
5651
5881
  cfg.modelMatrix = matrix;
@@ -5697,7 +5927,7 @@ async function persistAutonomySetting(deps, mutator) {
5697
5927
  }
5698
5928
 
5699
5929
  // src/slash-commands/settings.ts
5700
- var noOpVault2 = {
5930
+ var noOpVault3 = {
5701
5931
  encrypt: (v) => v,
5702
5932
  decrypt: (v) => v,
5703
5933
  isEncrypted: () => false
@@ -5762,7 +5992,7 @@ function buildSettingsCommand(opts) {
5762
5992
  const persistDeps = {
5763
5993
  configStore: opts.configStore,
5764
5994
  globalConfigPath: opts.paths.globalConfig,
5765
- vault: noOpVault2
5995
+ vault: noOpVault3
5766
5996
  };
5767
5997
  try {
5768
5998
  if (sub === "delay") {
@@ -6186,6 +6416,7 @@ function buildBuiltinSlashCommands(opts) {
6186
6416
  buildWorktreeCommand(opts),
6187
6417
  buildSettingsCommand(opts),
6188
6418
  buildSetModelCommand(opts),
6419
+ buildModelsCommand(opts),
6189
6420
  buildCollabCommand(opts),
6190
6421
  buildStatuslineCommand({
6191
6422
  cwd: opts.cwd,
@@ -6907,11 +7138,11 @@ async function restoreLast(homeFn = defaultHomeDir) {
6907
7138
  var theme = { primary: color.amber };
6908
7139
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
6909
7140
  try {
6910
- const { atomicWrite: atomicWrite10 } = await import('@wrongstack/core');
6911
- const fs22 = await import('fs/promises');
7141
+ const { atomicWrite: atomicWrite12 } = await import('@wrongstack/core');
7142
+ const fs24 = await import('fs/promises');
6912
7143
  let existing = {};
6913
7144
  try {
6914
- const raw = await fs22.readFile(configPath2, "utf8");
7145
+ const raw = await fs24.readFile(configPath2, "utf8");
6915
7146
  existing = JSON.parse(raw);
6916
7147
  } catch {
6917
7148
  }
@@ -6919,7 +7150,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
6919
7150
  existing.provider = provider;
6920
7151
  existing.model = model;
6921
7152
  await backupCurrent(homeFn);
6922
- await atomicWrite10(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7153
+ await atomicWrite12(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
6923
7154
  try {
6924
7155
  await appendHistory(
6925
7156
  oldCfg,
@@ -7242,12 +7473,12 @@ function pickGroupIndex(opts) {
7242
7473
  try {
7243
7474
  let current = 0;
7244
7475
  try {
7245
- const parsed = Number.parseInt(fs10.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7476
+ const parsed = Number.parseInt(fs11.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7246
7477
  if (Number.isFinite(parsed)) current = wrap(parsed);
7247
7478
  } catch {
7248
7479
  }
7249
- fs10.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7250
- fs10.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7480
+ fs11.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7481
+ fs11.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7251
7482
  return current;
7252
7483
  } catch {
7253
7484
  }
@@ -9154,8 +9385,49 @@ ${color.dim(`Current: ${deps.config.provider ?? "<unset>"} / ${deps.config.model
9154
9385
  return 1;
9155
9386
  }
9156
9387
  };
9388
+ function parseFlags2(args) {
9389
+ const flags = {};
9390
+ for (let i = 0; i < args.length; i++) {
9391
+ const a = args[i];
9392
+ if (a.startsWith("--")) {
9393
+ const eq = a.indexOf("=");
9394
+ if (eq !== -1) {
9395
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
9396
+ } else {
9397
+ const name = a.slice(2);
9398
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
9399
+ flags[name] = args[++i] ?? "";
9400
+ } else {
9401
+ flags[name] = true;
9402
+ }
9403
+ }
9404
+ }
9405
+ }
9406
+ return flags;
9407
+ }
9408
+ function positionals(args) {
9409
+ const out = [];
9410
+ for (let i = 0; i < args.length; i++) {
9411
+ const a = args[i];
9412
+ if (a.startsWith("--")) {
9413
+ const eq = a.indexOf("=");
9414
+ if (eq === -1) {
9415
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
9416
+ i++;
9417
+ }
9418
+ }
9419
+ continue;
9420
+ }
9421
+ out.push(a);
9422
+ }
9423
+ return out;
9424
+ }
9425
+ var DEFAULT_PER_PAGE = 15;
9157
9426
  var modelsCmd = async (args, deps) => {
9158
9427
  const sub = args[0];
9428
+ if (sub === "add") return modelsAdd(args.slice(1), deps);
9429
+ if (sub === "remove") return modelsRemove(args.slice(1), deps);
9430
+ if (sub === "list") return modelsList(args.slice(1), deps);
9159
9431
  if (sub === "refresh") {
9160
9432
  deps.renderer.writeInfo("Refreshing models.dev cache\u2026");
9161
9433
  try {
@@ -9169,9 +9441,13 @@ var modelsCmd = async (args, deps) => {
9169
9441
  return 1;
9170
9442
  }
9171
9443
  }
9172
- const providerId = sub ?? deps.config.provider;
9444
+ const flags = parseFlags2(args);
9445
+ const search = typeof flags["search"] === "string" ? flags["search"].toLowerCase() : "";
9446
+ const perPage = Number(flags["per-page"]) > 0 ? Number(flags["per-page"]) : DEFAULT_PER_PAGE;
9447
+ const page = Math.max(1, Number(flags["page"]) || 1);
9448
+ const providerId = sub ?? deps.config.provider ?? "";
9173
9449
  if (!providerId) {
9174
- deps.renderer.writeError("Usage: wstack models <provider> | refresh");
9450
+ deps.renderer.writeError("Usage: wstack models <provider> [--search <term>] [--page N] [--per-page N]");
9175
9451
  return 1;
9176
9452
  }
9177
9453
  let lookupId = providerId;
@@ -9195,34 +9471,209 @@ var modelsCmd = async (args, deps) => {
9195
9471
  `));
9196
9472
  const userModels = deps.config.providers?.[providerId]?.models;
9197
9473
  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(
9474
+ const allSorted = userModels && userModels.length > 0 ? userModels.map((id) => catalogById.get(id) ?? { id, name: id }) : [...provider.models].sort(
9199
9475
  (a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
9200
9476
  );
9201
9477
  if (userModels && userModels.length > 0)
9202
9478
  deps.renderer.write(color.dim(`(${userModels.length} model(s) from your saved config)
9203
9479
  `));
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(","))}
9480
+ const filtered = search ? allSorted.filter((m) => m.id.toLowerCase().includes(search)) : allSorted;
9481
+ const total = filtered.length;
9482
+ const totalPages = Math.max(1, Math.ceil(total / perPage));
9483
+ const actualPage = Math.min(page, totalPages);
9484
+ const start = (actualPage - 1) * perPage;
9485
+ const pageItems = filtered.slice(start, start + perPage);
9486
+ const end = Math.min(start + pageItems.length, total);
9487
+ const pageHint = totalPages > 1 ? color.cyan(`[page ${actualPage}/${totalPages}]`) : "";
9488
+ const searchHint = search ? color.yellow(` (filtered: "${search}" \u2014 ${total} match${total === 1 ? "" : "es"})`) : color.dim(` (${total} model${total === 1 ? "" : "s"})`);
9489
+ deps.renderer.write(`${pageHint}${searchHint}
9490
+ `);
9491
+ if (pageItems.length === 0) {
9492
+ deps.renderer.write(color.dim("(no models match)\n"));
9493
+ } else {
9494
+ if (start > 0)
9495
+ deps.renderer.write(color.dim(` ${String.fromCharCode(8593)} ${start} above
9496
+ `));
9497
+ for (const m of pageItems) {
9498
+ const caps = [];
9499
+ if ("tool_call" in m && m.tool_call) caps.push("tools");
9500
+ if ("reasoning" in m && m.reasoning) caps.push("reasoning");
9501
+ if ("modalities" in m && m.modalities?.input?.includes("image")) caps.push("vision");
9502
+ const ctx = "limit" in m && m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
9503
+ const cost = "cost" in m && m.cost?.input !== void 0 ? `${m.cost.input}/${m.cost.output ?? "?"}` : "";
9504
+ deps.renderer.write(
9505
+ ` ${m.id.padEnd(40)} ${color.dim(ctx.padStart(6))} ${color.dim(cost.padEnd(14))} ${color.dim(caps.join(","))}
9213
9506
  `
9214
- );
9507
+ );
9508
+ }
9509
+ if (end < total)
9510
+ deps.renderer.write(color.dim(` ${String.fromCharCode(8595)} ${total - end} below
9511
+ `));
9512
+ }
9513
+ const navLines = [];
9514
+ if (totalPages > 1) {
9515
+ if (actualPage > 1) navLines.push(`--page ${actualPage - 1} (prev)`);
9516
+ if (actualPage < totalPages) navLines.push(`--page ${actualPage + 1} (next)`);
9215
9517
  }
9518
+ navLines.push("--search <term> (filter)");
9519
+ deps.renderer.write(color.dim(`
9520
+ ${navLines.join(" \xB7 ")}
9521
+ `));
9216
9522
  const age = await deps.modelsRegistry.ageSeconds();
9217
9523
  deps.renderer.write(
9218
9524
  color.dim(
9219
- `
9220
- Cache age: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never fetched"}. Run \`wstack models refresh\` to update.
9525
+ `Cache age: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never fetched"}. Run \`wstack models refresh\` to update.
9221
9526
  `
9222
9527
  )
9223
9528
  );
9224
9529
  return 0;
9225
9530
  };
9531
+ async function mutateModelsConfig(deps, mutator) {
9532
+ const vault = deps.vault;
9533
+ const configPath2 = deps.paths.globalConfig;
9534
+ let fileExists = true;
9535
+ let raw;
9536
+ try {
9537
+ raw = await fsp3.readFile(configPath2, "utf8");
9538
+ } catch (err) {
9539
+ if (err.code !== "ENOENT") throw err;
9540
+ fileExists = false;
9541
+ raw = "{}";
9542
+ }
9543
+ let parsed;
9544
+ try {
9545
+ parsed = JSON.parse(raw);
9546
+ } catch (err) {
9547
+ if (fileExists) {
9548
+ throw new Error(
9549
+ `Refusing to overwrite corrupt config at ${configPath2} (${err.message}).`
9550
+ );
9551
+ }
9552
+ parsed = {};
9553
+ }
9554
+ const decrypted = decryptConfigSecrets$1(parsed, vault);
9555
+ const models = decrypted.models ?? {};
9556
+ mutator(models);
9557
+ decrypted.models = models;
9558
+ const encrypted = encryptConfigSecrets$1(decrypted, vault);
9559
+ await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
9560
+ }
9561
+ function parseSizeFlag(raw) {
9562
+ if (!raw) return void 0;
9563
+ const s = raw.trim().toLowerCase();
9564
+ const match = /^(\d+(?:\.\d+)?)\s*(k|m|b)?$/.exec(s);
9565
+ if (!match) return void 0;
9566
+ const num = Number.parseFloat(match[1]);
9567
+ const unit = match[2];
9568
+ if (unit === "b") return Math.round(num * 1e9);
9569
+ if (unit === "m") return Math.round(num * 1e6);
9570
+ if (unit === "k") return Math.round(num * 1e3);
9571
+ return Math.round(num);
9572
+ }
9573
+ function parseBoolFlag(flags, key) {
9574
+ if (flags[key] === true || flags[key] === "true") return true;
9575
+ if (flags[`no-${key}`] !== void 0) return false;
9576
+ return void 0;
9577
+ }
9578
+ async function modelsAdd(args, deps) {
9579
+ const flags = parseFlags2(args);
9580
+ const pos = positionals(args);
9581
+ const modelId = pos[0];
9582
+ if (!modelId) {
9583
+ deps.renderer.writeError(
9584
+ "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]"
9585
+ );
9586
+ return 1;
9587
+ }
9588
+ const existing = deps.config.models?.[modelId];
9589
+ if (existing) {
9590
+ deps.renderer.writeWarning(
9591
+ `Model "${modelId}" already defined. Overwriting.`
9592
+ );
9593
+ }
9594
+ const capabilities = {};
9595
+ const toolsVal = parseBoolFlag(flags, "tools");
9596
+ if (toolsVal !== void 0) capabilities.tools = toolsVal;
9597
+ const visionVal = parseBoolFlag(flags, "vision");
9598
+ if (visionVal !== void 0) capabilities.vision = visionVal;
9599
+ const streamingVal = parseBoolFlag(flags, "streaming");
9600
+ if (streamingVal !== void 0) capabilities.streaming = streamingVal;
9601
+ const reasoningVal = parseBoolFlag(flags, "reasoning");
9602
+ if (reasoningVal !== void 0) capabilities.reasoning = reasoningVal;
9603
+ const jsonModeVal = parseBoolFlag(flags, "json-mode");
9604
+ if (jsonModeVal !== void 0) capabilities.jsonMode = jsonModeVal;
9605
+ const maxContextRaw = typeof flags["max-context"] === "string" ? flags["max-context"] : void 0;
9606
+ const maxContext = parseSizeFlag(maxContextRaw);
9607
+ if (maxContext !== void 0) capabilities.maxContext = maxContext;
9608
+ const def = {};
9609
+ const nameFlag = typeof flags["name"] === "string" ? flags["name"] : void 0;
9610
+ const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
9611
+ if (nameFlag) def.name = nameFlag;
9612
+ if (providerFlag) def.provider = providerFlag;
9613
+ if (Object.keys(capabilities).length > 0) def.capabilities = capabilities;
9614
+ const maxOutputRaw = typeof flags["max-output"] === "string" ? flags["max-output"] : void 0;
9615
+ const maxOutput = parseSizeFlag(maxOutputRaw);
9616
+ if (maxOutput !== void 0) def.maxOutput = maxOutput;
9617
+ await mutateModelsConfig(deps, (models) => {
9618
+ models[modelId] = def;
9619
+ });
9620
+ deps.renderer.writeInfo(`Custom model "${modelId}" ${existing ? "updated" : "added"}.`);
9621
+ const capLines = [];
9622
+ if (def.capabilities) {
9623
+ for (const [k, v] of Object.entries(def.capabilities)) {
9624
+ capLines.push(` ${k}: ${v}`);
9625
+ }
9626
+ }
9627
+ if (def.maxOutput !== void 0) capLines.push(` maxOutput: ${def.maxOutput}`);
9628
+ if (capLines.length > 0) {
9629
+ deps.renderer.write(color.dim(capLines.join("\n") + "\n"));
9630
+ }
9631
+ return 0;
9632
+ }
9633
+ async function modelsRemove(args, deps) {
9634
+ const modelId = args[0];
9635
+ if (!modelId) {
9636
+ deps.renderer.writeError("Usage: wstack models remove <modelId>");
9637
+ return 1;
9638
+ }
9639
+ const existing = deps.config.models?.[modelId];
9640
+ if (!existing) {
9641
+ deps.renderer.writeError(`No custom model "${modelId}" found.`);
9642
+ return 1;
9643
+ }
9644
+ await mutateModelsConfig(deps, (models) => {
9645
+ delete models[modelId];
9646
+ });
9647
+ deps.renderer.writeInfo(`Removed custom model "${modelId}".`);
9648
+ return 0;
9649
+ }
9650
+ async function modelsList(_args, deps) {
9651
+ const models = deps.config.models ?? {};
9652
+ const entries = Object.entries(models);
9653
+ if (entries.length === 0) {
9654
+ deps.renderer.write(color.dim("No custom models defined.\n"));
9655
+ deps.renderer.write(color.dim("Use `wstack models add <modelId> --max-context 128k --tools`\n"));
9656
+ return 0;
9657
+ }
9658
+ deps.renderer.write(color.bold("Custom models\n"));
9659
+ for (const [id, def] of entries.sort(([a], [b]) => a.localeCompare(b))) {
9660
+ const label = def.name ?? id;
9661
+ const provider = def.provider ? ` ${color.dim(`(${def.provider})`)}` : "";
9662
+ deps.renderer.write(` ${color.bold(label)}${provider}
9663
+ `);
9664
+ if (def.capabilities) {
9665
+ for (const [k, v] of Object.entries(def.capabilities)) {
9666
+ deps.renderer.write(` ${color.dim(`${k}:`)} ${v}
9667
+ `);
9668
+ }
9669
+ }
9670
+ if (def.maxOutput !== void 0) {
9671
+ deps.renderer.write(` ${color.dim("maxOutput:")} ${def.maxOutput}
9672
+ `);
9673
+ }
9674
+ }
9675
+ return 0;
9676
+ }
9226
9677
  function redactKeys(obj) {
9227
9678
  if (!obj || typeof obj !== "object") return obj;
9228
9679
  if (Array.isArray(obj)) return obj.map(redactKeys);
@@ -9815,10 +10266,10 @@ var auditCmd = async (args, deps) => {
9815
10266
  return verify.ok ? 0 : 1;
9816
10267
  };
9817
10268
  async function listAudits(log, dir, deps) {
9818
- const fs22 = await import('fs/promises');
10269
+ const fs24 = await import('fs/promises');
9819
10270
  let entries;
9820
10271
  try {
9821
- entries = await fs22.readdir(dir);
10272
+ entries = await fs24.readdir(dir);
9822
10273
  } catch {
9823
10274
  deps.renderer.write(
9824
10275
  color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
@@ -9895,6 +10346,9 @@ var helpCmd = async (_args, deps) => {
9895
10346
  " wstack providers [--all] List providers from models.dev",
9896
10347
  " wstack models [<provider>] List models",
9897
10348
  " wstack models refresh Force-refresh cache",
10349
+ " wstack models add <mid> Add/override custom model (--max-context, --tools, --vision, \u2026)",
10350
+ " wstack models remove <mid> Remove a custom model",
10351
+ " wstack models list List all custom models",
9898
10352
  " wstack mcp [list] List MCP servers",
9899
10353
  " wstack plugin [list|status|official|install|add|remove|enable|disable] Manage plugins",
9900
10354
  " wstack projects List tracked projects",
@@ -9955,22 +10409,22 @@ function fmtDuration(ms) {
9955
10409
  const remMin = m - h * 60;
9956
10410
  return `${h}h${remMin}m`;
9957
10411
  }
9958
- function fmtTaskResultLine(r, color45) {
10412
+ function fmtTaskResultLine(r, color46) {
9959
10413
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
9960
10414
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
9961
10415
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
9962
10416
  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)}` : "";
10417
+ const errKindChip = errKind ? color46.dim(` [${errKind}]`) : "";
10418
+ const errSnip = errMsg || errKind ? `${errKindChip}${color46.dim(errTail)}` : "";
9965
10419
  switch (r.status) {
9966
10420
  case "success":
9967
- return { mark: color45.green("\u2713"), stats, tail: "" };
10421
+ return { mark: color46.green("\u2713"), stats, tail: "" };
9968
10422
  case "timeout":
9969
- return { mark: color45.yellow("\u23F1"), stats: `${color45.yellow("timeout")} ${stats}`, tail: errSnip };
10423
+ return { mark: color46.yellow("\u23F1"), stats: `${color46.yellow("timeout")} ${stats}`, tail: errSnip };
9970
10424
  case "stopped":
9971
- return { mark: color45.dim("\u2298"), stats: `${color45.dim("stopped")} ${stats}`, tail: errSnip };
10425
+ return { mark: color46.dim("\u2298"), stats: `${color46.dim("stopped")} ${stats}`, tail: errSnip };
9972
10426
  case "failed":
9973
- return { mark: color45.red("\u2717"), stats: `${color45.red("failed")} ${stats}`, tail: errSnip };
10427
+ return { mark: color46.red("\u2717"), stats: `${color46.red("failed")} ${stats}`, tail: errSnip };
9974
10428
  }
9975
10429
  }
9976
10430
 
@@ -10038,6 +10492,15 @@ async function boot(argv) {
10038
10492
  }).catch(() => {
10039
10493
  });
10040
10494
  }
10495
+ if (!flags["no-models-refresh"]) {
10496
+ try {
10497
+ await modelsRegistry.refresh();
10498
+ logger.info("models.dev catalog refreshed");
10499
+ } catch (err) {
10500
+ const msg = err instanceof Error ? err.message : String(err);
10501
+ logger.warn(`models.dev refresh failed (${msg}); using cached catalog`);
10502
+ }
10503
+ }
10041
10504
  const first = positional[0];
10042
10505
  if (first && subcommands[first]) {
10043
10506
  const container = createDefaultContainer({
@@ -11139,7 +11602,17 @@ async function execute(deps) {
11139
11602
  const visionAdapters = () => createToolVisionAdapters(agent.tools);
11140
11603
  const supportsVision = async () => {
11141
11604
  try {
11142
- const caps = await capabilitiesFor(modelsRegistry, context.provider.id, context.model);
11605
+ const providerConfig = config.providers?.[context.provider.id];
11606
+ const mergedModels = mergeCustomModelDefs(
11607
+ providerConfig?.customModels,
11608
+ config.models
11609
+ );
11610
+ const caps = await capabilitiesFor(
11611
+ modelsRegistry,
11612
+ context.provider.id,
11613
+ context.model,
11614
+ mergedModels
11615
+ );
11143
11616
  return caps.vision;
11144
11617
  } catch {
11145
11618
  return context.provider.capabilities.vision;
@@ -12833,7 +13306,7 @@ async function setupCompaction(params) {
12833
13306
  modelId: config.model ?? context.model
12834
13307
  });
12835
13308
  let autoCompactor;
12836
- if (config.context.autoCompact !== false) {
13309
+ if (config.context.autoCompact !== false && effectiveMaxContext > 0) {
12837
13310
  const auditLevel = resolveAuditLevel(fullConfig ?? config);
12838
13311
  const sessionBridge = providedBridge ?? createSessionEventBridge(sessionWriter, auditLevel);
12839
13312
  autoCompactor = new AutoCompactionMiddleware(
@@ -13424,7 +13897,12 @@ async function main(argv) {
13424
13897
  const modeId = activeMode?.id ?? "default";
13425
13898
  const modePrompt = activeMode?.prompt ?? "";
13426
13899
  const [resolvedCaps, resolvedModel] = await Promise.all([
13427
- capabilitiesFor(modelsRegistry, provider.id, config.model).catch(() => void 0),
13900
+ capabilitiesFor(
13901
+ modelsRegistry,
13902
+ provider.id,
13903
+ config.model,
13904
+ mergeCustomModelDefs(config.providers?.[provider.id]?.customModels, config.models)
13905
+ ).catch(() => void 0),
13428
13906
  modelsRegistry.getModel(config.provider, config.model).catch(() => void 0)
13429
13907
  ]);
13430
13908
  const modelCapabilities = resolvedCaps ? {
@@ -13668,9 +14146,11 @@ async function main(argv) {
13668
14146
  providerId,
13669
14147
  modelId
13670
14148
  });
13671
- effectiveMaxContext = mc > 0 ? mc : 2e5;
14149
+ effectiveMaxContext = mc;
13672
14150
  context.provider.capabilities.maxContext = effectiveMaxContext;
13673
- autoCompactor?.setMaxContext(effectiveMaxContext);
14151
+ if (effectiveMaxContext > 0) {
14152
+ autoCompactor?.setMaxContext(effectiveMaxContext);
14153
+ }
13674
14154
  events.emit("ctx.max_context", { providerId, modelId, maxContext: effectiveMaxContext });
13675
14155
  updateSpinnerContext();
13676
14156
  };