@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/README.md +121 -118
- package/dist/index.js +1157 -122
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
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,
|
|
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
|
|
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
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
4914
|
-
|
|
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 = {
|
|
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
|
-
|
|
4927
|
-
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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
|
|
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
|
|
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,
|
|
5927
|
+
const decrypted = decryptConfigSecrets$1(parsed, noOpVault2);
|
|
5508
5928
|
mutate(decrypted);
|
|
5509
|
-
const encrypted = encryptConfigSecrets$1(decrypted,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
6118
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
6911
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
7250
|
-
|
|
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
|
|
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>
|
|
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
|
|
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
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
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
|
|
10610
|
+
const fs25 = await import('fs/promises');
|
|
9819
10611
|
let entries;
|
|
9820
10612
|
try {
|
|
9821
|
-
entries = await
|
|
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,
|
|
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 ?
|
|
9964
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
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:
|
|
10770
|
+
return { mark: color46.green("\u2713"), stats, tail: "" };
|
|
9968
10771
|
case "timeout":
|
|
9969
|
-
return { mark:
|
|
10772
|
+
return { mark: color46.yellow("\u23F1"), stats: `${color46.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
9970
10773
|
case "stopped":
|
|
9971
|
-
return { mark:
|
|
10774
|
+
return { mark: color46.dim("\u2298"), stats: `${color46.dim("stopped")} ${stats}`, tail: errSnip };
|
|
9972
10775
|
case "failed":
|
|
9973
|
-
return { mark:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
14668
|
+
effectiveMaxContext = mc;
|
|
13672
14669
|
context.provider.capabilities.maxContext = effectiveMaxContext;
|
|
13673
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|