@wrongstack/cli 0.89.3 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1699 -751
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { color, writeErr, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, DefaultSecretScrubber, DefaultPathResolver, EventBus, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, SpecVersioning, atomicWrite, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, bootConfig as bootConfig$1, setOutputLineGuard, 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,
|
|
3
|
-
import * as path8 from 'path';
|
|
4
|
-
import { join } from 'path';
|
|
2
|
+
import { color, writeErr, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, DefaultSecretScrubber, DefaultPathResolver, EventBus, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, SpecVersioning, atomicWrite, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, loadTasks, emptyTaskFile, saveTasks, formatTaskProgress, formatTaskList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, InputBuilder, FsError } from '@wrongstack/core';
|
|
5
3
|
import * as fsp4 from 'fs/promises';
|
|
6
4
|
import { DefaultSecretVault, decryptConfigSecrets, encryptConfigSecrets, isSecretField } from '@wrongstack/core/security';
|
|
5
|
+
import * as path8 from 'path';
|
|
6
|
+
import { join } from 'path';
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import * as os2 from 'os';
|
|
9
9
|
import os2__default from 'os';
|
|
@@ -18,7 +18,7 @@ import { createDefaultContainer, routeImagesForModel, readClipboardImage } from
|
|
|
18
18
|
import { builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes } from '@wrongstack/tools';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import * as readline from 'readline';
|
|
21
|
-
import * as
|
|
21
|
+
import * as fs13 from 'fs';
|
|
22
22
|
import { writeFileSync, existsSync, readFileSync } from 'fs';
|
|
23
23
|
import { WrongStackACPServer } from '@wrongstack/acp/agent';
|
|
24
24
|
import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
|
|
@@ -36,6 +36,102 @@ var __export = (target, all) => {
|
|
|
36
36
|
for (var name in all)
|
|
37
37
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
38
38
|
};
|
|
39
|
+
function normalizeKeys(cfg) {
|
|
40
|
+
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
41
|
+
return cfg.apiKeys.map((k) => ({ ...k }));
|
|
42
|
+
}
|
|
43
|
+
if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
|
|
44
|
+
return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
function writeKeysBack(cfg, keys) {
|
|
49
|
+
if (keys.length === 0) {
|
|
50
|
+
delete cfg.apiKeys;
|
|
51
|
+
delete cfg.apiKey;
|
|
52
|
+
delete cfg.activeKey;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
cfg.apiKeys = keys;
|
|
56
|
+
const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
|
|
57
|
+
cfg.apiKey = active.apiKey;
|
|
58
|
+
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
59
|
+
cfg.activeKey = active.label;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function activeLabel(cfg, keys) {
|
|
63
|
+
if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
|
|
64
|
+
return keys[0]?.label;
|
|
65
|
+
}
|
|
66
|
+
function maskedKey(key) {
|
|
67
|
+
if (!key) return color.dim("\u2014");
|
|
68
|
+
if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
|
|
69
|
+
const head = key.slice(0, 4);
|
|
70
|
+
const tail = key.slice(-4);
|
|
71
|
+
return `${color.dim(head + "\u2026")}${tail}`;
|
|
72
|
+
}
|
|
73
|
+
function nowIso() {
|
|
74
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
75
|
+
}
|
|
76
|
+
async function loadConfigProviders(configPath2, vault, opts) {
|
|
77
|
+
const warn = opts?.warn;
|
|
78
|
+
let raw;
|
|
79
|
+
try {
|
|
80
|
+
raw = await fsp4.readFile(configPath2, "utf8");
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err.code !== "ENOENT") {
|
|
83
|
+
warn?.(`Could not read ${configPath2}: ${err.message}. Treating as empty.`);
|
|
84
|
+
}
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(raw);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
warn?.(`Config at ${configPath2} is not valid JSON: ${err.message}`);
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
const decrypted = decryptConfigSecrets(parsed, vault);
|
|
95
|
+
return decrypted.providers ?? {};
|
|
96
|
+
}
|
|
97
|
+
async function mutateConfigProviders(configPath2, vault, mutator) {
|
|
98
|
+
let raw;
|
|
99
|
+
let fileExists = true;
|
|
100
|
+
try {
|
|
101
|
+
raw = await fsp4.readFile(configPath2, "utf8");
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.code !== "ENOENT") {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Refusing to mutate ${configPath2}: ${err.message}`,
|
|
106
|
+
{ cause: err }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
fileExists = false;
|
|
110
|
+
raw = "{}";
|
|
111
|
+
}
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(raw);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (fileExists) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
|
|
119
|
+
{ cause: err }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
parsed = {};
|
|
123
|
+
}
|
|
124
|
+
const decrypted = decryptConfigSecrets(parsed, vault);
|
|
125
|
+
const providers = decrypted.providers ?? {};
|
|
126
|
+
mutator(providers);
|
|
127
|
+
decrypted.providers = providers;
|
|
128
|
+
const encrypted = encryptConfigSecrets(decrypted, vault);
|
|
129
|
+
await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
130
|
+
}
|
|
131
|
+
var init_provider_config_utils = __esm({
|
|
132
|
+
"src/provider-config-utils.ts"() {
|
|
133
|
+
}
|
|
134
|
+
});
|
|
39
135
|
function getSessionState(ctx) {
|
|
40
136
|
if (!ctx) return sddState;
|
|
41
137
|
let state = ctx.meta[SDD_META_KEY];
|
|
@@ -1496,102 +1592,6 @@ var init_sdd = __esm({
|
|
|
1496
1592
|
init_rendering();
|
|
1497
1593
|
}
|
|
1498
1594
|
});
|
|
1499
|
-
function normalizeKeys(cfg) {
|
|
1500
|
-
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
1501
|
-
return cfg.apiKeys.map((k) => ({ ...k }));
|
|
1502
|
-
}
|
|
1503
|
-
if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
|
|
1504
|
-
return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
|
|
1505
|
-
}
|
|
1506
|
-
return [];
|
|
1507
|
-
}
|
|
1508
|
-
function writeKeysBack(cfg, keys) {
|
|
1509
|
-
if (keys.length === 0) {
|
|
1510
|
-
delete cfg.apiKeys;
|
|
1511
|
-
delete cfg.apiKey;
|
|
1512
|
-
delete cfg.activeKey;
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
cfg.apiKeys = keys;
|
|
1516
|
-
const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
|
|
1517
|
-
cfg.apiKey = active.apiKey;
|
|
1518
|
-
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
1519
|
-
cfg.activeKey = active.label;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
function activeLabel(cfg, keys) {
|
|
1523
|
-
if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
|
|
1524
|
-
return keys[0]?.label;
|
|
1525
|
-
}
|
|
1526
|
-
function maskedKey(key) {
|
|
1527
|
-
if (!key) return color.dim("\u2014");
|
|
1528
|
-
if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
|
|
1529
|
-
const head = key.slice(0, 4);
|
|
1530
|
-
const tail = key.slice(-4);
|
|
1531
|
-
return `${color.dim(head + "\u2026")}${tail}`;
|
|
1532
|
-
}
|
|
1533
|
-
function nowIso() {
|
|
1534
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1535
|
-
}
|
|
1536
|
-
async function loadConfigProviders(configPath2, vault, opts) {
|
|
1537
|
-
const warn = opts?.warn;
|
|
1538
|
-
let raw;
|
|
1539
|
-
try {
|
|
1540
|
-
raw = await fsp4.readFile(configPath2, "utf8");
|
|
1541
|
-
} catch (err) {
|
|
1542
|
-
if (err.code !== "ENOENT") {
|
|
1543
|
-
warn?.(`Could not read ${configPath2}: ${err.message}. Treating as empty.`);
|
|
1544
|
-
}
|
|
1545
|
-
return {};
|
|
1546
|
-
}
|
|
1547
|
-
let parsed;
|
|
1548
|
-
try {
|
|
1549
|
-
parsed = JSON.parse(raw);
|
|
1550
|
-
} catch (err) {
|
|
1551
|
-
warn?.(`Config at ${configPath2} is not valid JSON: ${err.message}`);
|
|
1552
|
-
return {};
|
|
1553
|
-
}
|
|
1554
|
-
const decrypted = decryptConfigSecrets(parsed, vault);
|
|
1555
|
-
return decrypted.providers ?? {};
|
|
1556
|
-
}
|
|
1557
|
-
async function mutateConfigProviders(configPath2, vault, mutator) {
|
|
1558
|
-
let raw;
|
|
1559
|
-
let fileExists = true;
|
|
1560
|
-
try {
|
|
1561
|
-
raw = await fsp4.readFile(configPath2, "utf8");
|
|
1562
|
-
} catch (err) {
|
|
1563
|
-
if (err.code !== "ENOENT") {
|
|
1564
|
-
throw new Error(
|
|
1565
|
-
`Refusing to mutate ${configPath2}: ${err.message}`,
|
|
1566
|
-
{ cause: err }
|
|
1567
|
-
);
|
|
1568
|
-
}
|
|
1569
|
-
fileExists = false;
|
|
1570
|
-
raw = "{}";
|
|
1571
|
-
}
|
|
1572
|
-
let parsed;
|
|
1573
|
-
try {
|
|
1574
|
-
parsed = JSON.parse(raw);
|
|
1575
|
-
} catch (err) {
|
|
1576
|
-
if (fileExists) {
|
|
1577
|
-
throw new Error(
|
|
1578
|
-
`Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
|
|
1579
|
-
{ cause: err }
|
|
1580
|
-
);
|
|
1581
|
-
}
|
|
1582
|
-
parsed = {};
|
|
1583
|
-
}
|
|
1584
|
-
const decrypted = decryptConfigSecrets(parsed, vault);
|
|
1585
|
-
const providers = decrypted.providers ?? {};
|
|
1586
|
-
mutator(providers);
|
|
1587
|
-
decrypted.providers = providers;
|
|
1588
|
-
const encrypted = encryptConfigSecrets(decrypted, vault);
|
|
1589
|
-
await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
1590
|
-
}
|
|
1591
|
-
var init_provider_config_utils = __esm({
|
|
1592
|
-
"src/provider-config-utils.ts"() {
|
|
1593
|
-
}
|
|
1594
|
-
});
|
|
1595
1595
|
|
|
1596
1596
|
// src/update-check.ts
|
|
1597
1597
|
var update_check_exports = {};
|
|
@@ -2888,212 +2888,147 @@ function estimateTokens(messages) {
|
|
|
2888
2888
|
}
|
|
2889
2889
|
return total;
|
|
2890
2890
|
}
|
|
2891
|
-
|
|
2891
|
+
|
|
2892
|
+
// src/slash-commands/auth.ts
|
|
2893
|
+
init_provider_config_utils();
|
|
2894
|
+
function buildAuthCommand(opts) {
|
|
2895
|
+
const help = [
|
|
2896
|
+
"Usage:",
|
|
2897
|
+
" /auth Show saved providers and key status",
|
|
2898
|
+
" /auth status <provider> Show detail for one provider",
|
|
2899
|
+
" /auth open Show how to launch the interactive menu",
|
|
2900
|
+
"",
|
|
2901
|
+
"Run `wstack auth` for the full interactive key manager (add, edit, delete)."
|
|
2902
|
+
].join("\n");
|
|
2892
2903
|
return {
|
|
2893
|
-
name: "
|
|
2894
|
-
category: "
|
|
2895
|
-
description: "
|
|
2896
|
-
help
|
|
2897
|
-
"Usage:",
|
|
2898
|
-
" /autonomy Show current autonomy status",
|
|
2899
|
-
" /autonomy off Disabled \u2014 agent stops after each turn (default)",
|
|
2900
|
-
" /autonomy suggest Show next-step suggestions after each turn",
|
|
2901
|
-
" /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
|
|
2902
|
-
" /autonomy eternal Goal-driven loop \u2014 runs forever against /goal",
|
|
2903
|
-
" (prompts to confirm an existing goal; `--keep` to skip prompt)",
|
|
2904
|
-
" /autonomy parallel Parallel mode \u2014 4-8 agents per tick, fan-out parallelism",
|
|
2905
|
-
" (prompts to confirm an existing goal; `--keep` to skip prompt)",
|
|
2906
|
-
" /autonomy stop Stop eternal mode (no-op for other modes)",
|
|
2907
|
-
" /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 parallel \u2192 off",
|
|
2908
|
-
"",
|
|
2909
|
-
"Modes:",
|
|
2910
|
-
" off \u2014 Normal interactive mode. Agent stops and waits.",
|
|
2911
|
-
" suggest \u2014 After each turn, agent suggests next steps. You pick.",
|
|
2912
|
-
" auto \u2014 After each turn, agent picks the best next step and continues.",
|
|
2913
|
-
" Runs indefinitely until you press Esc or Ctrl+C.",
|
|
2914
|
-
" eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
|
|
2915
|
-
" Force-enables regular YOLO; destructive-gated calls still use",
|
|
2916
|
-
" the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
|
|
2917
|
-
" parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
|
|
2918
|
-
" spawns N agents, awaits results, aggregates. Requires /goal.",
|
|
2919
|
-
" Force-enables regular YOLO; destructive-gated calls still use",
|
|
2920
|
-
" the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
|
|
2921
|
-
"",
|
|
2922
|
-
"Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
|
|
2923
|
-
"Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
|
|
2924
|
-
"",
|
|
2925
|
-
"In auto/eternal/parallel modes the agent works autonomously. Press Esc to redirect,",
|
|
2926
|
-
"Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
|
|
2927
|
-
].join("\n"),
|
|
2904
|
+
name: "auth",
|
|
2905
|
+
category: "Config",
|
|
2906
|
+
description: "View API key status. Run wstack auth for the full interactive key manager.",
|
|
2907
|
+
help,
|
|
2928
2908
|
async run(args) {
|
|
2929
|
-
const parts = args.trim().
|
|
2930
|
-
const
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
const msg2 = "Autonomy mode is not available in this session.";
|
|
2934
|
-
opts.renderer.writeWarning(msg2);
|
|
2935
|
-
return { message: msg2 };
|
|
2909
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
2910
|
+
const sub = (parts[0] ?? "").toLowerCase();
|
|
2911
|
+
if (sub === "help" || sub === "--help") {
|
|
2912
|
+
return { message: this.help ?? "" };
|
|
2936
2913
|
}
|
|
2937
|
-
if (!
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2914
|
+
if (!opts.paths?.globalConfig) {
|
|
2915
|
+
return { message: `${color.red("Error")} auth not available \u2014 config path missing.` };
|
|
2916
|
+
}
|
|
2917
|
+
if (sub === "open") {
|
|
2918
|
+
return {
|
|
2919
|
+
message: [
|
|
2920
|
+
`${color.bold("API Key Manager")}`,
|
|
2921
|
+
"",
|
|
2922
|
+
` Run ${color.bold("wstack auth")} in a separate terminal to manage API keys interactively:`,
|
|
2923
|
+
"",
|
|
2924
|
+
` ${color.cyan("wstack auth")} Interactive menu`,
|
|
2925
|
+
` ${color.cyan("wstack auth <provider>")} Add a key for <provider>`,
|
|
2926
|
+
` ${color.cyan("wstack auth <p> --label <l>")} Add with custom label`,
|
|
2927
|
+
"",
|
|
2928
|
+
color.dim(" The interactive menu requires standard input (readline) which is not"),
|
|
2929
|
+
color.dim(" available inside the WrongStack session REPL.")
|
|
2930
|
+
].join("\n")
|
|
2945
2931
|
};
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
)
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
` Engine state: ${goal.engineState} \xB7 iterations: ${goal.iterations} \xB7 journal: ${goal.journal.length}`
|
|
2959
|
-
)
|
|
2960
|
-
);
|
|
2961
|
-
if (u.iterationsWithUsage > 0) {
|
|
2962
|
-
lines.push(
|
|
2963
|
-
color.dim(
|
|
2964
|
-
` Spent: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens`
|
|
2965
|
-
)
|
|
2966
|
-
);
|
|
2967
|
-
}
|
|
2968
|
-
const recent = goal.journal.slice(-10);
|
|
2969
|
-
const failed = recent.filter((e) => e.status === "failure").length;
|
|
2970
|
-
if (failed > 0) {
|
|
2971
|
-
lines.push(
|
|
2972
|
-
color.amber(` Recent failures: ${failed} of last ${recent.length} iterations`)
|
|
2973
|
-
);
|
|
2974
|
-
}
|
|
2932
|
+
}
|
|
2933
|
+
let providers;
|
|
2934
|
+
try {
|
|
2935
|
+
providers = await loadConfigProviders(
|
|
2936
|
+
opts.paths.globalConfig,
|
|
2937
|
+
// We don't have a full vault reference in slash commands;
|
|
2938
|
+
// use a simple passthrough vault since keys won't decrypt anyway
|
|
2939
|
+
// in this read-only view and the config may not have encrypted fields.
|
|
2940
|
+
{
|
|
2941
|
+
encrypt: (v) => v,
|
|
2942
|
+
decrypt: (v) => v,
|
|
2943
|
+
isEncrypted: () => false
|
|
2975
2944
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
opts.renderer.write(msg2);
|
|
2980
|
-
return { message: msg2 };
|
|
2945
|
+
);
|
|
2946
|
+
} catch {
|
|
2947
|
+
return { message: `${color.red("Error")} could not read config file.` };
|
|
2981
2948
|
}
|
|
2982
|
-
if (
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
return { message: msg3 };
|
|
2949
|
+
if (sub === "status") {
|
|
2950
|
+
const pid = parts[1];
|
|
2951
|
+
if (!pid) {
|
|
2952
|
+
return { message: `${color.amber("Usage:")} /auth status <provider>` };
|
|
2987
2953
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
opts.onAutonomy("off");
|
|
2992
|
-
let summaryLine = "";
|
|
2993
|
-
try {
|
|
2994
|
-
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
2995
|
-
if (goal) {
|
|
2996
|
-
const u = summarizeUsage(goal);
|
|
2997
|
-
if (u.iterationsWithUsage > 0) {
|
|
2998
|
-
summaryLine = "\n" + color.dim(
|
|
2999
|
-
` Spent so far: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens \xB7 ${goal.iterations} total iterations.`
|
|
3000
|
-
);
|
|
3001
|
-
} else if (goal.iterations > 0) {
|
|
3002
|
-
summaryLine = "\n" + color.dim(` Total iterations: ${goal.iterations}.`);
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
} catch {
|
|
2954
|
+
const cfg = providers[pid];
|
|
2955
|
+
if (!cfg) {
|
|
2956
|
+
return { message: `${color.yellow("Provider")} "${pid}" not found in saved config.` };
|
|
3006
2957
|
}
|
|
3007
|
-
const
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
} else if (arg === "parallel" || arg === "eternal-parallel" || arg === "fanout") {
|
|
3021
|
-
newMode = "eternal-parallel";
|
|
3022
|
-
} else if (arg === "toggle" || arg === "cycle") {
|
|
3023
|
-
const current = opts.onAutonomy() ?? "off";
|
|
3024
|
-
const cycle = ["off", "suggest", "auto", "eternal", "eternal-parallel"];
|
|
3025
|
-
newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
|
|
3026
|
-
} else {
|
|
3027
|
-
const msg2 = `Unknown argument: ${arg}. Use /autonomy on, off, suggest, eternal, parallel, stop, or toggle.`;
|
|
3028
|
-
opts.renderer.writeWarning(msg2);
|
|
3029
|
-
return { message: msg2 };
|
|
3030
|
-
}
|
|
3031
|
-
if (newMode === "eternal" || newMode === "eternal-parallel") {
|
|
3032
|
-
const wantKeep = modifiers.includes("--keep") || modifiers.includes("keep");
|
|
3033
|
-
const wantNew = modifiers.includes("--new") || modifiers.includes("new");
|
|
3034
|
-
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
3035
|
-
if (!goal) {
|
|
3036
|
-
const msg3 = `${color.red("Eternal/parallel mode requires a goal.")} Run \`/goal set <mission>\` first.`;
|
|
3037
|
-
opts.renderer.writeWarning(msg3);
|
|
3038
|
-
return { message: msg3 };
|
|
2958
|
+
const keys = cfg.apiKeys ?? [];
|
|
2959
|
+
const active = keys.find(
|
|
2960
|
+
(k) => cfg && cfg.activeKey === k.label
|
|
2961
|
+
) ?? keys[0];
|
|
2962
|
+
const lines2 = [
|
|
2963
|
+
`${color.bold(pid)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}`,
|
|
2964
|
+
"",
|
|
2965
|
+
` type: ${color.cyan(cfg.type ?? pid)}`,
|
|
2966
|
+
` family: ${cfg.family ? color.cyan(cfg.family) : color.dim("unset")}`,
|
|
2967
|
+
` baseUrl: ${cfg.baseUrl ? color.cyan(cfg.baseUrl) : color.dim("unset")}`
|
|
2968
|
+
];
|
|
2969
|
+
if (cfg.models?.length) {
|
|
2970
|
+
lines2.push(` models: ${color.cyan(cfg.models.join(", "))}`);
|
|
3039
2971
|
}
|
|
3040
|
-
if (
|
|
3041
|
-
|
|
3042
|
-
opts.renderer.writeWarning(msg3);
|
|
3043
|
-
return { message: msg3 };
|
|
2972
|
+
if (cfg.envVars?.length) {
|
|
2973
|
+
lines2.push(` envVars: ${color.cyan(cfg.envVars.join(", "))}`);
|
|
3044
2974
|
}
|
|
3045
|
-
|
|
3046
|
-
if (
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
}
|
|
3057
|
-
if (!answer) {
|
|
3058
|
-
const msg3 = `${color.amber("Skipped.")} To start a new mission: ${color.bold("/goal clear")} \u2192 ${color.bold("/goal set <mission>")} \u2192 ${color.bold(`/autonomy ${newMode}`)}. To force the existing one: ${color.bold(`/autonomy ${newMode} --keep`)}.`;
|
|
3059
|
-
opts.renderer.write(msg3);
|
|
3060
|
-
return { message: msg3 };
|
|
3061
|
-
}
|
|
3062
|
-
} else if (isStale) {
|
|
3063
|
-
const msg3 = `${color.amber("Stale goal detected.")} Previous mission has ${goal.iterations} iterations (engineState: ${goal.engineState}). Clear it first: ${color.bold("/goal clear")}, then set a new one: ${color.bold("/goal set <mission>")}.`;
|
|
3064
|
-
opts.renderer.writeWarning(msg3);
|
|
3065
|
-
return { message: msg3 };
|
|
2975
|
+
lines2.push("");
|
|
2976
|
+
if (keys.length === 0) {
|
|
2977
|
+
lines2.push(color.dim(" (no keys saved)"));
|
|
2978
|
+
} else {
|
|
2979
|
+
lines2.push(` ${color.dim("Keys:")}`);
|
|
2980
|
+
for (const k of keys) {
|
|
2981
|
+
const marker = k.label === active?.label ? color.green("\u25CF") : color.dim("\u25CB");
|
|
2982
|
+
const masked = k.label === active?.label ? color.dim("(active \u2014 masked)") : color.dim("(masked)");
|
|
2983
|
+
lines2.push(
|
|
2984
|
+
` ${marker} ${color.bold(k.label.padEnd(18))} ${masked} ${color.dim(k.createdAt)}`
|
|
2985
|
+
);
|
|
3066
2986
|
}
|
|
3067
2987
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
opts.renderer.writeWarning(msg3);
|
|
3071
|
-
return { message: msg3 };
|
|
3072
|
-
}
|
|
3073
|
-
if (opts.onYolo) opts.onYolo(true);
|
|
3074
|
-
opts.onAutonomy(newMode);
|
|
3075
|
-
opts.onEternalStart(newMode);
|
|
3076
|
-
const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
|
|
3077
|
-
const msg2 = `Autonomy mode: ${modeLabel} \u2014 engine launching against goal: ${color.bold(goal.goal)}
|
|
3078
|
-
${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
|
|
3079
|
-
opts.renderer.write(msg2);
|
|
3080
|
-
return { message: msg2 };
|
|
2988
|
+
lines2.push("", color.dim(` Manage: wstack auth \u2192 pick ${pid}`));
|
|
2989
|
+
return { message: lines2.join("\n") };
|
|
3081
2990
|
}
|
|
3082
|
-
const
|
|
3083
|
-
if (
|
|
3084
|
-
|
|
2991
|
+
const ids = Object.keys(providers).sort();
|
|
2992
|
+
if (ids.length === 0) {
|
|
2993
|
+
return {
|
|
2994
|
+
message: [
|
|
2995
|
+
`${color.bold("API Keys")} ${color.dim("\u2014 No providers configured")}`,
|
|
2996
|
+
"",
|
|
2997
|
+
color.dim(" Run `wstack auth` to add a provider with an API key."),
|
|
2998
|
+
"",
|
|
2999
|
+
color.dim(" Quick start:"),
|
|
3000
|
+
` ${color.cyan("wstack auth")} Interactive menu`,
|
|
3001
|
+
` ${color.cyan("wstack auth anthropic")} Direct add`,
|
|
3002
|
+
"",
|
|
3003
|
+
color.dim(" Or /auth help for more commands.")
|
|
3004
|
+
].join("\n")
|
|
3005
|
+
};
|
|
3085
3006
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3007
|
+
const lines = [
|
|
3008
|
+
`${color.bold("API Keys")} ${color.dim(`\u2014 ${ids.length} provider${ids.length === 1 ? "" : "s"}`)}`,
|
|
3009
|
+
""
|
|
3010
|
+
];
|
|
3011
|
+
for (const id of ids) {
|
|
3012
|
+
const cfg = providers[id];
|
|
3013
|
+
if (!cfg) continue;
|
|
3014
|
+
const keys = cfg.apiKeys ?? [];
|
|
3015
|
+
const famTag = cfg.family ? color.dim(`[${cfg.family}]`) : "";
|
|
3016
|
+
const aliasTag = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
|
|
3017
|
+
let status;
|
|
3018
|
+
if (keys.length === 0) {
|
|
3019
|
+
status = color.amber("no keys");
|
|
3020
|
+
} else if (keys.length === 1) {
|
|
3021
|
+
status = color.green(`1 key`);
|
|
3022
|
+
} else {
|
|
3023
|
+
status = color.green(`${keys.length} keys`);
|
|
3024
|
+
}
|
|
3025
|
+
lines.push(
|
|
3026
|
+
` ${color.bold(id.padEnd(22))} ${famTag} ${aliasTag} ${status}`
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
3029
|
+
lines.push("");
|
|
3030
|
+
lines.push(color.dim(" /auth status <id> Detail /auth open Full menu"));
|
|
3031
|
+
return { message: lines.join("\n") };
|
|
3097
3032
|
}
|
|
3098
3033
|
};
|
|
3099
3034
|
}
|
|
@@ -3255,43 +3190,252 @@ function buildAutoPhaseCommand(opts) {
|
|
|
3255
3190
|
}
|
|
3256
3191
|
};
|
|
3257
3192
|
}
|
|
3258
|
-
function
|
|
3193
|
+
function buildAutonomyCommand(opts) {
|
|
3259
3194
|
return {
|
|
3260
|
-
name: "
|
|
3195
|
+
name: "autonomy",
|
|
3261
3196
|
category: "Agent",
|
|
3262
|
-
description:
|
|
3263
|
-
argsHint: "<note>",
|
|
3197
|
+
description: "Toggle or query autonomy mode (self-driving agent).",
|
|
3264
3198
|
help: [
|
|
3265
|
-
"
|
|
3266
|
-
"
|
|
3267
|
-
"/
|
|
3199
|
+
"Usage:",
|
|
3200
|
+
" /autonomy Show current autonomy status",
|
|
3201
|
+
" /autonomy off Disabled \u2014 agent stops after each turn (default)",
|
|
3202
|
+
" /autonomy suggest Show next-step suggestions after each turn",
|
|
3203
|
+
" /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
|
|
3204
|
+
" /autonomy eternal Goal-driven loop \u2014 runs forever against /goal",
|
|
3205
|
+
" (prompts to confirm an existing goal; `--keep` to skip prompt)",
|
|
3206
|
+
" /autonomy parallel Parallel mode \u2014 4-8 agents per tick, fan-out parallelism",
|
|
3207
|
+
" (prompts to confirm an existing goal; `--keep` to skip prompt)",
|
|
3208
|
+
" /autonomy stop Stop eternal mode (no-op for other modes)",
|
|
3209
|
+
" /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 parallel \u2192 off",
|
|
3268
3210
|
"",
|
|
3269
|
-
"
|
|
3211
|
+
"Modes:",
|
|
3212
|
+
" off \u2014 Normal interactive mode. Agent stops and waits.",
|
|
3213
|
+
" suggest \u2014 After each turn, agent suggests next steps. You pick.",
|
|
3214
|
+
" auto \u2014 After each turn, agent picks the best next step and continues.",
|
|
3215
|
+
" Runs indefinitely until you press Esc or Ctrl+C.",
|
|
3216
|
+
" eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
|
|
3217
|
+
" Force-enables regular YOLO; destructive-gated calls still use",
|
|
3218
|
+
" the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
|
|
3219
|
+
" parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
|
|
3220
|
+
" spawns N agents, awaits results, aggregates. Requires /goal.",
|
|
3221
|
+
" Force-enables regular YOLO; destructive-gated calls still use",
|
|
3222
|
+
" the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
|
|
3223
|
+
"",
|
|
3224
|
+
"Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
|
|
3225
|
+
"Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
|
|
3226
|
+
"",
|
|
3227
|
+
"In auto/eternal/parallel modes the agent works autonomously. Press Esc to redirect,",
|
|
3228
|
+
"Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
|
|
3270
3229
|
].join("\n"),
|
|
3271
3230
|
async run(args) {
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
|
|
3231
|
+
const parts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
3232
|
+
const arg = parts[0] ?? "";
|
|
3233
|
+
const modifiers = parts.slice(1);
|
|
3234
|
+
if (!opts.onAutonomy) {
|
|
3235
|
+
const msg2 = "Autonomy mode is not available in this session.";
|
|
3236
|
+
opts.renderer.writeWarning(msg2);
|
|
3237
|
+
return { message: msg2 };
|
|
3275
3238
|
}
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
const
|
|
3279
|
-
|
|
3280
|
-
|
|
3239
|
+
if (!arg || arg === "status") {
|
|
3240
|
+
const current = opts.onAutonomy();
|
|
3241
|
+
const labels2 = {
|
|
3242
|
+
off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
|
|
3243
|
+
suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
|
|
3244
|
+
auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`,
|
|
3245
|
+
eternal: `${color.red("ETERNAL")} ${color.dim("(goal-driven loop \u2014 YOLO, until /autonomy stop)")}`,
|
|
3246
|
+
"eternal-parallel": `${color.magenta("PARALLEL")} ${color.dim("(4-8 subagents per tick \u2014 fan-out, until /autonomy stop)")}`
|
|
3281
3247
|
};
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3248
|
+
const lines = [`Autonomy mode: ${labels2[current] ?? current}`];
|
|
3249
|
+
try {
|
|
3250
|
+
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
3251
|
+
if (goal) {
|
|
3252
|
+
const u = summarizeUsage(goal);
|
|
3253
|
+
lines.push(
|
|
3254
|
+
color.dim(
|
|
3255
|
+
` Goal: ${goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal}`
|
|
3256
|
+
)
|
|
3257
|
+
);
|
|
3258
|
+
lines.push(
|
|
3259
|
+
color.dim(
|
|
3260
|
+
` Engine state: ${goal.engineState} \xB7 iterations: ${goal.iterations} \xB7 journal: ${goal.journal.length}`
|
|
3261
|
+
)
|
|
3262
|
+
);
|
|
3263
|
+
if (u.iterationsWithUsage > 0) {
|
|
3264
|
+
lines.push(
|
|
3265
|
+
color.dim(
|
|
3266
|
+
` Spent: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens`
|
|
3267
|
+
)
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
const recent = goal.journal.slice(-10);
|
|
3271
|
+
const failed = recent.filter((e) => e.status === "failure").length;
|
|
3272
|
+
if (failed > 0) {
|
|
3273
|
+
lines.push(
|
|
3274
|
+
color.amber(` Recent failures: ${failed} of last ${recent.length} iterations`)
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
} catch {
|
|
3279
|
+
}
|
|
3280
|
+
const msg2 = lines.join("\n");
|
|
3281
|
+
opts.renderer.write(msg2);
|
|
3282
|
+
return { message: msg2 };
|
|
3283
|
+
}
|
|
3284
|
+
if (arg === "stop" || arg === "halt" || arg === "kill") {
|
|
3285
|
+
if (!opts.onEternalStop) {
|
|
3286
|
+
const msg3 = "No eternal-mode controller wired in this session.";
|
|
3287
|
+
opts.renderer.writeWarning(msg3);
|
|
3288
|
+
return { message: msg3 };
|
|
3289
|
+
}
|
|
3290
|
+
opts.getEternalEngine?.()?.stop();
|
|
3291
|
+
opts.getParallelEngine?.()?.stop();
|
|
3292
|
+
opts.onEternalStop();
|
|
3293
|
+
opts.onAutonomy("off");
|
|
3294
|
+
let summaryLine = "";
|
|
3295
|
+
try {
|
|
3296
|
+
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
3297
|
+
if (goal) {
|
|
3298
|
+
const u = summarizeUsage(goal);
|
|
3299
|
+
if (u.iterationsWithUsage > 0) {
|
|
3300
|
+
summaryLine = "\n" + color.dim(
|
|
3301
|
+
` Spent so far: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens \xB7 ${goal.iterations} total iterations.`
|
|
3302
|
+
);
|
|
3303
|
+
} else if (goal.iterations > 0) {
|
|
3304
|
+
summaryLine = "\n" + color.dim(` Total iterations: ${goal.iterations}.`);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
} catch {
|
|
3308
|
+
}
|
|
3309
|
+
const msg2 = `${color.amber("Eternal/parallel mode stop requested.")} In-flight eternal work is cancelled; parallel fan-out stops after the current tick cleans up.${summaryLine}`;
|
|
3310
|
+
opts.renderer.write(msg2);
|
|
3311
|
+
return { message: msg2 };
|
|
3312
|
+
}
|
|
3313
|
+
let newMode;
|
|
3314
|
+
if (arg === "on" || arg === "enable" || arg === "true" || arg === "auto") {
|
|
3315
|
+
newMode = "auto";
|
|
3316
|
+
} else if (arg === "off" || arg === "disable" || arg === "false") {
|
|
3317
|
+
newMode = "off";
|
|
3318
|
+
} else if (arg === "suggest" || arg === "suggestions") {
|
|
3319
|
+
newMode = "suggest";
|
|
3320
|
+
} else if (arg === "eternal" || arg === "forever" || arg === "infinite" || arg === "sittinsene") {
|
|
3321
|
+
newMode = "eternal";
|
|
3322
|
+
} else if (arg === "parallel" || arg === "eternal-parallel" || arg === "fanout") {
|
|
3323
|
+
newMode = "eternal-parallel";
|
|
3324
|
+
} else if (arg === "toggle" || arg === "cycle") {
|
|
3325
|
+
const current = opts.onAutonomy() ?? "off";
|
|
3326
|
+
const cycle = ["off", "suggest", "auto", "eternal", "eternal-parallel"];
|
|
3327
|
+
newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
|
|
3328
|
+
} else {
|
|
3329
|
+
const msg2 = `Unknown argument: ${arg}. Use /autonomy on, off, suggest, eternal, parallel, stop, or toggle.`;
|
|
3330
|
+
opts.renderer.writeWarning(msg2);
|
|
3331
|
+
return { message: msg2 };
|
|
3332
|
+
}
|
|
3333
|
+
if (newMode === "eternal" || newMode === "eternal-parallel") {
|
|
3334
|
+
const wantKeep = modifiers.includes("--keep") || modifiers.includes("keep");
|
|
3335
|
+
const wantNew = modifiers.includes("--new") || modifiers.includes("new");
|
|
3336
|
+
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
3337
|
+
if (!goal) {
|
|
3338
|
+
const msg3 = `${color.red("Eternal/parallel mode requires a goal.")} Run \`/goal set <mission>\` first.`;
|
|
3339
|
+
opts.renderer.writeWarning(msg3);
|
|
3340
|
+
return { message: msg3 };
|
|
3341
|
+
}
|
|
3342
|
+
if (wantNew) {
|
|
3343
|
+
const msg3 = `${color.amber("New mission requested.")} Clear the current goal first: ${color.bold("/goal clear")}, then ${color.bold("/goal set <mission>")}, then re-run ${color.bold(`/autonomy ${newMode}`)}.`;
|
|
3344
|
+
opts.renderer.writeWarning(msg3);
|
|
3345
|
+
return { message: msg3 };
|
|
3346
|
+
}
|
|
3347
|
+
const isStale = goal.iterations > 0 || goal.engineState === "running";
|
|
3348
|
+
if (!wantKeep) {
|
|
3349
|
+
if (opts.confirm) {
|
|
3350
|
+
const goalPreview = goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal;
|
|
3351
|
+
const detail = isStale ? `${color.amber("Stale goal")} (${goal.iterations} iterations, engineState: ${goal.engineState}): "${goalPreview}". Continue with this mission?` : `Existing goal: "${goalPreview}". Use this mission?`;
|
|
3352
|
+
const defaultYes = !isStale;
|
|
3353
|
+
const answer = await opts.confirm(detail, defaultYes);
|
|
3354
|
+
if (answer === null) {
|
|
3355
|
+
const msg3 = `${color.dim("Cancelled.")} Autonomy mode unchanged.`;
|
|
3356
|
+
opts.renderer.write(msg3);
|
|
3357
|
+
return { message: msg3 };
|
|
3358
|
+
}
|
|
3359
|
+
if (!answer) {
|
|
3360
|
+
const msg3 = `${color.amber("Skipped.")} To start a new mission: ${color.bold("/goal clear")} \u2192 ${color.bold("/goal set <mission>")} \u2192 ${color.bold(`/autonomy ${newMode}`)}. To force the existing one: ${color.bold(`/autonomy ${newMode} --keep`)}.`;
|
|
3361
|
+
opts.renderer.write(msg3);
|
|
3362
|
+
return { message: msg3 };
|
|
3363
|
+
}
|
|
3364
|
+
} else if (isStale) {
|
|
3365
|
+
const msg3 = `${color.amber("Stale goal detected.")} Previous mission has ${goal.iterations} iterations (engineState: ${goal.engineState}). Clear it first: ${color.bold("/goal clear")}, then set a new one: ${color.bold("/goal set <mission>")}.`;
|
|
3366
|
+
opts.renderer.writeWarning(msg3);
|
|
3367
|
+
return { message: msg3 };
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
if (!opts.onEternalStart) {
|
|
3371
|
+
const msg3 = "Eternal mode controller is not wired in this session.";
|
|
3372
|
+
opts.renderer.writeWarning(msg3);
|
|
3373
|
+
return { message: msg3 };
|
|
3374
|
+
}
|
|
3375
|
+
if (opts.onYolo) opts.onYolo(true);
|
|
3376
|
+
opts.onAutonomy(newMode);
|
|
3377
|
+
opts.onEternalStart(newMode);
|
|
3378
|
+
const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
|
|
3379
|
+
const msg2 = `Autonomy mode: ${modeLabel} \u2014 engine launching against goal: ${color.bold(goal.goal)}
|
|
3380
|
+
${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
|
|
3381
|
+
opts.renderer.write(msg2);
|
|
3382
|
+
return { message: msg2 };
|
|
3383
|
+
}
|
|
3384
|
+
const previous = opts.onAutonomy();
|
|
3385
|
+
if ((previous === "eternal" || previous === "eternal-parallel") && opts.onEternalStop) {
|
|
3386
|
+
opts.onEternalStop();
|
|
3387
|
+
}
|
|
3388
|
+
opts.onAutonomy(newMode);
|
|
3389
|
+
const labels = {
|
|
3390
|
+
off: `${color.green("OFF")} \u2014 agent stops after each turn`,
|
|
3391
|
+
suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
|
|
3392
|
+
auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`,
|
|
3393
|
+
eternal: `${color.red("ETERNAL")} \u2014 goal-driven sittin-sene loop`,
|
|
3394
|
+
"eternal-parallel": `${color.magenta("PARALLEL")} \u2014 fan-out 4-8 subagents per tick`
|
|
3395
|
+
};
|
|
3396
|
+
const msg = `Autonomy mode: ${labels[newMode]}`;
|
|
3397
|
+
opts.renderer.write(msg);
|
|
3398
|
+
return { message: msg };
|
|
3399
|
+
}
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
function buildBtwCommand(opts) {
|
|
3403
|
+
return {
|
|
3404
|
+
name: "btw",
|
|
3405
|
+
category: "Agent",
|
|
3406
|
+
description: 'Drop a "by the way" note for the running agent without interrupting it \u2014 delivered at the next step',
|
|
3407
|
+
argsHint: "<note>",
|
|
3408
|
+
help: [
|
|
3409
|
+
"/btw <note> Stash a note; the agent reads it at the start of its next",
|
|
3410
|
+
" iteration (between tool calls) without restarting.",
|
|
3411
|
+
"/btw Show how many notes are pending.",
|
|
3412
|
+
"",
|
|
3413
|
+
"Use `/steer` instead when you need to abort the current work immediately."
|
|
3414
|
+
].join("\n"),
|
|
3415
|
+
async run(args) {
|
|
3416
|
+
const ctx = opts.context;
|
|
3417
|
+
if (!ctx) {
|
|
3418
|
+
return { message: "No active session \u2014 start a turn first, then use /btw to nudge it." };
|
|
3419
|
+
}
|
|
3420
|
+
const text = args.trim();
|
|
3421
|
+
if (!text) {
|
|
3422
|
+
const n = pendingBtwCount(ctx);
|
|
3423
|
+
return {
|
|
3424
|
+
message: n === 0 ? "No notes pending. Usage: /btw <note>" : `${n} note(s) pending \u2014 will reach the agent at its next step.`
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
const pending = setBtwNote(ctx, text);
|
|
3428
|
+
return {
|
|
3429
|
+
message: `\u21AF Noted (${pending} pending) \u2014 the agent will fold this in at its next step:
|
|
3430
|
+
${text}`
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// src/slash-commands/clear.ts
|
|
3437
|
+
function buildClearCommand(opts) {
|
|
3438
|
+
return {
|
|
3295
3439
|
name: "clear",
|
|
3296
3440
|
category: "Session",
|
|
3297
3441
|
description: "Reset the session and start a new one.",
|
|
@@ -5131,6 +5275,112 @@ function buildFleetCommand(opts) {
|
|
|
5131
5275
|
}
|
|
5132
5276
|
};
|
|
5133
5277
|
}
|
|
5278
|
+
|
|
5279
|
+
// src/slash-commands/goal-refiner.ts
|
|
5280
|
+
async function refineGoal(rawGoal, provider, model) {
|
|
5281
|
+
const prompt = buildRefinementPrompt(rawGoal);
|
|
5282
|
+
try {
|
|
5283
|
+
const signal = AbortSignal.timeout(3e4);
|
|
5284
|
+
const response = await provider.complete({
|
|
5285
|
+
model,
|
|
5286
|
+
system: [{ type: "text", text: prompt }],
|
|
5287
|
+
messages: [{ role: "user", content: "Produce the refined goal." }],
|
|
5288
|
+
maxTokens: 1e3
|
|
5289
|
+
}, { signal });
|
|
5290
|
+
const text = extractText(response);
|
|
5291
|
+
if (!text) return null;
|
|
5292
|
+
return parseRefinement(text, rawGoal);
|
|
5293
|
+
} catch {
|
|
5294
|
+
return null;
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
function buildRefinementPrompt(rawGoal) {
|
|
5298
|
+
return [
|
|
5299
|
+
"You are a goal refinement assistant. Your job is to take a user's raw",
|
|
5300
|
+
"goal description and turn it into a clear, unambiguous, actionable mission",
|
|
5301
|
+
"with concrete, verifiable deliverables.",
|
|
5302
|
+
"",
|
|
5303
|
+
"Rules:",
|
|
5304
|
+
"- The refined goal must be self-contained \u2014 someone reading only the",
|
|
5305
|
+
" refined goal should understand exactly what to do without seeing the",
|
|
5306
|
+
" original.",
|
|
5307
|
+
"- Each deliverable must be a single, checkable item. Prefer concrete",
|
|
5308
|
+
' artifacts: "file X exists at path Y", "test Z passes", "function A',
|
|
5309
|
+
' is refactored into module B". Avoid vague items like "improve code".',
|
|
5310
|
+
"- Include acceptance criteria where helpful.",
|
|
5311
|
+
"- If the goal is already clear and concrete, refine it minimally \u2014 do",
|
|
5312
|
+
" not add fluff.",
|
|
5313
|
+
"",
|
|
5314
|
+
"Output format (exact \u2014 use these markers):",
|
|
5315
|
+
"",
|
|
5316
|
+
"REFINED_GOAL:",
|
|
5317
|
+
"<the refined goal text, 1-3 sentences>",
|
|
5318
|
+
"",
|
|
5319
|
+
"DELIVERABLES:",
|
|
5320
|
+
"- <deliverable 1>",
|
|
5321
|
+
"- <deliverable 2>",
|
|
5322
|
+
"- ...",
|
|
5323
|
+
"",
|
|
5324
|
+
"---",
|
|
5325
|
+
"",
|
|
5326
|
+
`RAW GOAL: ${rawGoal}`,
|
|
5327
|
+
"",
|
|
5328
|
+
"---",
|
|
5329
|
+
"",
|
|
5330
|
+
"Now produce the refined version:"
|
|
5331
|
+
].join("\n");
|
|
5332
|
+
}
|
|
5333
|
+
function extractText(result) {
|
|
5334
|
+
if (!result || typeof result !== "object") return null;
|
|
5335
|
+
const r = result;
|
|
5336
|
+
if (Array.isArray(r.content)) {
|
|
5337
|
+
const texts = r.content.filter((b) => b.type === "text").map((b) => b.text ?? "");
|
|
5338
|
+
return texts.join("") || null;
|
|
5339
|
+
}
|
|
5340
|
+
if (Array.isArray(r.choices)) {
|
|
5341
|
+
const choice = r.choices[0];
|
|
5342
|
+
return choice?.message?.content ?? null;
|
|
5343
|
+
}
|
|
5344
|
+
if (typeof r.text === "string") return r.text;
|
|
5345
|
+
return null;
|
|
5346
|
+
}
|
|
5347
|
+
function parseRefinement(text, fallbackGoal) {
|
|
5348
|
+
const refinedMatch = text.match(/REFINED_GOAL:\s*\n?([\s\S]*?)(?=\nDELIVERABLES:|$)/i);
|
|
5349
|
+
const refinedGoal = refinedMatch?.[1]?.trim() || fallbackGoal;
|
|
5350
|
+
const deliverablesMatch = text.match(/DELIVERABLES:\s*\n([\s\S]*?)$/i);
|
|
5351
|
+
const deliverablesRaw = deliverablesMatch?.[1] ?? "";
|
|
5352
|
+
const deliverables = deliverablesRaw.split("\n").map((line) => line.replace(/^[\s-]*[-*]\s*/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("REFINED_GOAL"));
|
|
5353
|
+
return {
|
|
5354
|
+
refinedGoal,
|
|
5355
|
+
deliverables: deliverables.length > 0 ? deliverables : []
|
|
5356
|
+
};
|
|
5357
|
+
}
|
|
5358
|
+
function refineGoalHeuristic(rawGoal) {
|
|
5359
|
+
const trimmed = rawGoal.trim();
|
|
5360
|
+
return {
|
|
5361
|
+
refinedGoal: trimmed,
|
|
5362
|
+
deliverables: extractHeuristicDeliverables(trimmed)
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
function extractHeuristicDeliverables(goal) {
|
|
5366
|
+
const deliverables = [];
|
|
5367
|
+
const lines = goal.split(/[.;]\s*/);
|
|
5368
|
+
for (const line of lines) {
|
|
5369
|
+
const cleaned = line.trim();
|
|
5370
|
+
if (!cleaned) continue;
|
|
5371
|
+
if (/\b(add|build|create|fix|implement|refactor|write|remove|update|migrate|set up|configure|deploy|test|document)\b/i.test(
|
|
5372
|
+
cleaned
|
|
5373
|
+
)) {
|
|
5374
|
+
deliverables.push(cleaned);
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
if (deliverables.length === 0) {
|
|
5378
|
+
deliverables.push(goal.trim());
|
|
5379
|
+
}
|
|
5380
|
+
return deliverables;
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
// src/slash-commands/goal.ts
|
|
5134
5384
|
var KNOWN_VERBS = /* @__PURE__ */ new Set([
|
|
5135
5385
|
"",
|
|
5136
5386
|
"show",
|
|
@@ -5142,25 +5392,31 @@ var KNOWN_VERBS = /* @__PURE__ */ new Set([
|
|
|
5142
5392
|
"journal",
|
|
5143
5393
|
"log",
|
|
5144
5394
|
"pause",
|
|
5145
|
-
"resume"
|
|
5395
|
+
"resume",
|
|
5396
|
+
"refine"
|
|
5146
5397
|
]);
|
|
5147
5398
|
function buildGoalCommand(opts) {
|
|
5148
5399
|
return {
|
|
5149
5400
|
name: "goal",
|
|
5150
5401
|
category: "Agent",
|
|
5151
|
-
description: "Set, inspect, or clear the long-running autonomous mission
|
|
5402
|
+
description: "Set, inspect, or clear the long-running autonomous mission. Auto-refines goals for clarity.",
|
|
5152
5403
|
help: [
|
|
5153
5404
|
"Usage:",
|
|
5154
|
-
" /goal Show current goal + recent journal",
|
|
5155
|
-
" /goal set <text> Set a new goal (
|
|
5405
|
+
" /goal Show current goal + progress + recent journal",
|
|
5406
|
+
" /goal set <text> Set a new goal (auto-refined for clarity)",
|
|
5407
|
+
" /goal refine Re-refine the current goal",
|
|
5156
5408
|
" /goal clear Clear the goal (stops eternal mode if running)",
|
|
5157
|
-
" /goal pause Pause at end of current iteration
|
|
5158
|
-
" /goal resume Resume a paused goal
|
|
5159
|
-
" /goal status Same as /goal (alias)",
|
|
5409
|
+
" /goal pause Pause at end of current iteration",
|
|
5410
|
+
" /goal resume Resume a paused goal",
|
|
5160
5411
|
" /goal journal [N] Show last N journal entries (default 25)",
|
|
5161
5412
|
"",
|
|
5413
|
+
"When a goal is set, WrongStack auto-refines it using the LLM to:",
|
|
5414
|
+
" \u2022 Make it unambiguous and concrete",
|
|
5415
|
+
" \u2022 Extract verifiable deliverables with acceptance criteria",
|
|
5416
|
+
" \u2022 Estimate completion progress (shown as a progress bar)",
|
|
5417
|
+
"",
|
|
5162
5418
|
"Stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
|
|
5163
|
-
"
|
|
5419
|
+
"The engine updates progress after each iteration toward the deliverable list.",
|
|
5164
5420
|
"",
|
|
5165
5421
|
"Goals live in ~/.wrongstack/projects/<hash>/goal.json and persist across sessions.",
|
|
5166
5422
|
"A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
|
|
@@ -5196,24 +5452,93 @@ function buildGoalCommand(opts) {
|
|
|
5196
5452
|
opts.renderer.writeWarning(msg2);
|
|
5197
5453
|
return { message: msg2 };
|
|
5198
5454
|
}
|
|
5455
|
+
let refined = null;
|
|
5456
|
+
if (opts.llmProvider && opts.llmModel) {
|
|
5457
|
+
opts.renderer.write(color.dim("Refining goal with LLM\u2026"));
|
|
5458
|
+
refined = await refineGoal(setText, opts.llmProvider, opts.llmModel);
|
|
5459
|
+
}
|
|
5460
|
+
if (!refined) {
|
|
5461
|
+
refined = refineGoalHeuristic(setText);
|
|
5462
|
+
}
|
|
5199
5463
|
const existing = await loadGoal(goalPath);
|
|
5200
|
-
const
|
|
5464
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5465
|
+
const next = existing ? {
|
|
5466
|
+
...existing,
|
|
5467
|
+
goal: setText,
|
|
5468
|
+
refinedGoal: refined.refinedGoal,
|
|
5469
|
+
deliverables: refined.deliverables,
|
|
5470
|
+
setAt: now,
|
|
5471
|
+
lastActivityAt: now,
|
|
5472
|
+
progress: void 0,
|
|
5473
|
+
// reset progress
|
|
5474
|
+
progressNote: void 0
|
|
5475
|
+
} : {
|
|
5476
|
+
...emptyGoal(setText),
|
|
5477
|
+
refinedGoal: refined.refinedGoal,
|
|
5478
|
+
deliverables: refined.deliverables
|
|
5479
|
+
};
|
|
5201
5480
|
await saveGoal(goalPath, next);
|
|
5202
|
-
const
|
|
5203
|
-
|
|
5204
|
-
${color.
|
|
5481
|
+
const lines = [];
|
|
5482
|
+
lines.push(
|
|
5483
|
+
`\u{1F3AF} ${color.green("Goal locked:")} ${color.bold(refined.refinedGoal)}`
|
|
5484
|
+
);
|
|
5485
|
+
if (refined.refinedGoal !== setText) {
|
|
5486
|
+
lines.push(color.dim(` (original: "${setText.length > 60 ? setText.slice(0, 60) + "\u2026" : setText}")`));
|
|
5487
|
+
}
|
|
5488
|
+
if (refined.deliverables.length > 0) {
|
|
5489
|
+
lines.push("");
|
|
5490
|
+
lines.push(`${color.bold("Deliverables")} (${refined.deliverables.length}):`);
|
|
5491
|
+
for (const d of refined.deliverables) {
|
|
5492
|
+
lines.push(` ${color.dim("\u25CB")} ${d}`);
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
lines.push("");
|
|
5496
|
+
lines.push(
|
|
5497
|
+
color.dim(`Stored in ${goalPath} \u2014 progress tracked automatically.`)
|
|
5498
|
+
);
|
|
5499
|
+
const msg = lines.join("\n");
|
|
5500
|
+
opts.renderer.write(msg);
|
|
5501
|
+
return {
|
|
5502
|
+
message: msg,
|
|
5503
|
+
runText: buildGoalPreamble(refined.refinedGoal, refined.deliverables)
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
5506
|
+
case "refine": {
|
|
5507
|
+
const current = await loadGoal(goalPath);
|
|
5508
|
+
if (!current) {
|
|
5509
|
+
const msg2 = "No goal set to refine. Use /goal set <text> first.";
|
|
5510
|
+
opts.renderer.writeWarning(msg2);
|
|
5511
|
+
return { message: msg2 };
|
|
5512
|
+
}
|
|
5513
|
+
let refined = null;
|
|
5514
|
+
if (opts.llmProvider && opts.llmModel) {
|
|
5515
|
+
opts.renderer.write(color.dim("Re-refining goal with LLM\u2026"));
|
|
5516
|
+
refined = await refineGoal(current.goal, opts.llmProvider, opts.llmModel);
|
|
5517
|
+
}
|
|
5518
|
+
if (!refined) {
|
|
5519
|
+
refined = refineGoalHeuristic(current.goal);
|
|
5520
|
+
}
|
|
5521
|
+
const updated = {
|
|
5522
|
+
...current,
|
|
5523
|
+
refinedGoal: refined.refinedGoal,
|
|
5524
|
+
deliverables: refined.deliverables
|
|
5525
|
+
};
|
|
5526
|
+
await saveGoal(goalPath, updated);
|
|
5527
|
+
const msg = `${color.green("\u2713")} Goal re-refined with ${refined.deliverables.length} deliverables.`;
|
|
5205
5528
|
opts.renderer.write(msg);
|
|
5206
|
-
return { message: msg
|
|
5529
|
+
return { message: `${msg}
|
|
5530
|
+
|
|
5531
|
+
${formatGoal(updated)}` };
|
|
5207
5532
|
}
|
|
5208
5533
|
case "clear":
|
|
5209
5534
|
case "reset": {
|
|
5210
|
-
const
|
|
5211
|
-
if (!
|
|
5535
|
+
const current = await loadGoal(goalPath);
|
|
5536
|
+
if (!current) {
|
|
5212
5537
|
const msg2 = "No goal to clear.";
|
|
5213
5538
|
opts.renderer.write(msg2);
|
|
5214
5539
|
return { message: msg2 };
|
|
5215
5540
|
}
|
|
5216
|
-
const abandoned = { ...
|
|
5541
|
+
const abandoned = { ...current, goalState: "abandoned" };
|
|
5217
5542
|
await saveGoal(goalPath, abandoned);
|
|
5218
5543
|
const { unlink: unlink4 } = await import('fs/promises');
|
|
5219
5544
|
try {
|
|
@@ -5288,7 +5613,7 @@ ${lines.join("\n")}`;
|
|
|
5288
5613
|
return { message: msg };
|
|
5289
5614
|
}
|
|
5290
5615
|
default: {
|
|
5291
|
-
const msg = `Unknown subcommand "${verb}". Try: show | set <text> | clear | journal [N]`;
|
|
5616
|
+
const msg = `Unknown subcommand "${verb}". Try: show | set <text> | refine | clear | journal [N]`;
|
|
5292
5617
|
opts.renderer.writeWarning(msg);
|
|
5293
5618
|
return { message: msg };
|
|
5294
5619
|
}
|
|
@@ -6751,12 +7076,14 @@ async function patchGlobalConfig2(globalConfigPath, mutate) {
|
|
|
6751
7076
|
function buildSetModelCommand(opts) {
|
|
6752
7077
|
const help = [
|
|
6753
7078
|
"Usage:",
|
|
6754
|
-
" /setmodel Show leader model +
|
|
7079
|
+
" /setmodel Show leader model + matrix + resolution summary",
|
|
6755
7080
|
" /setmodel list List keyed providers, their models, and valid keys",
|
|
6756
|
-
" /setmodel leader <provider> <model> Set the main (leader) model",
|
|
7081
|
+
" /setmodel leader <provider> <model> Set the main (leader / brain) model",
|
|
6757
7082
|
" /setmodel set <key> <provider>/<model> Pin a role/phase/* to a model",
|
|
6758
7083
|
" /setmodel set <key> <model> Pin to a model on the leader provider",
|
|
6759
7084
|
" /setmodel clear <key> Remove a matrix entry",
|
|
7085
|
+
" /setmodel resolve <role> Walk the resolution chain for one role",
|
|
7086
|
+
" /setmodel doctor Validate matrix entries (orphans, typos, missing keys)",
|
|
6760
7087
|
"",
|
|
6761
7088
|
"Keys: a catalog role (e.g. security-scanner), a phase (" + MATRIX_PHASE_KEYS.join(", ") + "),",
|
|
6762
7089
|
"or * for the fleet-wide default. Precedence at spawn: role \u2192 phase \u2192 * \u2192 leader.",
|
|
@@ -6770,24 +7097,57 @@ function buildSetModelCommand(opts) {
|
|
|
6770
7097
|
const lines = [
|
|
6771
7098
|
`${color.bold("WrongStack")} ${color.dim("\u2014 Models")}`,
|
|
6772
7099
|
"",
|
|
6773
|
-
` leader
|
|
6774
|
-
""
|
|
6775
|
-
` ${color.bold("task \u2192 model matrix")} ${color.dim("(role \u2192 phase \u2192 * \u2192 leader)")}`
|
|
7100
|
+
` ${color.bold("leader")} ${color.cyan(`${config.provider}/${config.model}`)} ${color.dim("/setmodel leader <provider> <model>")}`,
|
|
7101
|
+
""
|
|
6776
7102
|
];
|
|
6777
7103
|
if (keys.length === 0) {
|
|
6778
7104
|
lines.push(
|
|
6779
|
-
`
|
|
7105
|
+
` ${color.bold("matrix")} ${color.dim("(empty)")}`,
|
|
7106
|
+
` ${color.dim("pin a role: /setmodel set <role> <provider>/<model>")}`,
|
|
7107
|
+
` ${color.dim("set default: /setmodel set * <provider>/<model>")}`
|
|
6780
7108
|
);
|
|
6781
7109
|
} else {
|
|
7110
|
+
lines.push(` ${color.bold("matrix")} ${color.dim("(role \u2192 phase \u2192 * \u2192 leader)")}`);
|
|
6782
7111
|
for (const k of keys.sort()) {
|
|
6783
7112
|
const kind = matrixKeyKind(k);
|
|
6784
7113
|
const tag = kind === "unknown" ? color.red("?") : color.dim(kind);
|
|
6785
7114
|
lines.push(` ${color.amber(k.padEnd(22))} \u2192 ${fmtEntry(expectDefined(matrix[k]))} ${tag}`);
|
|
6786
7115
|
}
|
|
6787
7116
|
}
|
|
6788
|
-
|
|
7117
|
+
const summaryRoles = getSummaryRoles();
|
|
7118
|
+
if (summaryRoles.length > 0) {
|
|
7119
|
+
lines.push("");
|
|
7120
|
+
lines.push(` ${color.bold("resolution")} ${color.dim("(selected roles)")}`);
|
|
7121
|
+
for (const role of summaryRoles) {
|
|
7122
|
+
const entry = resolveModelMatrix(matrix, role);
|
|
7123
|
+
const provider = entry?.provider ?? config.provider;
|
|
7124
|
+
const model = entry?.model ?? config.model;
|
|
7125
|
+
const source = resolutionSource(matrix, role);
|
|
7126
|
+
lines.push(` ${color.dim(role.padEnd(22))} \u2192 ${color.cyan(`${provider}/${model}`)} ${color.dim(source)}`);
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
lines.push("", color.dim(" /setmodel list \xB7 resolve <role> \xB7 doctor \xB7 help"));
|
|
6789
7130
|
return lines.join("\n");
|
|
6790
7131
|
}
|
|
7132
|
+
function getSummaryRoles() {
|
|
7133
|
+
const picks = [];
|
|
7134
|
+
for (const phase of MATRIX_PHASE_KEYS) {
|
|
7135
|
+
const agents = AGENTS_BY_PHASE[phase];
|
|
7136
|
+
if (agents && agents.length > 0) {
|
|
7137
|
+
picks.push(agents[0].config.role);
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
picks.push("security-scanner", "bug-hunter");
|
|
7141
|
+
return [...new Set(picks)].sort();
|
|
7142
|
+
}
|
|
7143
|
+
function resolutionSource(matrix, role) {
|
|
7144
|
+
if (!matrix) return "leader";
|
|
7145
|
+
if (matrix[role]) return "role";
|
|
7146
|
+
const phase = phaseForRole(role);
|
|
7147
|
+
if (phase && matrix[phase]) return `phase (${phase})`;
|
|
7148
|
+
if (matrix["*"]) return "default (*)";
|
|
7149
|
+
return "leader";
|
|
7150
|
+
}
|
|
6791
7151
|
return {
|
|
6792
7152
|
name: "setmodel",
|
|
6793
7153
|
category: "Config",
|
|
@@ -6804,6 +7164,7 @@ function buildSetModelCommand(opts) {
|
|
|
6804
7164
|
const config = opts.configStore.get();
|
|
6805
7165
|
const keyed = keyedProviderIds(config);
|
|
6806
7166
|
const globalConfigPath = opts.paths.globalConfig;
|
|
7167
|
+
const matrix = config.modelMatrix ?? {};
|
|
6807
7168
|
if (sub === "list") {
|
|
6808
7169
|
const provLines = keyed.map((id) => {
|
|
6809
7170
|
const models = config.providers?.[id]?.models ?? [];
|
|
@@ -6824,6 +7185,119 @@ function buildSetModelCommand(opts) {
|
|
|
6824
7185
|
].join("\n")
|
|
6825
7186
|
};
|
|
6826
7187
|
}
|
|
7188
|
+
if (sub === "resolve") {
|
|
7189
|
+
const role = parts[1];
|
|
7190
|
+
if (!role) {
|
|
7191
|
+
return { message: `${color.amber("Usage:")} /setmodel resolve <role>` };
|
|
7192
|
+
}
|
|
7193
|
+
const kind = matrixKeyKind(role);
|
|
7194
|
+
if (kind === "unknown" && role !== "*") {
|
|
7195
|
+
return {
|
|
7196
|
+
message: `${color.red("Unknown role")}: "${role}". Use ${color.dim("/setmodel list")} to see valid roles.`
|
|
7197
|
+
};
|
|
7198
|
+
}
|
|
7199
|
+
const lines = [
|
|
7200
|
+
`${color.bold("Resolution chain")} for ${color.amber(role)}`,
|
|
7201
|
+
""
|
|
7202
|
+
];
|
|
7203
|
+
const phase = phaseForRole(role);
|
|
7204
|
+
const resolved = resolveModelMatrix(matrix, role);
|
|
7205
|
+
if (matrix[role]) {
|
|
7206
|
+
lines.push(` 1. matrix["${role}"] \u2192 ${fmtEntry(expectDefined(matrix[role]))} ${color.green("\u2713 exact role")}`);
|
|
7207
|
+
} else {
|
|
7208
|
+
lines.push(` 1. matrix["${role}"] \u2192 ${color.dim("not set")}`);
|
|
7209
|
+
}
|
|
7210
|
+
if (phase) {
|
|
7211
|
+
if (matrix[phase]) {
|
|
7212
|
+
lines.push(` 2. matrix["${phase}"] \u2192 ${fmtEntry(expectDefined(matrix[phase]))} ${matrix[role] ? color.dim("(skipped \u2014 role matched)") : color.green("\u2713 phase match")}`);
|
|
7213
|
+
} else {
|
|
7214
|
+
lines.push(` 2. matrix["${phase}"] \u2192 ${color.dim("not set")}`);
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
7217
|
+
if (matrix["*"]) {
|
|
7218
|
+
const skipped = matrix[role] || phase && matrix[phase];
|
|
7219
|
+
lines.push(` 3. matrix["*"] \u2192 ${fmtEntry(expectDefined(matrix["*"]))} ${skipped ? color.dim("(skipped)") : color.green("\u2713 default")}`);
|
|
7220
|
+
} else {
|
|
7221
|
+
lines.push(` 3. matrix["*"] \u2192 ${color.dim("not set")}`);
|
|
7222
|
+
}
|
|
7223
|
+
const leaderSkipped = matrix[role] || phase && matrix[phase] || matrix["*"];
|
|
7224
|
+
lines.push(` 4. ${color.dim("leader fallback")} \u2192 ${color.cyan(`${config.provider}/${config.model}`)} ${leaderSkipped ? color.dim("(skipped)") : color.green("\u2713 used")}`);
|
|
7225
|
+
lines.push("");
|
|
7226
|
+
if (resolved) {
|
|
7227
|
+
const rp = resolved.provider ?? config.provider;
|
|
7228
|
+
lines.push(`${color.green("\u2713 Resolved")}: ${color.cyan(`${rp}/${resolved.model}`)}`);
|
|
7229
|
+
} else {
|
|
7230
|
+
lines.push(`${color.green("\u2713 Resolved")}: ${color.cyan(`${config.provider}/${config.model}`)} ${color.dim("(leader)")}`);
|
|
7231
|
+
}
|
|
7232
|
+
return { message: lines.join("\n") };
|
|
7233
|
+
}
|
|
7234
|
+
if (sub === "doctor") {
|
|
7235
|
+
const issues = [];
|
|
7236
|
+
const warnings = [];
|
|
7237
|
+
for (const [key, entry] of Object.entries(matrix)) {
|
|
7238
|
+
const kind = matrixKeyKind(key);
|
|
7239
|
+
if (kind === "unknown") {
|
|
7240
|
+
issues.push(
|
|
7241
|
+
`${color.red("\u2717")} ${color.amber(key)}: not a valid role, phase, or * \u2014 ${color.dim("typo or stale entry?")}`
|
|
7242
|
+
);
|
|
7243
|
+
}
|
|
7244
|
+
if (entry.provider) {
|
|
7245
|
+
const provCfg2 = config.providers?.[entry.provider];
|
|
7246
|
+
if (!provCfg2) {
|
|
7247
|
+
issues.push(
|
|
7248
|
+
`${color.red("\u2717")} ${color.amber(key)}: provider "${entry.provider}" is not configured`
|
|
7249
|
+
);
|
|
7250
|
+
} else if (!providerHasKey(provCfg2)) {
|
|
7251
|
+
warnings.push(
|
|
7252
|
+
`${color.amber("\u26A0")} ${color.amber(key)}: provider "${entry.provider}" has no API key`
|
|
7253
|
+
);
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
const effectiveProvider = entry.provider ?? config.provider;
|
|
7257
|
+
const provCfg = config.providers?.[effectiveProvider];
|
|
7258
|
+
if (provCfg?.models && provCfg.models.length > 0) {
|
|
7259
|
+
if (!provCfg.models.includes(entry.model)) {
|
|
7260
|
+
warnings.push(
|
|
7261
|
+
`${color.amber("\u26A0")} ${color.amber(key)}: model "${entry.model}" not in ${effectiveProvider}'s model list (${provCfg.models.join(", ")})`
|
|
7262
|
+
);
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7266
|
+
if (Object.keys(matrix).length > 0 && !matrix["*"]) {
|
|
7267
|
+
const covered = /* @__PURE__ */ new Set();
|
|
7268
|
+
for (const [key] of Object.entries(matrix)) {
|
|
7269
|
+
covered.add(key);
|
|
7270
|
+
}
|
|
7271
|
+
const phasesCovered = new Set(Object.keys(matrix).filter((k) => matrixKeyKind(k) === "phase"));
|
|
7272
|
+
const unprotected = [];
|
|
7273
|
+
for (const role of Object.keys(AGENT_CATALOG)) {
|
|
7274
|
+
if (covered.has(role)) continue;
|
|
7275
|
+
const ph = phaseForRole(role);
|
|
7276
|
+
if (ph && phasesCovered.has(ph)) continue;
|
|
7277
|
+
unprotected.push(role);
|
|
7278
|
+
}
|
|
7279
|
+
if (unprotected.length > 0) {
|
|
7280
|
+
const sample = unprotected.slice(0, 10);
|
|
7281
|
+
const suffix = unprotected.length > 10 ? ` +${unprotected.length - 10} more` : "";
|
|
7282
|
+
warnings.push(
|
|
7283
|
+
`${color.amber("\u26A0")} ${unprotected.length} role(s) have no matrix coverage and no * default: ${sample.join(", ")}${suffix}`
|
|
7284
|
+
);
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
const header = [
|
|
7288
|
+
`${color.bold("Matrix Doctor")} ${color.dim("\u2014 " + Object.keys(matrix).length + " entries")}`,
|
|
7289
|
+
""
|
|
7290
|
+
];
|
|
7291
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
7292
|
+
header.push(`${color.green("\u2713")} All matrix entries are valid. No issues found.`);
|
|
7293
|
+
}
|
|
7294
|
+
const allLines = [
|
|
7295
|
+
...header,
|
|
7296
|
+
...issues.length ? ["", `${color.bold("Issues")}:`, ...issues] : [],
|
|
7297
|
+
...warnings.length ? ["", `${color.bold("Warnings")}:`, ...warnings] : []
|
|
7298
|
+
];
|
|
7299
|
+
return { message: allLines.join("\n") };
|
|
7300
|
+
}
|
|
6827
7301
|
try {
|
|
6828
7302
|
if (sub === "leader") {
|
|
6829
7303
|
const provider = parts[1];
|
|
@@ -6868,9 +7342,9 @@ function buildSetModelCommand(opts) {
|
|
|
6868
7342
|
};
|
|
6869
7343
|
}
|
|
6870
7344
|
const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
|
|
6871
|
-
const
|
|
6872
|
-
|
|
6873
|
-
cfg.modelMatrix =
|
|
7345
|
+
const matrix2 = { ...cfg.modelMatrix ?? {} };
|
|
7346
|
+
matrix2[key] = parsed.provider ? { provider: parsed.provider, model: parsed.model } : { model: parsed.model };
|
|
7347
|
+
cfg.modelMatrix = matrix2;
|
|
6874
7348
|
});
|
|
6875
7349
|
opts.configStore.update({
|
|
6876
7350
|
modelMatrix: decrypted.modelMatrix
|
|
@@ -6885,9 +7359,9 @@ function buildSetModelCommand(opts) {
|
|
|
6885
7359
|
return { message: `${color.amber("No matrix entry")} for "${key}".` };
|
|
6886
7360
|
}
|
|
6887
7361
|
const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
|
|
6888
|
-
const
|
|
6889
|
-
delete
|
|
6890
|
-
cfg.modelMatrix =
|
|
7362
|
+
const matrix2 = { ...cfg.modelMatrix ?? {} };
|
|
7363
|
+
delete matrix2[key];
|
|
7364
|
+
cfg.modelMatrix = matrix2;
|
|
6891
7365
|
});
|
|
6892
7366
|
opts.configStore.update({
|
|
6893
7367
|
modelMatrix: decrypted.modelMatrix
|
|
@@ -7456,6 +7930,198 @@ function buildTodosCommand(opts) {
|
|
|
7456
7930
|
}
|
|
7457
7931
|
};
|
|
7458
7932
|
}
|
|
7933
|
+
function findTask(tasks, query) {
|
|
7934
|
+
const asIndex = Number.parseInt(query, 10);
|
|
7935
|
+
if (!Number.isNaN(asIndex)) {
|
|
7936
|
+
const idx = asIndex - 1;
|
|
7937
|
+
const item = tasks[idx];
|
|
7938
|
+
if (item) return { idx, item };
|
|
7939
|
+
}
|
|
7940
|
+
const byId = tasks.findIndex((t) => t.id === query);
|
|
7941
|
+
if (byId >= 0) {
|
|
7942
|
+
const item = tasks[byId];
|
|
7943
|
+
if (item) return { idx: byId, item };
|
|
7944
|
+
}
|
|
7945
|
+
const q = query.toLowerCase();
|
|
7946
|
+
const byTitle = tasks.findIndex((t) => t.title.toLowerCase().includes(q));
|
|
7947
|
+
if (byTitle >= 0) {
|
|
7948
|
+
const item = tasks[byTitle];
|
|
7949
|
+
if (item) return { idx: byTitle, item };
|
|
7950
|
+
}
|
|
7951
|
+
return null;
|
|
7952
|
+
}
|
|
7953
|
+
function validateType(s) {
|
|
7954
|
+
const valid = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
|
|
7955
|
+
return valid.includes(s) ? s : null;
|
|
7956
|
+
}
|
|
7957
|
+
function validatePriority(s) {
|
|
7958
|
+
const valid = ["critical", "high", "medium", "low"];
|
|
7959
|
+
return valid.includes(s) ? s : null;
|
|
7960
|
+
}
|
|
7961
|
+
function validateStatus(s) {
|
|
7962
|
+
const valid = ["pending", "in_progress", "blocked", "failed", "review", "completed"];
|
|
7963
|
+
return valid.includes(s) ? s : null;
|
|
7964
|
+
}
|
|
7965
|
+
function buildTasksCommand(_opts) {
|
|
7966
|
+
return {
|
|
7967
|
+
name: "tasks",
|
|
7968
|
+
category: "Inspect",
|
|
7969
|
+
description: "Manage structured tasks with dependencies, types, and priorities: /tasks [show|add <title>|start|done|fail|status <id> <status>|promote <id>|clear]",
|
|
7970
|
+
help: [
|
|
7971
|
+
"Usage:",
|
|
7972
|
+
" /tasks Show task progress + list",
|
|
7973
|
+
" /tasks show Same as no args",
|
|
7974
|
+
" /tasks add <title> [type] [prio] Add a task",
|
|
7975
|
+
" /tasks start <id|index> Mark task in-progress",
|
|
7976
|
+
" /tasks done <id|index> Mark task completed",
|
|
7977
|
+
" /tasks fail <id|index> Mark task failed",
|
|
7978
|
+
" /tasks status <id> <status> Set exact status (pending|in_progress|blocked|review|completed|failed)",
|
|
7979
|
+
" /tasks depends <id> <depId...> Set dependencies for a task",
|
|
7980
|
+
" /tasks assign <id> <agent> Assign task to an agent/subagent",
|
|
7981
|
+
" /tasks promote <id> Promote task to todo items",
|
|
7982
|
+
" /tasks clear Remove all tasks",
|
|
7983
|
+
"",
|
|
7984
|
+
"Types: feature, bugfix, refactor, docs, test, chore",
|
|
7985
|
+
"Priorities: critical, high, medium, low"
|
|
7986
|
+
].join("\n"),
|
|
7987
|
+
async run(args, ctx) {
|
|
7988
|
+
const taskPath = ctx.meta?.["task.path"];
|
|
7989
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
7990
|
+
return { message: "Task storage is not configured for this session." };
|
|
7991
|
+
}
|
|
7992
|
+
const sessionId = ctx.session?.id ?? "unknown";
|
|
7993
|
+
const file = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
|
|
7994
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
7995
|
+
const restJoined = rest.join(" ").trim();
|
|
7996
|
+
switch (verb) {
|
|
7997
|
+
case "":
|
|
7998
|
+
case "show":
|
|
7999
|
+
case "list":
|
|
8000
|
+
return { message: formatTaskList(file.tasks) };
|
|
8001
|
+
case "progress":
|
|
8002
|
+
case "statusline":
|
|
8003
|
+
return { message: formatTaskProgress(file.tasks) };
|
|
8004
|
+
case "add": {
|
|
8005
|
+
if (!restJoined) return { message: "Usage: /tasks add <title> [type] [priority]" };
|
|
8006
|
+
const parts = restJoined.split(/\s+/);
|
|
8007
|
+
const title = parts[0] ?? "";
|
|
8008
|
+
const type = validateType(parts[1] ?? "") ?? "feature";
|
|
8009
|
+
const priority = validatePriority(parts[2] ?? "") ?? "medium";
|
|
8010
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8011
|
+
const task = {
|
|
8012
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
8013
|
+
title,
|
|
8014
|
+
type,
|
|
8015
|
+
priority,
|
|
8016
|
+
status: "pending",
|
|
8017
|
+
createdAt: now,
|
|
8018
|
+
updatedAt: now
|
|
8019
|
+
};
|
|
8020
|
+
file.tasks.push(task);
|
|
8021
|
+
await saveTasks(taskPath, file);
|
|
8022
|
+
return { message: `Added: ${task.title}
|
|
8023
|
+
|
|
8024
|
+
${formatTaskProgress(file.tasks)}` };
|
|
8025
|
+
}
|
|
8026
|
+
case "start":
|
|
8027
|
+
case "done":
|
|
8028
|
+
case "fail": {
|
|
8029
|
+
if (!restJoined) return { message: `Usage: /tasks ${verb} <id|index>` };
|
|
8030
|
+
const found = findTask(file.tasks, restJoined);
|
|
8031
|
+
if (!found) return { message: `No task matched "${restJoined}".` };
|
|
8032
|
+
const statusMap = {
|
|
8033
|
+
start: "in_progress",
|
|
8034
|
+
done: "completed",
|
|
8035
|
+
fail: "failed"
|
|
8036
|
+
};
|
|
8037
|
+
found.item.status = statusMap[verb] ?? "pending";
|
|
8038
|
+
found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8039
|
+
await saveTasks(taskPath, file);
|
|
8040
|
+
return { message: `Marked ${verb}: ${found.item.title}
|
|
8041
|
+
|
|
8042
|
+
${formatTaskProgress(file.tasks)}` };
|
|
8043
|
+
}
|
|
8044
|
+
case "status": {
|
|
8045
|
+
if (rest.length < 2) return { message: "Usage: /tasks status <id> <pending|in_progress|blocked|review|completed|failed>" };
|
|
8046
|
+
const targetId = rest[0] ?? "";
|
|
8047
|
+
const newStatus = validateStatus(rest[1] ?? "");
|
|
8048
|
+
if (!newStatus) return { message: `Invalid status "${rest[1]}". Use: pending, in_progress, blocked, review, completed, failed.` };
|
|
8049
|
+
const found = findTask(file.tasks, targetId);
|
|
8050
|
+
if (!found) return { message: `No task matched "${targetId}".` };
|
|
8051
|
+
found.item.status = newStatus;
|
|
8052
|
+
found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8053
|
+
await saveTasks(taskPath, file);
|
|
8054
|
+
return { message: `Status \u2192 ${newStatus}: ${found.item.title}
|
|
8055
|
+
|
|
8056
|
+
${formatTaskProgress(file.tasks)}` };
|
|
8057
|
+
}
|
|
8058
|
+
case "depends":
|
|
8059
|
+
case "deps": {
|
|
8060
|
+
if (rest.length < 2) return { message: "Usage: /tasks depends <id> <depId1> [depId2 ...]" };
|
|
8061
|
+
const targetId = rest[0] ?? "";
|
|
8062
|
+
const depIds = rest.slice(1);
|
|
8063
|
+
const found = findTask(file.tasks, targetId);
|
|
8064
|
+
if (!found) return { message: `No task matched "${targetId}".` };
|
|
8065
|
+
found.item.dependsOn = depIds;
|
|
8066
|
+
found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8067
|
+
await saveTasks(taskPath, file);
|
|
8068
|
+
return { message: `Dependencies set for "${found.item.title}": ${depIds.join(", ")}` };
|
|
8069
|
+
}
|
|
8070
|
+
case "assign": {
|
|
8071
|
+
if (rest.length < 2) return { message: "Usage: /tasks assign <id> <agent>" };
|
|
8072
|
+
const targetId = rest[0] ?? "";
|
|
8073
|
+
const agent = rest.slice(1).join(" ");
|
|
8074
|
+
const found = findTask(file.tasks, targetId);
|
|
8075
|
+
if (!found) return { message: `No task matched "${targetId}".` };
|
|
8076
|
+
found.item.assignee = agent;
|
|
8077
|
+
found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8078
|
+
await saveTasks(taskPath, file);
|
|
8079
|
+
return { message: `Assigned to ${agent}: "${found.item.title}"` };
|
|
8080
|
+
}
|
|
8081
|
+
case "promote": {
|
|
8082
|
+
if (!restJoined) return { message: "Usage: /tasks promote <id|index>" };
|
|
8083
|
+
const found = findTask(file.tasks, restJoined);
|
|
8084
|
+
if (!found) return { message: `No task matched "${restJoined}".` };
|
|
8085
|
+
found.item.status = "in_progress";
|
|
8086
|
+
found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8087
|
+
const todos = [
|
|
8088
|
+
{
|
|
8089
|
+
id: `todo_${Date.now()}_task`,
|
|
8090
|
+
content: found.item.title,
|
|
8091
|
+
status: "in_progress",
|
|
8092
|
+
activeForm: found.item.title
|
|
8093
|
+
}
|
|
8094
|
+
];
|
|
8095
|
+
if (found.item.description) {
|
|
8096
|
+
todos.push({
|
|
8097
|
+
id: `todo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
8098
|
+
content: found.item.description.slice(0, 200),
|
|
8099
|
+
status: "pending"
|
|
8100
|
+
});
|
|
8101
|
+
}
|
|
8102
|
+
ctx.state.replaceTodos(todos);
|
|
8103
|
+
await saveTasks(taskPath, file);
|
|
8104
|
+
return {
|
|
8105
|
+
message: `Promoted to ${todos.length} todo(s): "${found.item.title}"
|
|
8106
|
+
|
|
8107
|
+
${formatTaskProgress(file.tasks)}`
|
|
8108
|
+
};
|
|
8109
|
+
}
|
|
8110
|
+
case "clear": {
|
|
8111
|
+
const n = file.tasks.length;
|
|
8112
|
+
if (n === 0) return { message: "Tasks were already empty." };
|
|
8113
|
+
file.tasks = [];
|
|
8114
|
+
await saveTasks(taskPath, file);
|
|
8115
|
+
return { message: `Cleared ${n} task${n === 1 ? "" : "s"}.` };
|
|
8116
|
+
}
|
|
8117
|
+
default:
|
|
8118
|
+
return {
|
|
8119
|
+
message: `Unknown subcommand "${verb}". Try: show | add <title> | start <id> | done <id> | fail <id> | status <id> <s> | depends <id> <deps> | assign <id> <agent> | promote <id> | clear`
|
|
8120
|
+
};
|
|
8121
|
+
}
|
|
8122
|
+
}
|
|
8123
|
+
};
|
|
8124
|
+
}
|
|
7459
8125
|
function buildToolsCommand(opts) {
|
|
7460
8126
|
return {
|
|
7461
8127
|
name: "tools",
|
|
@@ -7583,6 +8249,7 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
7583
8249
|
buildPluginCommand(opts),
|
|
7584
8250
|
buildPruneCommand(opts),
|
|
7585
8251
|
buildMcpSlashCommand(opts),
|
|
8252
|
+
buildAuthCommand(opts),
|
|
7586
8253
|
buildDiagCommand(opts),
|
|
7587
8254
|
buildStatsCommand(opts),
|
|
7588
8255
|
buildSpawnCommand(opts),
|
|
@@ -7592,6 +8259,7 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
7592
8259
|
buildEnhanceCommand(opts),
|
|
7593
8260
|
buildMemoryCommand(opts),
|
|
7594
8261
|
buildTodosCommand(opts),
|
|
8262
|
+
buildTasksCommand(),
|
|
7595
8263
|
buildSddCommand(opts),
|
|
7596
8264
|
buildSaveCommand(opts),
|
|
7597
8265
|
buildLoadCommand(opts),
|
|
@@ -8769,12 +9437,12 @@ function pickGroupIndex(opts) {
|
|
|
8769
9437
|
try {
|
|
8770
9438
|
let current = 0;
|
|
8771
9439
|
try {
|
|
8772
|
-
const parsed = Number.parseInt(
|
|
9440
|
+
const parsed = Number.parseInt(fs13.readFileSync(opts.cursorFile, "utf8").trim(), 10);
|
|
8773
9441
|
if (Number.isFinite(parsed)) current = wrap(parsed);
|
|
8774
9442
|
} catch {
|
|
8775
9443
|
}
|
|
8776
|
-
|
|
8777
|
-
|
|
9444
|
+
fs13.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
|
|
9445
|
+
fs13.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
|
|
8778
9446
|
return current;
|
|
8779
9447
|
} catch {
|
|
8780
9448
|
}
|
|
@@ -9282,49 +9950,114 @@ async function spawnACPAgent(args, deps) {
|
|
|
9282
9950
|
}
|
|
9283
9951
|
}
|
|
9284
9952
|
|
|
9285
|
-
// src/auth-menu.ts
|
|
9953
|
+
// src/auth-menu/add-provider.ts
|
|
9286
9954
|
init_provider_config_utils();
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9955
|
+
|
|
9956
|
+
// src/auth-menu/helpers.ts
|
|
9957
|
+
init_provider_config_utils();
|
|
9958
|
+
async function loadProviders(deps) {
|
|
9959
|
+
return loadConfigProviders(deps.globalConfigPath, deps.vault, {
|
|
9960
|
+
warn: (msg) => deps.renderer.writeWarning(msg)
|
|
9961
|
+
});
|
|
9962
|
+
}
|
|
9963
|
+
|
|
9964
|
+
// src/auth-menu/shared.ts
|
|
9965
|
+
init_provider_config_utils();
|
|
9966
|
+
function renderProviderLine(renderer, id, cfg, idx) {
|
|
9967
|
+
const keys = normalizeKeys(cfg);
|
|
9968
|
+
const active = activeLabel(cfg, keys);
|
|
9969
|
+
const firstKey = keys[0];
|
|
9970
|
+
let summary;
|
|
9971
|
+
if (keys.length === 0) {
|
|
9972
|
+
summary = color.dim("(no keys)");
|
|
9973
|
+
} else if (keys.length === 1) {
|
|
9974
|
+
summary = maskedKey(firstKey?.apiKey ?? "");
|
|
9975
|
+
} else {
|
|
9976
|
+
const activeKeyObj = active != null ? keys.find((k) => k.label === active) : void 0;
|
|
9977
|
+
summary = `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
|
|
9978
|
+
}
|
|
9979
|
+
const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
|
|
9980
|
+
const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
|
|
9981
|
+
renderer.write(
|
|
9982
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
|
|
9983
|
+
`
|
|
9984
|
+
);
|
|
9985
|
+
}
|
|
9986
|
+
function renderProviderHeader(renderer, providerId, cfg) {
|
|
9987
|
+
const keys = normalizeKeys(cfg);
|
|
9988
|
+
const active = activeLabel(cfg, keys);
|
|
9989
|
+
renderer.write(
|
|
9990
|
+
`
|
|
9991
|
+
${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
|
|
9992
|
+
`
|
|
9993
|
+
);
|
|
9994
|
+
const details = [
|
|
9995
|
+
color.dim(` type: ${cfg.type ?? providerId}`),
|
|
9996
|
+
color.dim(
|
|
9997
|
+
` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`
|
|
9998
|
+
),
|
|
9999
|
+
color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
|
|
10000
|
+
];
|
|
10001
|
+
if (cfg.envVars && cfg.envVars.length > 0) {
|
|
10002
|
+
details.push(color.dim(` envVars: ${cfg.envVars.join(", ")}`));
|
|
10003
|
+
}
|
|
10004
|
+
if (cfg.models && cfg.models.length > 0) {
|
|
10005
|
+
details.push(color.dim(` models: ${cfg.models.join(", ")}`));
|
|
10006
|
+
}
|
|
10007
|
+
renderer.write(details.join("\n") + "\n");
|
|
10008
|
+
if (keys.length === 0) {
|
|
10009
|
+
renderer.write(color.dim(" (no keys saved)\n"));
|
|
10010
|
+
} else {
|
|
10011
|
+
for (let i = 0; i < keys.length; i++) {
|
|
10012
|
+
renderKeyLine(renderer, keys[i], i + 1, active);
|
|
9316
10013
|
}
|
|
9317
|
-
deps.renderer.writeError(`Unknown selection: "${choice}"`);
|
|
9318
10014
|
}
|
|
9319
10015
|
}
|
|
9320
|
-
function
|
|
10016
|
+
function renderKeyLine(renderer, key, idx, active) {
|
|
10017
|
+
const marker = key.label === active ? color.green("\u25CF") : color.dim("\u25CB");
|
|
10018
|
+
renderer.write(
|
|
10019
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color.dim(key.createdAt)}
|
|
10020
|
+
`
|
|
10021
|
+
);
|
|
10022
|
+
}
|
|
10023
|
+
function renderActions(renderer, keysLength) {
|
|
9321
10024
|
renderer.write(`
|
|
9322
|
-
${color.
|
|
9323
|
-
|
|
10025
|
+
${color.dim("Actions:")}
|
|
10026
|
+
`);
|
|
10027
|
+
renderer.write(` ${color.bold("a")} Add another key
|
|
10028
|
+
`);
|
|
10029
|
+
if (keysLength > 0) {
|
|
10030
|
+
renderer.write(` ${color.bold("u")} <n> Update key <n>
|
|
10031
|
+
`);
|
|
10032
|
+
renderer.write(` ${color.bold("d")} <n> Delete key <n>
|
|
10033
|
+
`);
|
|
10034
|
+
renderer.write(` ${color.bold("s")} <n> Set key <n> as active
|
|
10035
|
+
`);
|
|
10036
|
+
}
|
|
10037
|
+
renderer.write(` ${color.bold("f")} Edit family
|
|
10038
|
+
`);
|
|
10039
|
+
renderer.write(` ${color.bold("B")} Edit baseUrl
|
|
9324
10040
|
`);
|
|
10041
|
+
renderer.write(` ${color.bold("m")} Edit visible model list
|
|
10042
|
+
`);
|
|
10043
|
+
renderer.write(` ${color.bold("x")} Remove this provider entirely
|
|
10044
|
+
`);
|
|
10045
|
+
renderer.write(` ${color.bold("b")} Back
|
|
10046
|
+
`);
|
|
10047
|
+
renderer.write(` ${color.bold("q")} Quit
|
|
10048
|
+
`);
|
|
10049
|
+
}
|
|
10050
|
+
function renderTopMenu(renderer, providers) {
|
|
10051
|
+
renderer.write(
|
|
10052
|
+
`
|
|
10053
|
+
${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
|
|
10054
|
+
|
|
10055
|
+
`
|
|
10056
|
+
);
|
|
9325
10057
|
const ids = Object.keys(providers).sort();
|
|
9326
10058
|
if (ids.length === 0) {
|
|
9327
10059
|
renderer.write(color.dim(" No providers configured yet.\n"));
|
|
10060
|
+
renderer.write(color.dim(" Use (a) to add one from the models.dev catalog, or (c) for a custom provider.\n"));
|
|
9328
10061
|
} else {
|
|
9329
10062
|
renderer.write(` ${color.dim("Saved providers:")}
|
|
9330
10063
|
`);
|
|
@@ -9332,306 +10065,116 @@ ${color.bold("WrongStack")} ${color.dim("\u2014 API keys")}
|
|
|
9332
10065
|
for (const id of ids) {
|
|
9333
10066
|
const cfg = providers[id];
|
|
9334
10067
|
if (!cfg) continue;
|
|
9335
|
-
|
|
9336
|
-
const active = activeLabel(cfg, keys);
|
|
9337
|
-
const firstKey = keys[0];
|
|
9338
|
-
const summary = keys.length === 0 ? color.dim("(no keys)") : keys.length === 1 ? maskedKey(firstKey?.apiKey ?? "") : `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ${maskedKey(keys.find((k) => k.label === active)?.apiKey ?? firstKey?.apiKey ?? "")}`;
|
|
9339
|
-
const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
|
|
9340
|
-
const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
|
|
9341
|
-
renderer.write(
|
|
9342
|
-
` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
|
|
9343
|
-
`
|
|
9344
|
-
);
|
|
10068
|
+
renderProviderLine(renderer, id, cfg, idx);
|
|
9345
10069
|
idx++;
|
|
9346
10070
|
}
|
|
9347
10071
|
}
|
|
9348
10072
|
renderer.write(`
|
|
9349
10073
|
${color.dim("Actions:")}
|
|
9350
10074
|
`);
|
|
9351
|
-
renderer.write(` ${color.bold("a")} Add
|
|
10075
|
+
renderer.write(` ${color.bold("a")} Add a provider (from catalog)
|
|
9352
10076
|
`);
|
|
9353
|
-
renderer.write(` ${color.bold("c")} Add custom provider
|
|
10077
|
+
renderer.write(` ${color.bold("c")} Add a custom provider
|
|
10078
|
+
`);
|
|
10079
|
+
if (ids.length > 0) {
|
|
10080
|
+
renderer.write(` ${color.dim("1-")}${color.dim(String(ids.length))} ${color.bold("Manage a provider")}
|
|
9354
10081
|
`);
|
|
10082
|
+
}
|
|
9355
10083
|
renderer.write(` ${color.bold("q")} Quit
|
|
9356
10084
|
`);
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
`))
|
|
10085
|
+
}
|
|
10086
|
+
async function readKeyInput(deps, intent) {
|
|
10087
|
+
const key = (await deps.reader.readSecret(
|
|
10088
|
+
` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
|
|
10089
|
+
)).trim();
|
|
10090
|
+
if (!key) {
|
|
10091
|
+
deps.renderer.writeError("No key entered.");
|
|
10092
|
+
return void 0;
|
|
9361
10093
|
}
|
|
10094
|
+
return key;
|
|
9362
10095
|
}
|
|
9363
|
-
async function
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
10096
|
+
async function confirm(deps, question) {
|
|
10097
|
+
const answer = (await deps.reader.readLine(
|
|
10098
|
+
` ${color.amber("?")} ${question} ${color.dim("[y/N/q]")} `
|
|
10099
|
+
)).trim().toLowerCase();
|
|
10100
|
+
if (answer === "q" || answer === "quit") return null;
|
|
10101
|
+
return answer === "y" || answer === "yes";
|
|
10102
|
+
}
|
|
10103
|
+
function suggestLabel(usedLabels) {
|
|
10104
|
+
let candidate = "default";
|
|
10105
|
+
if (!usedLabels.has(candidate)) return candidate;
|
|
10106
|
+
let n = 2;
|
|
10107
|
+
while (usedLabels.has(`key${n}`)) n++;
|
|
10108
|
+
return `key${n}`;
|
|
10109
|
+
}
|
|
10110
|
+
function validateFamily(raw) {
|
|
10111
|
+
const valid = ["anthropic", "openai", "openai-compatible", "google"];
|
|
10112
|
+
return valid.includes(raw) ? raw : null;
|
|
10113
|
+
}
|
|
10114
|
+
|
|
10115
|
+
// src/auth-menu/add-provider.ts
|
|
10116
|
+
async function addFromCatalog(deps) {
|
|
10117
|
+
let catalog = [];
|
|
10118
|
+
try {
|
|
10119
|
+
catalog = (await deps.modelsRegistry.listProviders()).filter(
|
|
10120
|
+
(p) => p.family !== "unsupported"
|
|
10121
|
+
);
|
|
10122
|
+
} catch {
|
|
10123
|
+
deps.renderer.writeWarning(
|
|
10124
|
+
"Catalog unavailable \u2014 falling back to manual entry.\n"
|
|
10125
|
+
);
|
|
10126
|
+
}
|
|
10127
|
+
if (catalog.length === 0) {
|
|
10128
|
+
return addManualEntry(deps);
|
|
10129
|
+
}
|
|
10130
|
+
const saved = new Set(Object.keys(await loadProviders(deps)));
|
|
10131
|
+
deps.renderer.write(
|
|
10132
|
+
color.dim(
|
|
10133
|
+
` Catalog: ${catalog.length} providers. Filter to narrow, "s" for unsaved-only, or enter to show all.
|
|
9376
10134
|
`
|
|
10135
|
+
)
|
|
10136
|
+
);
|
|
10137
|
+
const filterRaw = (await deps.reader.readLine(
|
|
10138
|
+
` ${color.amber("?")} Filter ${color.dim('(substring / "s" / q to quit)')}: `
|
|
10139
|
+
)).trim();
|
|
10140
|
+
if (filterRaw === "q") return false;
|
|
10141
|
+
const filterLc = filterRaw.toLowerCase();
|
|
10142
|
+
const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
|
|
10143
|
+
function matches(p) {
|
|
10144
|
+
if (showUnsavedOnly) return !saved.has(p.id);
|
|
10145
|
+
if (!filterLc) return true;
|
|
10146
|
+
return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
|
|
10147
|
+
}
|
|
10148
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
10149
|
+
let filteredCount = 0;
|
|
10150
|
+
for (const p of catalog) {
|
|
10151
|
+
if (!matches(p)) continue;
|
|
10152
|
+
filteredCount++;
|
|
10153
|
+
const list = byFamily.get(p.family) ?? [];
|
|
10154
|
+
list.push(p);
|
|
10155
|
+
byFamily.set(p.family, list);
|
|
10156
|
+
}
|
|
10157
|
+
if (filteredCount === 0) {
|
|
10158
|
+
deps.renderer.writeError(
|
|
10159
|
+
`No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\`.`
|
|
9377
10160
|
);
|
|
10161
|
+
return false;
|
|
10162
|
+
}
|
|
10163
|
+
if (filterRaw && !showUnsavedOnly) {
|
|
9378
10164
|
deps.renderer.write(
|
|
9379
|
-
color.dim(
|
|
9380
|
-
`
|
|
9381
|
-
` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}
|
|
10165
|
+
color.dim(
|
|
10166
|
+
` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
|
|
9382
10167
|
`
|
|
9383
|
-
)
|
|
9384
|
-
`)
|
|
9385
|
-
);
|
|
9386
|
-
if (cfg.envVars && cfg.envVars.length > 0) {
|
|
9387
|
-
deps.renderer.write(color.dim(` envVars: ${cfg.envVars.join(", ")}
|
|
9388
|
-
`));
|
|
9389
|
-
}
|
|
9390
|
-
if (cfg.models && cfg.models.length > 0) {
|
|
9391
|
-
deps.renderer.write(color.dim(` models: ${cfg.models.join(", ")}
|
|
9392
|
-
`));
|
|
9393
|
-
}
|
|
9394
|
-
if (keys.length === 0) {
|
|
9395
|
-
deps.renderer.write(color.dim(" (no keys saved)\n"));
|
|
9396
|
-
} else {
|
|
9397
|
-
for (let i = 0; i < keys.length; i++) {
|
|
9398
|
-
const k = expectDefined(keys[i]);
|
|
9399
|
-
const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
|
|
9400
|
-
deps.renderer.write(
|
|
9401
|
-
` ${color.dim(`${i + 1}.`.padStart(4))} ${marker} ${k.label.padEnd(20)} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}
|
|
9402
|
-
`
|
|
9403
|
-
);
|
|
9404
|
-
}
|
|
9405
|
-
}
|
|
9406
|
-
deps.renderer.write(`
|
|
9407
|
-
${color.dim("Actions:")}
|
|
9408
|
-
`);
|
|
9409
|
-
deps.renderer.write(` ${color.bold("a")} Add another key
|
|
9410
|
-
`);
|
|
9411
|
-
if (keys.length > 0) {
|
|
9412
|
-
deps.renderer.write(` ${color.bold("u")} <n> Update key <n>
|
|
9413
|
-
`);
|
|
9414
|
-
deps.renderer.write(` ${color.bold("d")} <n> Delete key <n>
|
|
9415
|
-
`);
|
|
9416
|
-
deps.renderer.write(` ${color.bold("s")} <n> Set key <n> as active
|
|
9417
|
-
`);
|
|
9418
|
-
}
|
|
9419
|
-
deps.renderer.write(` ${color.bold("f")} Edit family
|
|
9420
|
-
`);
|
|
9421
|
-
deps.renderer.write(` ${color.bold("B")} Edit baseUrl
|
|
9422
|
-
`);
|
|
9423
|
-
deps.renderer.write(` ${color.bold("m")} Edit visible model list
|
|
9424
|
-
`);
|
|
9425
|
-
deps.renderer.write(` ${color.bold("x")} Remove this provider entirely
|
|
9426
|
-
`);
|
|
9427
|
-
deps.renderer.write(` ${color.bold("b")} Back
|
|
9428
|
-
`);
|
|
9429
|
-
deps.renderer.write(` ${color.bold("q")} Quit
|
|
9430
|
-
`);
|
|
9431
|
-
const raw = (await deps.reader.readLine(`
|
|
9432
|
-
${color.amber("?")} ${providerId} > `)).trim();
|
|
9433
|
-
if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") return;
|
|
9434
|
-
const [verb, argRaw] = raw.split(/\s+/, 2);
|
|
9435
|
-
const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
|
|
9436
|
-
if (verb === "a" || verb === "add") {
|
|
9437
|
-
await addKeyForProvider(providerId, deps, cfg);
|
|
9438
|
-
continue;
|
|
9439
|
-
}
|
|
9440
|
-
if (verb === "x" || verb === "remove") {
|
|
9441
|
-
const confirm = (await deps.reader.readLine(
|
|
9442
|
-
` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N/q]")} `
|
|
9443
|
-
)).trim().toLowerCase();
|
|
9444
|
-
if (confirm === "q") continue;
|
|
9445
|
-
if (confirm === "y" || confirm === "yes") {
|
|
9446
|
-
await mutateProviders(deps, (all) => {
|
|
9447
|
-
delete all[providerId];
|
|
9448
|
-
});
|
|
9449
|
-
deps.renderer.write(` ${color.green("\u2713")} Removed ${providerId}.
|
|
9450
|
-
`);
|
|
9451
|
-
return;
|
|
9452
|
-
}
|
|
9453
|
-
continue;
|
|
9454
|
-
}
|
|
9455
|
-
if (verb === "u" || verb === "update") {
|
|
9456
|
-
if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
|
|
9457
|
-
deps.renderer.writeError(`Usage: u <1-${keys.length}>`);
|
|
9458
|
-
continue;
|
|
9459
|
-
}
|
|
9460
|
-
const target = expectDefined(keys[arg - 1]);
|
|
9461
|
-
const newKey = await readKeyInput(deps, `Updated key for ${target.label}`);
|
|
9462
|
-
if (!newKey) continue;
|
|
9463
|
-
await mutateProviders(deps, (all) => {
|
|
9464
|
-
const p = all[providerId];
|
|
9465
|
-
if (!p) return;
|
|
9466
|
-
const list = normalizeKeys(p).map(
|
|
9467
|
-
(k) => k.label === target.label ? { ...k, apiKey: newKey, createdAt: nowIso() } : k
|
|
9468
|
-
);
|
|
9469
|
-
writeKeysBack(p, list);
|
|
9470
|
-
});
|
|
9471
|
-
deps.renderer.write(` ${color.green("\u2713")} Updated ${providerId}/${target.label}.
|
|
9472
|
-
`);
|
|
9473
|
-
continue;
|
|
9474
|
-
}
|
|
9475
|
-
if (verb === "d" || verb === "delete" || verb === "rm") {
|
|
9476
|
-
if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
|
|
9477
|
-
deps.renderer.writeError(`Usage: d <1-${keys.length}>`);
|
|
9478
|
-
continue;
|
|
9479
|
-
}
|
|
9480
|
-
const target = expectDefined(keys[arg - 1]);
|
|
9481
|
-
const confirm = (await deps.reader.readLine(
|
|
9482
|
-
` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N/q]")} `
|
|
9483
|
-
)).trim().toLowerCase();
|
|
9484
|
-
if (confirm === "q") continue;
|
|
9485
|
-
if (confirm !== "y" && confirm !== "yes") continue;
|
|
9486
|
-
await mutateProviders(deps, (all) => {
|
|
9487
|
-
const p = all[providerId];
|
|
9488
|
-
if (!p) return;
|
|
9489
|
-
const list = normalizeKeys(p).filter((k) => k.label !== target.label);
|
|
9490
|
-
writeKeysBack(p, list);
|
|
9491
|
-
if (p.activeKey === target.label) {
|
|
9492
|
-
p.activeKey = list[0]?.label;
|
|
9493
|
-
}
|
|
9494
|
-
});
|
|
9495
|
-
deps.renderer.write(` ${color.green("\u2713")} Deleted ${providerId}/${target.label}.
|
|
9496
|
-
`);
|
|
9497
|
-
continue;
|
|
9498
|
-
}
|
|
9499
|
-
if (verb === "f" || verb === "family") {
|
|
9500
|
-
const current = cfg.family ?? "";
|
|
9501
|
-
const ans = (await deps.reader.readLine(
|
|
9502
|
-
` ${color.amber("?")} Family ${color.dim(`(anthropic | openai | openai-compatible | google, empty = unset, current: ${current || "unset"})`)}: `
|
|
9503
|
-
)).trim();
|
|
9504
|
-
if (ans !== "" && !["anthropic", "openai", "openai-compatible", "google"].includes(ans)) {
|
|
9505
|
-
deps.renderer.writeError(`Invalid family: "${ans}"`);
|
|
9506
|
-
continue;
|
|
9507
|
-
}
|
|
9508
|
-
await mutateProviders(deps, (all) => {
|
|
9509
|
-
const p = all[providerId];
|
|
9510
|
-
if (!p) return;
|
|
9511
|
-
if (ans === "") delete p.family;
|
|
9512
|
-
else p.family = ans;
|
|
9513
|
-
});
|
|
9514
|
-
deps.renderer.write(` ${color.green("\u2713")} family \u2192 ${ans || "(unset)"}
|
|
9515
|
-
`);
|
|
9516
|
-
continue;
|
|
9517
|
-
}
|
|
9518
|
-
if (verb === "B" || verb === "baseurl" || verb === "base-url") {
|
|
9519
|
-
const current = cfg.baseUrl ?? "";
|
|
9520
|
-
const ans = (await deps.reader.readLine(
|
|
9521
|
-
` ${color.amber("?")} Base URL ${color.dim(`(empty = unset, current: ${current || "unset"})`)}: `
|
|
9522
|
-
)).trim();
|
|
9523
|
-
await mutateProviders(deps, (all) => {
|
|
9524
|
-
const p = all[providerId];
|
|
9525
|
-
if (!p) return;
|
|
9526
|
-
if (ans === "") delete p.baseUrl;
|
|
9527
|
-
else p.baseUrl = ans;
|
|
9528
|
-
});
|
|
9529
|
-
deps.renderer.write(` ${color.green("\u2713")} baseUrl \u2192 ${ans || "(unset)"}
|
|
9530
|
-
`);
|
|
9531
|
-
continue;
|
|
9532
|
-
}
|
|
9533
|
-
if (verb === "m" || verb === "models") {
|
|
9534
|
-
const current = (cfg.models ?? []).join(", ");
|
|
9535
|
-
const ans = (await deps.reader.readLine(
|
|
9536
|
-
` ${color.amber("?")} Model ids ${color.dim(`(comma-separated, empty = catalog default, current: ${current || "none"})`)}: `
|
|
9537
|
-
)).trim();
|
|
9538
|
-
const list = ans ? ans.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
9539
|
-
await mutateProviders(deps, (all) => {
|
|
9540
|
-
const p = all[providerId];
|
|
9541
|
-
if (!p) return;
|
|
9542
|
-
if (list.length === 0) delete p.models;
|
|
9543
|
-
else p.models = list;
|
|
9544
|
-
});
|
|
9545
|
-
deps.renderer.write(
|
|
9546
|
-
` ${color.green("\u2713")} models \u2192 ${list.length === 0 ? "(catalog default)" : list.join(", ")}
|
|
9547
|
-
`
|
|
9548
|
-
);
|
|
9549
|
-
continue;
|
|
9550
|
-
}
|
|
9551
|
-
if (verb === "s" || verb === "set" || verb === "active") {
|
|
9552
|
-
if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
|
|
9553
|
-
deps.renderer.writeError(`Usage: s <1-${keys.length}>`);
|
|
9554
|
-
continue;
|
|
9555
|
-
}
|
|
9556
|
-
const target = expectDefined(keys[arg - 1]);
|
|
9557
|
-
await mutateProviders(deps, (all) => {
|
|
9558
|
-
const p = all[providerId];
|
|
9559
|
-
if (!p) return;
|
|
9560
|
-
const list = normalizeKeys(p);
|
|
9561
|
-
writeKeysBack(p, list);
|
|
9562
|
-
p.activeKey = target.label;
|
|
9563
|
-
});
|
|
9564
|
-
deps.renderer.write(
|
|
9565
|
-
` ${color.green("\u2713")} Active key for ${providerId} \u2192 ${color.bold(target.label)}.
|
|
9566
|
-
`
|
|
9567
|
-
);
|
|
9568
|
-
continue;
|
|
9569
|
-
}
|
|
9570
|
-
deps.renderer.writeError(`Unknown action: "${raw}"`);
|
|
9571
|
-
}
|
|
9572
|
-
}
|
|
9573
|
-
async function addForNewProvider(deps) {
|
|
9574
|
-
let catalog = [];
|
|
9575
|
-
try {
|
|
9576
|
-
catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
|
|
9577
|
-
} catch {
|
|
9578
|
-
deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.");
|
|
9579
|
-
}
|
|
9580
|
-
if (catalog.length === 0) {
|
|
9581
|
-
const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
|
|
9582
|
-
if (!pid || pid === "q") return;
|
|
9583
|
-
const fam = (await deps.reader.readLine(
|
|
9584
|
-
` ${color.amber("?")} Family (anthropic/openai/openai-compatible/google): `
|
|
9585
|
-
)).trim();
|
|
9586
|
-
const baseUrl2 = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
|
|
9587
|
-
await addKeyForProvider(pid, deps, {
|
|
9588
|
-
type: pid,
|
|
9589
|
-
family: fam || void 0,
|
|
9590
|
-
...baseUrl2 ? { baseUrl: baseUrl2 } : {}
|
|
9591
|
-
});
|
|
9592
|
-
return;
|
|
9593
|
-
}
|
|
9594
|
-
const saved = new Set(Object.keys(await loadProviders(deps)));
|
|
9595
|
-
deps.renderer.write(
|
|
9596
|
-
color.dim(
|
|
9597
|
-
` Catalog has ${catalog.length} providers. Filter by name to narrow, or "s" for unsaved-only.
|
|
9598
|
-
`
|
|
9599
|
-
)
|
|
9600
|
-
);
|
|
9601
|
-
const filterRaw = (await deps.reader.readLine(
|
|
9602
|
-
` ${color.amber("?")} Filter ${color.dim('(substring, "s" for unsaved-only, q to quit)')}: `
|
|
9603
|
-
)).trim();
|
|
9604
|
-
if (filterRaw === "q") return;
|
|
9605
|
-
const filterLc = filterRaw.toLowerCase();
|
|
9606
|
-
const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
|
|
9607
|
-
const matches = (p) => {
|
|
9608
|
-
if (showUnsavedOnly) return !saved.has(p.id);
|
|
9609
|
-
if (!filterLc) return true;
|
|
9610
|
-
return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
|
|
9611
|
-
};
|
|
9612
|
-
const byFamily = /* @__PURE__ */ new Map();
|
|
9613
|
-
let filteredCount = 0;
|
|
9614
|
-
for (const p of catalog) {
|
|
9615
|
-
if (!matches(p)) continue;
|
|
9616
|
-
filteredCount++;
|
|
9617
|
-
const list = byFamily.get(p.family) ?? [];
|
|
9618
|
-
list.push(p);
|
|
9619
|
-
byFamily.set(p.family, list);
|
|
9620
|
-
}
|
|
9621
|
-
if (filteredCount === 0) {
|
|
9622
|
-
deps.renderer.writeError(
|
|
9623
|
-
`No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\` for valid ids.`
|
|
9624
|
-
);
|
|
9625
|
-
return;
|
|
9626
|
-
}
|
|
9627
|
-
if (filterRaw && !showUnsavedOnly) {
|
|
9628
|
-
deps.renderer.write(
|
|
9629
|
-
color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
|
|
9630
|
-
`)
|
|
10168
|
+
)
|
|
9631
10169
|
);
|
|
9632
10170
|
}
|
|
9633
10171
|
const ordered = [];
|
|
9634
|
-
const familyOrder = [
|
|
10172
|
+
const familyOrder = [
|
|
10173
|
+
"anthropic",
|
|
10174
|
+
"openai",
|
|
10175
|
+
"google",
|
|
10176
|
+
"openai-compatible"
|
|
10177
|
+
];
|
|
9635
10178
|
let idx = 1;
|
|
9636
10179
|
deps.renderer.write("\n");
|
|
9637
10180
|
for (const fam of familyOrder) {
|
|
@@ -9657,7 +10200,7 @@ async function addForNewProvider(deps) {
|
|
|
9657
10200
|
`
|
|
9658
10201
|
${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("[q to quit]")}: `
|
|
9659
10202
|
)).trim();
|
|
9660
|
-
if (!answer || answer === "q") return;
|
|
10203
|
+
if (!answer || answer === "q") return false;
|
|
9661
10204
|
let chosen;
|
|
9662
10205
|
const num = Number.parseInt(answer, 10);
|
|
9663
10206
|
if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
|
|
@@ -9667,29 +10210,35 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
|
|
|
9667
10210
|
}
|
|
9668
10211
|
if (!chosen) {
|
|
9669
10212
|
deps.renderer.writeError(`No such provider: "${answer}"`);
|
|
9670
|
-
return;
|
|
10213
|
+
return false;
|
|
9671
10214
|
}
|
|
10215
|
+
return addKeyForCatalogProvider(deps, chosen);
|
|
10216
|
+
}
|
|
10217
|
+
async function addKeyForCatalogProvider(deps, chosen) {
|
|
9672
10218
|
deps.renderer.write(
|
|
9673
10219
|
color.dim(`
|
|
9674
|
-
Defaults from models.dev \u2014 press Enter to keep, or type
|
|
10220
|
+
Defaults from models.dev \u2014 press Enter to keep, or type overrides.
|
|
9675
10221
|
`)
|
|
9676
10222
|
);
|
|
9677
|
-
const famRaw = (await deps.reader.readLine(
|
|
9678
|
-
|
|
10223
|
+
const famRaw = (await deps.reader.readLine(
|
|
10224
|
+
` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `
|
|
10225
|
+
)).trim();
|
|
10226
|
+
if (famRaw === "q") return false;
|
|
9679
10227
|
let family = chosen.family;
|
|
9680
10228
|
if (famRaw) {
|
|
9681
|
-
|
|
10229
|
+
const validated = validateFamily(famRaw);
|
|
10230
|
+
if (!validated) {
|
|
9682
10231
|
deps.renderer.writeError(
|
|
9683
|
-
`Invalid family: "${famRaw}" (must be anthropic
|
|
10232
|
+
`Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
|
|
9684
10233
|
);
|
|
9685
|
-
return;
|
|
10234
|
+
return false;
|
|
9686
10235
|
}
|
|
9687
|
-
family =
|
|
10236
|
+
family = validated;
|
|
9688
10237
|
}
|
|
9689
10238
|
const baseRaw = (await deps.reader.readLine(
|
|
9690
10239
|
` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
|
|
9691
10240
|
)).trim();
|
|
9692
|
-
if (baseRaw === "q") return;
|
|
10241
|
+
if (baseRaw === "q") return false;
|
|
9693
10242
|
const baseUrl = baseRaw || chosen.apiBase;
|
|
9694
10243
|
const providersNow = await loadProviders(deps);
|
|
9695
10244
|
let suggestedAlias = chosen.id;
|
|
@@ -9703,7 +10252,7 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
|
|
|
9703
10252
|
suggestedAlias = candidate;
|
|
9704
10253
|
}
|
|
9705
10254
|
const aliasRaw = (await deps.reader.readLine(
|
|
9706
|
-
` ${color.amber("?")} Save
|
|
10255
|
+
` ${color.amber("?")} Save as alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used with --provider <alias>)")}: `
|
|
9707
10256
|
)).trim();
|
|
9708
10257
|
const alias = aliasRaw || suggestedAlias;
|
|
9709
10258
|
const existing = providersNow[alias];
|
|
@@ -9717,10 +10266,10 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
|
|
|
9717
10266
|
New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
|
|
9718
10267
|
Pick a different alias to keep them separate.`
|
|
9719
10268
|
);
|
|
9720
|
-
return;
|
|
10269
|
+
return false;
|
|
9721
10270
|
}
|
|
9722
10271
|
}
|
|
9723
|
-
|
|
10272
|
+
return addKeyForProvider(alias, deps, {
|
|
9724
10273
|
type: chosen.id,
|
|
9725
10274
|
family,
|
|
9726
10275
|
baseUrl,
|
|
@@ -9730,39 +10279,41 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
|
|
|
9730
10279
|
async function addCustomProvider(deps) {
|
|
9731
10280
|
deps.renderer.write(
|
|
9732
10281
|
`
|
|
9733
|
-
${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the
|
|
10282
|
+
${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the catalog.")}
|
|
9734
10283
|
`
|
|
9735
10284
|
);
|
|
9736
10285
|
const type = (await deps.reader.readLine(
|
|
9737
10286
|
` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
|
|
9738
10287
|
)).trim();
|
|
9739
|
-
if (!type || type === "q") return;
|
|
10288
|
+
if (!type || type === "q") return false;
|
|
9740
10289
|
const existing = (await loadProviders(deps))[type];
|
|
9741
10290
|
if (existing) {
|
|
9742
|
-
deps.renderer.writeWarning(
|
|
9743
|
-
|
|
10291
|
+
deps.renderer.writeWarning(
|
|
10292
|
+
`"${type}" already exists. Pick it from the main menu to edit.`
|
|
10293
|
+
);
|
|
10294
|
+
return false;
|
|
9744
10295
|
}
|
|
9745
10296
|
const familyRaw = (await deps.reader.readLine(
|
|
9746
10297
|
` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
|
|
9747
10298
|
)).trim();
|
|
9748
|
-
if (familyRaw === "q") return;
|
|
9749
|
-
|
|
10299
|
+
if (familyRaw === "q") return false;
|
|
10300
|
+
const family = validateFamily(familyRaw);
|
|
10301
|
+
if (!family) {
|
|
9750
10302
|
deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
|
|
9751
|
-
return;
|
|
10303
|
+
return false;
|
|
9752
10304
|
}
|
|
9753
|
-
const family = familyRaw;
|
|
9754
10305
|
const baseUrl = (await deps.reader.readLine(
|
|
9755
|
-
` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1,
|
|
10306
|
+
` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, optional)")}: `
|
|
9756
10307
|
)).trim();
|
|
9757
10308
|
const modelsRaw = (await deps.reader.readLine(
|
|
9758
10309
|
` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
|
|
9759
10310
|
)).trim();
|
|
9760
10311
|
const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
9761
10312
|
const envVarsRaw = (await deps.reader.readLine(
|
|
9762
|
-
` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional
|
|
10313
|
+
` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional)")}: `
|
|
9763
10314
|
)).trim();
|
|
9764
10315
|
const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
9765
|
-
|
|
10316
|
+
return addKeyForProvider(type, deps, {
|
|
9766
10317
|
type,
|
|
9767
10318
|
family,
|
|
9768
10319
|
...baseUrl ? { baseUrl } : {},
|
|
@@ -9770,38 +10321,52 @@ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies
|
|
|
9770
10321
|
...envVars ? { envVars } : {}
|
|
9771
10322
|
});
|
|
9772
10323
|
}
|
|
10324
|
+
async function addManualEntry(deps) {
|
|
10325
|
+
const pid = (await deps.reader.readLine(
|
|
10326
|
+
` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `
|
|
10327
|
+
)).trim();
|
|
10328
|
+
if (!pid || pid === "q") return false;
|
|
10329
|
+
const famRaw = (await deps.reader.readLine(
|
|
10330
|
+
` ${color.amber("?")} Family ${color.dim("(anthropic/openai/openai-compatible/google)")}: `
|
|
10331
|
+
)).trim();
|
|
10332
|
+
const family = validateFamily(famRaw);
|
|
10333
|
+
if (!family) {
|
|
10334
|
+
deps.renderer.writeError(`Invalid family: "${famRaw}"`);
|
|
10335
|
+
return false;
|
|
10336
|
+
}
|
|
10337
|
+
const baseUrl = (await deps.reader.readLine(
|
|
10338
|
+
` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `
|
|
10339
|
+
)).trim();
|
|
10340
|
+
return addKeyForProvider(pid, deps, {
|
|
10341
|
+
type: pid,
|
|
10342
|
+
family,
|
|
10343
|
+
...baseUrl ? { baseUrl } : {}
|
|
10344
|
+
});
|
|
10345
|
+
}
|
|
9773
10346
|
async function addKeyForProvider(providerId, deps, template) {
|
|
9774
10347
|
const providers = await loadProviders(deps);
|
|
9775
10348
|
const existing = providers[providerId];
|
|
9776
10349
|
const existingKeys = existing ? normalizeKeys(existing) : [];
|
|
9777
10350
|
const usedLabels = new Set(existingKeys.map((k) => k.label));
|
|
9778
|
-
|
|
9779
|
-
if (
|
|
9780
|
-
let n = 2;
|
|
9781
|
-
while (usedLabels.has(`key${n}`)) n++;
|
|
9782
|
-
defaultLabel = `key${n}`;
|
|
9783
|
-
}
|
|
9784
|
-
const labelRaw = (await deps.reader.readLine(
|
|
9785
|
-
` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
|
|
9786
|
-
)).trim();
|
|
9787
|
-
const label = labelRaw || defaultLabel;
|
|
9788
|
-
if (usedLabels.has(label)) {
|
|
9789
|
-
deps.renderer.writeError(
|
|
9790
|
-
`Label "${label}" already used for ${providerId}. Use update (u) instead.`
|
|
9791
|
-
);
|
|
9792
|
-
return;
|
|
9793
|
-
}
|
|
10351
|
+
const label = await promptForLabel(deps, usedLabels);
|
|
10352
|
+
if (!label) return false;
|
|
9794
10353
|
const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
|
|
9795
|
-
if (!apiKey)
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
10354
|
+
if (!apiKey) return false;
|
|
10355
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10356
|
+
const existingProv = all[providerId] ?? {
|
|
10357
|
+
type: providerId,
|
|
10358
|
+
...template
|
|
10359
|
+
};
|
|
9801
10360
|
if (!existingProv.type) existingProv.type = providerId;
|
|
9802
|
-
if (!existingProv.family && template.family)
|
|
9803
|
-
|
|
9804
|
-
|
|
10361
|
+
if (!existingProv.family && template.family) {
|
|
10362
|
+
existingProv.family = template.family;
|
|
10363
|
+
}
|
|
10364
|
+
if (!existingProv.baseUrl && template.baseUrl) {
|
|
10365
|
+
existingProv.baseUrl = template.baseUrl;
|
|
10366
|
+
}
|
|
10367
|
+
if (!existingProv.envVars && template.envVars) {
|
|
10368
|
+
existingProv.envVars = template.envVars;
|
|
10369
|
+
}
|
|
9805
10370
|
const list = normalizeKeys(existingProv);
|
|
9806
10371
|
list.push({ label, apiKey, createdAt: nowIso() });
|
|
9807
10372
|
writeKeysBack(existingProv, list);
|
|
@@ -9809,10 +10374,242 @@ async function addKeyForProvider(providerId, deps, template) {
|
|
|
9809
10374
|
all[providerId] = existingProv;
|
|
9810
10375
|
});
|
|
9811
10376
|
deps.renderer.write(
|
|
9812
|
-
` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
|
|
10377
|
+
` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
|
|
10378
|
+
`
|
|
10379
|
+
);
|
|
10380
|
+
deps.renderer.write(
|
|
10381
|
+
color.dim(
|
|
10382
|
+
` Launch: wstack --provider ${providerId} "<task>"
|
|
9813
10383
|
`
|
|
10384
|
+
)
|
|
9814
10385
|
);
|
|
10386
|
+
return true;
|
|
10387
|
+
}
|
|
10388
|
+
async function promptForLabel(deps, usedLabels) {
|
|
10389
|
+
const defaultLabel = suggestLabel(usedLabels);
|
|
10390
|
+
const labelRaw = (await deps.reader.readLine(
|
|
10391
|
+
` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
|
|
10392
|
+
)).trim();
|
|
10393
|
+
const label = labelRaw || defaultLabel;
|
|
10394
|
+
if (usedLabels.has(label)) {
|
|
10395
|
+
deps.renderer.writeError(
|
|
10396
|
+
`Label "${label}" is already used. Use update (u) instead.`
|
|
10397
|
+
);
|
|
10398
|
+
return null;
|
|
10399
|
+
}
|
|
10400
|
+
return label;
|
|
10401
|
+
}
|
|
10402
|
+
|
|
10403
|
+
// src/auth-menu/provider-menu.ts
|
|
10404
|
+
init_provider_config_utils();
|
|
10405
|
+
async function manageProvider(providerId, deps) {
|
|
10406
|
+
for (; ; ) {
|
|
10407
|
+
const providers = await loadProviders(deps);
|
|
10408
|
+
const cfg = providers[providerId];
|
|
10409
|
+
if (!cfg) {
|
|
10410
|
+
deps.renderer.writeError(`Provider "${providerId}" no longer in config.`);
|
|
10411
|
+
return;
|
|
10412
|
+
}
|
|
10413
|
+
const keys = normalizeKeys(cfg);
|
|
10414
|
+
renderProviderHeader(deps.renderer, providerId, cfg);
|
|
10415
|
+
renderActions(deps.renderer, keys.length);
|
|
10416
|
+
const raw = (await deps.reader.readLine(
|
|
10417
|
+
`
|
|
10418
|
+
${color.amber("?")} ${providerId} > `
|
|
10419
|
+
)).trim();
|
|
10420
|
+
if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") {
|
|
10421
|
+
return;
|
|
10422
|
+
}
|
|
10423
|
+
const [verb, argRaw] = raw.split(/\s+/, 2);
|
|
10424
|
+
const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
|
|
10425
|
+
const handled = await dispatchAction(verb, arg, providerId, keys, cfg, deps);
|
|
10426
|
+
if (handled === "exit") return;
|
|
10427
|
+
if (handled === "continue") continue;
|
|
10428
|
+
}
|
|
10429
|
+
}
|
|
10430
|
+
async function dispatchAction(verb, arg, providerId, keys, cfg, deps) {
|
|
10431
|
+
if (verb === "a" || verb === "add") {
|
|
10432
|
+
await addKeyForProvider(providerId, deps, cfg);
|
|
10433
|
+
return "continue";
|
|
10434
|
+
}
|
|
10435
|
+
if (verb === "x" || verb === "remove") {
|
|
10436
|
+
const answer = await confirm(deps, `Remove provider "${providerId}" and ${keys.length} key(s)?`);
|
|
10437
|
+
if (answer === null) return "continue";
|
|
10438
|
+
if (answer) {
|
|
10439
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10440
|
+
delete all[providerId];
|
|
10441
|
+
});
|
|
10442
|
+
deps.renderer.write(` ${color.green("\u2713")} Removed ${providerId}.
|
|
10443
|
+
`);
|
|
10444
|
+
return "exit";
|
|
10445
|
+
}
|
|
10446
|
+
return "continue";
|
|
10447
|
+
}
|
|
10448
|
+
if (verb === "u" || verb === "update") {
|
|
10449
|
+
if (!validKeyIndex(arg, keys.length, deps, "u")) return "continue";
|
|
10450
|
+
const target = expectDefined(keys[arg - 1]);
|
|
10451
|
+
const newKey = await readKeyInput(deps, `New key for ${target.label}`);
|
|
10452
|
+
if (!newKey) return "continue";
|
|
10453
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10454
|
+
const p = all[providerId];
|
|
10455
|
+
if (!p) return;
|
|
10456
|
+
const list = normalizeKeys(p).map(
|
|
10457
|
+
(k) => k.label === target.label ? { ...k, apiKey: newKey, createdAt: nowIso() } : k
|
|
10458
|
+
);
|
|
10459
|
+
writeKeysBack(p, list);
|
|
10460
|
+
});
|
|
10461
|
+
deps.renderer.write(` ${color.green("\u2713")} Updated ${providerId}/${target.label}.
|
|
10462
|
+
`);
|
|
10463
|
+
return "continue";
|
|
10464
|
+
}
|
|
10465
|
+
if (verb === "d" || verb === "delete" || verb === "rm") {
|
|
10466
|
+
if (!validKeyIndex(arg, keys.length, deps, "d")) return "continue";
|
|
10467
|
+
const target = expectDefined(keys[arg - 1]);
|
|
10468
|
+
const answer = await confirm(
|
|
10469
|
+
deps,
|
|
10470
|
+
`Delete key "${target.label}" (${maskedKey(target.apiKey)})?`
|
|
10471
|
+
);
|
|
10472
|
+
if (answer === null) return "continue";
|
|
10473
|
+
if (!answer) return "continue";
|
|
10474
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10475
|
+
const p = all[providerId];
|
|
10476
|
+
if (!p) return;
|
|
10477
|
+
const list = normalizeKeys(p).filter((k) => k.label !== target.label);
|
|
10478
|
+
writeKeysBack(p, list);
|
|
10479
|
+
if (p.activeKey === target.label) {
|
|
10480
|
+
p.activeKey = list[0]?.label;
|
|
10481
|
+
}
|
|
10482
|
+
});
|
|
10483
|
+
deps.renderer.write(` ${color.green("\u2713")} Deleted ${providerId}/${target.label}.
|
|
10484
|
+
`);
|
|
10485
|
+
return "continue";
|
|
10486
|
+
}
|
|
10487
|
+
if (verb === "s" || verb === "set" || verb === "active") {
|
|
10488
|
+
if (!validKeyIndex(arg, keys.length, deps, "s")) return "continue";
|
|
10489
|
+
const target = expectDefined(keys[arg - 1]);
|
|
10490
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10491
|
+
const p = all[providerId];
|
|
10492
|
+
if (!p) return;
|
|
10493
|
+
const list = normalizeKeys(p);
|
|
10494
|
+
writeKeysBack(p, list);
|
|
10495
|
+
p.activeKey = target.label;
|
|
10496
|
+
});
|
|
10497
|
+
deps.renderer.write(
|
|
10498
|
+
` ${color.green("\u2713")} Active key \u2192 ${color.bold(target.label)}.
|
|
10499
|
+
`
|
|
10500
|
+
);
|
|
10501
|
+
return "continue";
|
|
10502
|
+
}
|
|
10503
|
+
if (verb === "f" || verb === "family") {
|
|
10504
|
+
const current = cfg.family ?? "";
|
|
10505
|
+
const ans = (await deps.reader.readLine(
|
|
10506
|
+
` ${color.amber("?")} Family ${color.dim(`(anthropic | openai | openai-compatible | google, empty = unset, current: ${current || "unset"})`)}: `
|
|
10507
|
+
)).trim();
|
|
10508
|
+
if (ans !== "") {
|
|
10509
|
+
const validated = validateFamily(ans);
|
|
10510
|
+
if (!validated) {
|
|
10511
|
+
deps.renderer.writeError(`Invalid family: "${ans}". Must be one of: anthropic, openai, openai-compatible, google.`);
|
|
10512
|
+
return "continue";
|
|
10513
|
+
}
|
|
10514
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10515
|
+
const p = all[providerId];
|
|
10516
|
+
if (!p) return;
|
|
10517
|
+
p.family = validated;
|
|
10518
|
+
});
|
|
10519
|
+
deps.renderer.write(` ${color.green("\u2713")} family \u2192 ${validated}
|
|
10520
|
+
`);
|
|
10521
|
+
} else {
|
|
10522
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10523
|
+
const p = all[providerId];
|
|
10524
|
+
if (!p) return;
|
|
10525
|
+
delete p.family;
|
|
10526
|
+
});
|
|
10527
|
+
deps.renderer.write(` ${color.green("\u2713")} family \u2192 (unset)
|
|
10528
|
+
`);
|
|
10529
|
+
}
|
|
10530
|
+
return "continue";
|
|
10531
|
+
}
|
|
10532
|
+
if (verb === "B" || verb === "baseurl" || verb === "base-url") {
|
|
10533
|
+
const current = cfg.baseUrl ?? "";
|
|
10534
|
+
const ans = (await deps.reader.readLine(
|
|
10535
|
+
` ${color.amber("?")} Base URL ${color.dim(`(empty = unset, current: ${current || "unset"})`)}: `
|
|
10536
|
+
)).trim();
|
|
10537
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10538
|
+
const p = all[providerId];
|
|
10539
|
+
if (!p) return;
|
|
10540
|
+
if (ans === "") delete p.baseUrl;
|
|
10541
|
+
else p.baseUrl = ans;
|
|
10542
|
+
});
|
|
10543
|
+
deps.renderer.write(` ${color.green("\u2713")} baseUrl \u2192 ${ans || "(unset)"}
|
|
10544
|
+
`);
|
|
10545
|
+
return "continue";
|
|
10546
|
+
}
|
|
10547
|
+
if (verb === "m" || verb === "models") {
|
|
10548
|
+
const current = (cfg.models ?? []).join(", ");
|
|
10549
|
+
const ans = (await deps.reader.readLine(
|
|
10550
|
+
` ${color.amber("?")} Model ids ${color.dim(`(comma-separated, empty = catalog default, current: ${current || "none"})`)}: `
|
|
10551
|
+
)).trim();
|
|
10552
|
+
const list = ans ? ans.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
10553
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10554
|
+
const p = all[providerId];
|
|
10555
|
+
if (!p) return;
|
|
10556
|
+
if (list.length === 0) delete p.models;
|
|
10557
|
+
else p.models = list;
|
|
10558
|
+
});
|
|
10559
|
+
deps.renderer.write(
|
|
10560
|
+
` ${color.green("\u2713")} models \u2192 ${list.length === 0 ? "(catalog default)" : list.join(", ")}
|
|
10561
|
+
`
|
|
10562
|
+
);
|
|
10563
|
+
return "continue";
|
|
10564
|
+
}
|
|
10565
|
+
deps.renderer.writeError(`Unknown action: "${verb}". Type b for back or q to quit.`);
|
|
10566
|
+
return "unknown";
|
|
10567
|
+
}
|
|
10568
|
+
function validKeyIndex(arg, max, deps, verb) {
|
|
10569
|
+
if (!Number.isFinite(arg) || arg < 1 || arg > max) {
|
|
10570
|
+
deps.renderer.writeError(`Usage: ${verb} <1-${max}>`);
|
|
10571
|
+
return false;
|
|
10572
|
+
}
|
|
10573
|
+
return true;
|
|
9815
10574
|
}
|
|
10575
|
+
|
|
10576
|
+
// src/auth-menu/top-menu.ts
|
|
10577
|
+
async function runTopMenu(deps) {
|
|
10578
|
+
for (; ; ) {
|
|
10579
|
+
const providers = await loadProviders(deps);
|
|
10580
|
+
renderTopMenu(deps.renderer, providers);
|
|
10581
|
+
const ids = Object.keys(providers).sort();
|
|
10582
|
+
const choice = (await deps.reader.readLine(`
|
|
10583
|
+
${color.amber("?")} Pick: `)).trim().toLowerCase();
|
|
10584
|
+
if (!choice || choice === "q" || choice === "quit" || choice === "exit") {
|
|
10585
|
+
deps.renderer.write(color.dim("Done.\n"));
|
|
10586
|
+
return 0;
|
|
10587
|
+
}
|
|
10588
|
+
if (choice === "a" || choice === "add") {
|
|
10589
|
+
await addFromCatalog(deps);
|
|
10590
|
+
continue;
|
|
10591
|
+
}
|
|
10592
|
+
if (choice === "c" || choice === "custom") {
|
|
10593
|
+
await addCustomProvider(deps);
|
|
10594
|
+
continue;
|
|
10595
|
+
}
|
|
10596
|
+
const idx = Number.parseInt(choice, 10);
|
|
10597
|
+
if (!Number.isNaN(idx) && idx >= 1 && idx <= ids.length) {
|
|
10598
|
+
const pid = ids[idx - 1];
|
|
10599
|
+
await manageProvider(pid, deps);
|
|
10600
|
+
continue;
|
|
10601
|
+
}
|
|
10602
|
+
const byId = ids.find((id) => id.toLowerCase() === choice);
|
|
10603
|
+
if (byId) {
|
|
10604
|
+
await manageProvider(byId, deps);
|
|
10605
|
+
continue;
|
|
10606
|
+
}
|
|
10607
|
+
deps.renderer.writeError(`Unknown selection: "${choice}"`);
|
|
10608
|
+
}
|
|
10609
|
+
}
|
|
10610
|
+
|
|
10611
|
+
// src/auth-menu/direct.ts
|
|
10612
|
+
init_provider_config_utils();
|
|
9816
10613
|
async function runAuthDirect(deps, opts) {
|
|
9817
10614
|
const { providerId } = opts;
|
|
9818
10615
|
const providers = await loadProviders(deps);
|
|
@@ -9840,7 +10637,9 @@ async function runAuthDirect(deps, opts) {
|
|
|
9840
10637
|
opts.baseUrl ??= knownBase;
|
|
9841
10638
|
opts.envVars ??= knownEnv;
|
|
9842
10639
|
}
|
|
9843
|
-
const usedLabels = new Set(
|
|
10640
|
+
const usedLabels = new Set(
|
|
10641
|
+
existing ? normalizeKeys(existing).map((k) => k.label) : []
|
|
10642
|
+
);
|
|
9844
10643
|
let label = opts.label ?? "default";
|
|
9845
10644
|
if (usedLabels.has(label)) {
|
|
9846
10645
|
let n = 2;
|
|
@@ -9850,7 +10649,7 @@ async function runAuthDirect(deps, opts) {
|
|
|
9850
10649
|
}
|
|
9851
10650
|
const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
|
|
9852
10651
|
if (!apiKey) return 1;
|
|
9853
|
-
await
|
|
10652
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
9854
10653
|
const p = all[providerId] ?? { type: providerId };
|
|
9855
10654
|
if (!p.type) p.type = providerId;
|
|
9856
10655
|
if (!p.family && opts.family) p.family = opts.family;
|
|
@@ -9866,26 +10665,9 @@ async function runAuthDirect(deps, opts) {
|
|
|
9866
10665
|
deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
|
|
9867
10666
|
return 0;
|
|
9868
10667
|
}
|
|
9869
|
-
async function readKeyInput(deps, intent) {
|
|
9870
|
-
const key = (await deps.reader.readSecret(
|
|
9871
|
-
` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
|
|
9872
|
-
)).trim();
|
|
9873
|
-
if (!key) {
|
|
9874
|
-
deps.renderer.writeError("No key entered.");
|
|
9875
|
-
return void 0;
|
|
9876
|
-
}
|
|
9877
|
-
return key;
|
|
9878
|
-
}
|
|
9879
|
-
function loadProviders(deps) {
|
|
9880
|
-
return loadConfigProviders(deps.globalConfigPath, deps.vault, {
|
|
9881
|
-
warn: (msg) => deps.renderer.writeWarning(msg)
|
|
9882
|
-
});
|
|
9883
|
-
}
|
|
9884
|
-
function mutateProviders(deps, mutator) {
|
|
9885
|
-
return mutateConfigProviders(deps.globalConfigPath, deps.vault, mutator);
|
|
9886
|
-
}
|
|
9887
10668
|
|
|
9888
10669
|
// src/subcommands/handlers/auth.ts
|
|
10670
|
+
init_provider_config_utils();
|
|
9889
10671
|
var authCmd = async (args, deps) => {
|
|
9890
10672
|
const flags = parseAuthFlags(args);
|
|
9891
10673
|
const menuDeps = {
|
|
@@ -9895,15 +10677,175 @@ var authCmd = async (args, deps) => {
|
|
|
9895
10677
|
vault: deps.vault,
|
|
9896
10678
|
globalConfigPath: deps.paths.globalConfig
|
|
9897
10679
|
};
|
|
9898
|
-
if (flags.positional.length === 0)
|
|
10680
|
+
if (flags.positional.length === 0) {
|
|
10681
|
+
return runTopMenu(menuDeps);
|
|
10682
|
+
}
|
|
10683
|
+
const first = flags.positional[0];
|
|
10684
|
+
if (first === "list" || first === "ls") {
|
|
10685
|
+
return runAuthList(menuDeps);
|
|
10686
|
+
}
|
|
10687
|
+
if (first === "status") {
|
|
10688
|
+
const pid = flags.positional[1];
|
|
10689
|
+
if (!pid) {
|
|
10690
|
+
deps.renderer.writeError("Usage: wstack auth status <provider>");
|
|
10691
|
+
return 1;
|
|
10692
|
+
}
|
|
10693
|
+
return runAuthStatus(menuDeps, pid);
|
|
10694
|
+
}
|
|
10695
|
+
if (first === "remove" || first === "rm") {
|
|
10696
|
+
const pid = flags.positional[1];
|
|
10697
|
+
if (!pid) {
|
|
10698
|
+
deps.renderer.writeError("Usage: wstack auth remove <provider> [--force]");
|
|
10699
|
+
return 1;
|
|
10700
|
+
}
|
|
10701
|
+
return runAuthRemove(menuDeps, pid);
|
|
10702
|
+
}
|
|
9899
10703
|
return runAuthDirect(menuDeps, {
|
|
9900
|
-
providerId:
|
|
10704
|
+
providerId: first,
|
|
9901
10705
|
label: flags.label,
|
|
9902
10706
|
family: flags.family,
|
|
9903
10707
|
baseUrl: flags.baseUrl,
|
|
9904
10708
|
envVars: flags.envVars
|
|
9905
10709
|
});
|
|
9906
10710
|
};
|
|
10711
|
+
async function runAuthList(deps) {
|
|
10712
|
+
let providers;
|
|
10713
|
+
try {
|
|
10714
|
+
providers = await loadConfigProviders(deps.globalConfigPath, deps.vault);
|
|
10715
|
+
} catch (err) {
|
|
10716
|
+
deps.renderer.writeError(`Could not read config: ${err.message}`);
|
|
10717
|
+
return 1;
|
|
10718
|
+
}
|
|
10719
|
+
const ids = Object.keys(providers).sort();
|
|
10720
|
+
if (ids.length === 0) {
|
|
10721
|
+
deps.renderer.write(
|
|
10722
|
+
`${color.dim("No providers configured.")}
|
|
10723
|
+
${color.dim("Run")} ${color.bold("wstack auth")} ${color.dim("to add one.")}
|
|
10724
|
+
`
|
|
10725
|
+
);
|
|
10726
|
+
return 0;
|
|
10727
|
+
}
|
|
10728
|
+
deps.renderer.write(`
|
|
10729
|
+
${color.bold("Saved providers")} ${color.dim(`(${ids.length})`)}
|
|
10730
|
+
|
|
10731
|
+
`);
|
|
10732
|
+
for (const id of ids) {
|
|
10733
|
+
const cfg = providers[id];
|
|
10734
|
+
if (!cfg) continue;
|
|
10735
|
+
const keys = normalizeKeys(cfg);
|
|
10736
|
+
const active = cfg.activeKey ?? keys[0]?.label;
|
|
10737
|
+
const famTag = cfg.family ? `${cfg.family}` : color.amber("no-family");
|
|
10738
|
+
const aliasHint = cfg.type && cfg.type !== id ? color.dim(` (\u2192 ${cfg.type})`) : "";
|
|
10739
|
+
const modelHint = cfg.models && cfg.models.length > 0 ? color.dim(` [${cfg.models.length} models]`) : "";
|
|
10740
|
+
deps.renderer.write(` ${color.bold(id)}${aliasHint}
|
|
10741
|
+
`);
|
|
10742
|
+
deps.renderer.write(
|
|
10743
|
+
` family: ${famTag} baseUrl: ${cfg.baseUrl ?? color.dim("unset")}${modelHint}
|
|
10744
|
+
`
|
|
10745
|
+
);
|
|
10746
|
+
if (keys.length === 0) {
|
|
10747
|
+
deps.renderer.write(` ${color.amber("no keys")}
|
|
10748
|
+
`);
|
|
10749
|
+
} else {
|
|
10750
|
+
deps.renderer.write(` ${color.dim(`${keys.length} key${keys.length === 1 ? "" : "s"}:`)}
|
|
10751
|
+
`);
|
|
10752
|
+
for (const k of keys) {
|
|
10753
|
+
const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
|
|
10754
|
+
deps.renderer.write(
|
|
10755
|
+
` ${marker} ${k.label.padEnd(18)} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}
|
|
10756
|
+
`
|
|
10757
|
+
);
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
deps.renderer.write("\n");
|
|
10761
|
+
}
|
|
10762
|
+
deps.renderer.write(
|
|
10763
|
+
color.dim(`Manage: wstack auth Add key: wstack auth <provider>
|
|
10764
|
+
`)
|
|
10765
|
+
);
|
|
10766
|
+
return 0;
|
|
10767
|
+
}
|
|
10768
|
+
async function runAuthStatus(deps, providerId) {
|
|
10769
|
+
let providers;
|
|
10770
|
+
try {
|
|
10771
|
+
providers = await loadConfigProviders(deps.globalConfigPath, deps.vault);
|
|
10772
|
+
} catch (err) {
|
|
10773
|
+
deps.renderer.writeError(`Could not read config: ${err.message}`);
|
|
10774
|
+
return 1;
|
|
10775
|
+
}
|
|
10776
|
+
const cfg = providers[providerId];
|
|
10777
|
+
if (!cfg) {
|
|
10778
|
+
deps.renderer.writeError(`Provider "${providerId}" not found in config.`);
|
|
10779
|
+
deps.renderer.write(
|
|
10780
|
+
color.dim(`Run ${color.bold("wstack auth list")} to see saved providers.
|
|
10781
|
+
`)
|
|
10782
|
+
);
|
|
10783
|
+
return 1;
|
|
10784
|
+
}
|
|
10785
|
+
const keys = normalizeKeys(cfg);
|
|
10786
|
+
const active = cfg.activeKey ?? keys[0]?.label;
|
|
10787
|
+
const lines = [
|
|
10788
|
+
`
|
|
10789
|
+
${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}`,
|
|
10790
|
+
"",
|
|
10791
|
+
` type: ${color.cyan(cfg.type ?? providerId)}`,
|
|
10792
|
+
` family: ${cfg.family ? color.cyan(cfg.family) : color.dim("unset")}`,
|
|
10793
|
+
` baseUrl: ${cfg.baseUrl ? color.cyan(cfg.baseUrl) : color.dim("unset")}`
|
|
10794
|
+
];
|
|
10795
|
+
if (cfg.models?.length) {
|
|
10796
|
+
lines.push(` models: ${color.cyan(cfg.models.join(", "))}`);
|
|
10797
|
+
}
|
|
10798
|
+
if (cfg.envVars?.length) {
|
|
10799
|
+
lines.push(` envVars: ${color.cyan(cfg.envVars.join(", "))}`);
|
|
10800
|
+
}
|
|
10801
|
+
lines.push("");
|
|
10802
|
+
if (keys.length === 0) {
|
|
10803
|
+
lines.push(color.amber(" (no keys saved)"));
|
|
10804
|
+
} else {
|
|
10805
|
+
lines.push(` ${color.dim("Keys:")}`);
|
|
10806
|
+
for (const k of keys) {
|
|
10807
|
+
const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
|
|
10808
|
+
lines.push(
|
|
10809
|
+
` ${marker} ${color.bold(k.label.padEnd(18))} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}`
|
|
10810
|
+
);
|
|
10811
|
+
}
|
|
10812
|
+
}
|
|
10813
|
+
lines.push("");
|
|
10814
|
+
lines.push(color.dim(`Manage: wstack auth \u2192 pick ${providerId}`));
|
|
10815
|
+
deps.renderer.write(lines.join("\n") + "\n");
|
|
10816
|
+
return 0;
|
|
10817
|
+
}
|
|
10818
|
+
async function runAuthRemove(deps, providerId) {
|
|
10819
|
+
const providers = await loadConfigProviders(deps.globalConfigPath, deps.vault);
|
|
10820
|
+
if (!providers[providerId]) {
|
|
10821
|
+
deps.renderer.writeError(`Provider "${providerId}" not found.`);
|
|
10822
|
+
return 1;
|
|
10823
|
+
}
|
|
10824
|
+
deps.renderer.write(
|
|
10825
|
+
`${color.amber("!")} This will remove "${providerId}" and all its saved keys.
|
|
10826
|
+
`
|
|
10827
|
+
);
|
|
10828
|
+
const answer = (await deps.reader.readLine(
|
|
10829
|
+
` ${color.amber("?")} Confirm removal? ${color.dim("[y/N]")} `
|
|
10830
|
+
)).trim().toLowerCase();
|
|
10831
|
+
if (answer !== "y" && answer !== "yes") {
|
|
10832
|
+
deps.renderer.write(color.dim("Cancelled.\n"));
|
|
10833
|
+
return 0;
|
|
10834
|
+
}
|
|
10835
|
+
try {
|
|
10836
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
10837
|
+
delete all[providerId];
|
|
10838
|
+
});
|
|
10839
|
+
deps.renderer.write(
|
|
10840
|
+
` ${color.green("\u2713")} Removed ${color.bold(providerId)}.
|
|
10841
|
+
`
|
|
10842
|
+
);
|
|
10843
|
+
return 0;
|
|
10844
|
+
} catch (err) {
|
|
10845
|
+
deps.renderer.writeError(`Failed to remove: ${err.message}`);
|
|
10846
|
+
return 1;
|
|
10847
|
+
}
|
|
10848
|
+
}
|
|
9907
10849
|
|
|
9908
10850
|
// src/subcommands/handlers/update.ts
|
|
9909
10851
|
init_update_check();
|
|
@@ -11726,6 +12668,10 @@ var helpCmd = async (_args, deps) => {
|
|
|
11726
12668
|
" wstack sessions List recent sessions",
|
|
11727
12669
|
" wstack init Pick provider + model from models.dev",
|
|
11728
12670
|
" wstack auth Interactive key manager (list/add/update/delete)",
|
|
12671
|
+
" wstack auth list Quick listing of saved providers and keys",
|
|
12672
|
+
" wstack auth status <id> Detailed view of one provider",
|
|
12673
|
+
" wstack auth remove <id> Delete a provider (asks for confirmation)",
|
|
12674
|
+
" wstack auth <provider> Add a key for a provider (--label, --family, \u2026)",
|
|
11729
12675
|
" wstack config [show|edit] Show or edit effective config",
|
|
11730
12676
|
" wstack tools List registered tools",
|
|
11731
12677
|
" wstack skills List discovered skills",
|
|
@@ -11806,22 +12752,22 @@ function fmtDuration(ms) {
|
|
|
11806
12752
|
const remMin = m - h * 60;
|
|
11807
12753
|
return `${h}h${remMin}m`;
|
|
11808
12754
|
}
|
|
11809
|
-
function fmtTaskResultLine(r,
|
|
12755
|
+
function fmtTaskResultLine(r, color56) {
|
|
11810
12756
|
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
11811
12757
|
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
11812
12758
|
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
11813
12759
|
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
11814
|
-
const errKindChip = errKind ?
|
|
11815
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
12760
|
+
const errKindChip = errKind ? color56.dim(` [${errKind}]`) : "";
|
|
12761
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color56.dim(errTail)}` : "";
|
|
11816
12762
|
switch (r.status) {
|
|
11817
12763
|
case "success":
|
|
11818
|
-
return { mark:
|
|
12764
|
+
return { mark: color56.green("\u2713"), stats, tail: "" };
|
|
11819
12765
|
case "timeout":
|
|
11820
|
-
return { mark:
|
|
12766
|
+
return { mark: color56.yellow("\u23F1"), stats: `${color56.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
11821
12767
|
case "stopped":
|
|
11822
|
-
return { mark:
|
|
12768
|
+
return { mark: color56.dim("\u2298"), stats: `${color56.dim("stopped")} ${stats}`, tail: errSnip };
|
|
11823
12769
|
case "failed":
|
|
11824
|
-
return { mark:
|
|
12770
|
+
return { mark: color56.red("\u2717"), stats: `${color56.red("failed")} ${stats}`, tail: errSnip };
|
|
11825
12771
|
}
|
|
11826
12772
|
}
|
|
11827
12773
|
|
|
@@ -12430,7 +13376,7 @@ function parsePredictions(raw, max = 3) {
|
|
|
12430
13376
|
}
|
|
12431
13377
|
return out;
|
|
12432
13378
|
}
|
|
12433
|
-
function
|
|
13379
|
+
function extractText2(content) {
|
|
12434
13380
|
if (Array.isArray(content)) {
|
|
12435
13381
|
return content[0]?.text ?? "";
|
|
12436
13382
|
}
|
|
@@ -12461,7 +13407,7 @@ async function predictNextTasks(input, opts) {
|
|
|
12461
13407
|
},
|
|
12462
13408
|
{ signal: internal.signal }
|
|
12463
13409
|
);
|
|
12464
|
-
return parsePredictions(
|
|
13410
|
+
return parsePredictions(extractText2(resp.content), max);
|
|
12465
13411
|
} catch {
|
|
12466
13412
|
return [];
|
|
12467
13413
|
} finally {
|
|
@@ -15250,7 +16196,7 @@ async function setupCodebaseIndexing(deps) {
|
|
|
15250
16196
|
let watcher;
|
|
15251
16197
|
if (idx.watchExternal) {
|
|
15252
16198
|
try {
|
|
15253
|
-
watcher =
|
|
16199
|
+
watcher = fs13.watch(projectRoot, { recursive: true }, (_event, filename) => {
|
|
15254
16200
|
if (!filename) return;
|
|
15255
16201
|
const rel = filename.toString();
|
|
15256
16202
|
if (isIgnored(rel)) return;
|
|
@@ -15578,6 +16524,8 @@ async function setupSession(params) {
|
|
|
15578
16524
|
);
|
|
15579
16525
|
const planPath = path8.join(wpaths.projectSessions, `${session?.id}.plan.json`);
|
|
15580
16526
|
context.state.setMeta("plan.path", planPath);
|
|
16527
|
+
const taskPath = path8.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
|
|
16528
|
+
context.state.setMeta("task.path", taskPath);
|
|
15581
16529
|
let dirState;
|
|
15582
16530
|
if (resumeId) {
|
|
15583
16531
|
try {
|