@wrongstack/cli 0.267.0 → 0.268.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 +2769 -653
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
package/dist/index.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, DefaultSecretScrubber, projectHash, wstackGlobalRoot,
|
|
2
|
+
import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, resolveHqDataDir, ensureHqFirstRunAuthFile, DEFAULT_HQ_REDACTION_POLICY, watchHqAuthFile, parseHqFrame, HQ_PROTOCOL_VERSION, parseHqEventPayload, scrubAndTruncateHqPreview, DefaultSecretScrubber, projectHash, wstackGlobalRoot, TOKENS, ToolRegistry, createHqPublisherFromEnv, GlobalMailbox, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, normalizeTokenSavingTier, SlashCommandRegistry, attachDepWatcherBridge, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveProjectDir, recentTextTurns, enhanceUserPrompt, projectSlug, DefaultSystemPromptBuilder, mutateTasks, loadTasks, resolveContextWindowPolicy, repairToolUseAdjacency, mutatePlan, setPlanItemStatus, getPlanTemplate, loadPlan, emptyPlan, addPlanItem, savePlan, resolveProviderModelList, DefaultLogger, DefaultModelsRegistry, isStdinTTY, atomicWrite, DefaultPathResolver, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, makeAutonomyPromptContributor, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, createDefaultPipelines, applyModelRuntime, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, WIDE_SUBAGENT_CAPABILITIES, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, AutonomousCoordinator, resolveWstackPaths, setQueuedMessagesSnapshot, noOpVault, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, DefaultSessionRewinder, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, ToolAuditLog, DefaultSessionStore as DefaultSessionStore$1, ProviderRegistry, StreamHangError, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, getContextWindowMode, AGENT_CATALOG, dispatchAgent, formatTodosList, formatTaskList, formatTaskProgress, formatPlan, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, FsError, ConfigError, InputBuilder, HQ_AUTH_FILE_VERSION, truncate, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, resolveMailboxIdentity, mutateHqAuthFile, mintHqToken, readHqAuthFile, isSecretField as isSecretField$1 } from '@wrongstack/core';
|
|
3
3
|
import * as fsp5 from 'fs/promises';
|
|
4
4
|
import { decryptConfigSecrets, encryptConfigSecrets, DefaultSecretVault, isSecretField } from '@wrongstack/core/security';
|
|
5
5
|
import * as path4 from 'path';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import * as crypto3 from 'crypto';
|
|
8
|
-
import { createHash,
|
|
8
|
+
import { createHash, randomUUID, randomBytes } from 'crypto';
|
|
9
|
+
import * as http from 'http';
|
|
10
|
+
import { createServer } from 'http';
|
|
11
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
12
|
import { createRequire } from 'module';
|
|
10
13
|
import * as os from 'os';
|
|
11
14
|
import os__default from 'os';
|
|
12
15
|
import { findFreePort, AutoPhaseWebSocketHandler, generateAuthToken, verifyClient, handleGitInfo, handleShellOpen, handleGitDiff, handleGitChanges, handleSkillsExport, handleSkillsEdit, handleSkillsCreate, handleSkillsUpdate, handleSkillsUninstall, handleSkillsInstall, handleSkillsContent, handleMcpRestart, handleMcpDisable, handleMcpEnable, handleMcpDiscover, handleMcpSleep, handleMcpWake, handleMcpUpdate, handleMcpRemove, handleMcpAdd, handleMcpList, handleMemoryForget, handleMemoryRemember, handleMemoryList, handleFilesWrite, handleFilesRead, handleFilesTree, handleFilesList, createEternalSubscription, createHttpServer, registerInstance, openBrowser as openBrowser$1, unregisterInstance, estimateTokens as estimateTokens$1, stringifyContent, messagePreview, messageTokens, createCustomModeStore } from '@wrongstack/webui/server';
|
|
13
16
|
import { setOAuthTokenPersister, makeProviderFromConfig, capabilitiesFor, buildProviderFactoriesFromRegistry, refreshCopilotToken, copilotBaseUrlFromToken } from '@wrongstack/providers';
|
|
14
|
-
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
17
|
+
import { toErrorMessage, readJsonObjectFile, updateJsonObjectFile, setJsonPath, jsonObjectFileExists, removeJsonPath } from '@wrongstack/core/utils';
|
|
15
18
|
import { getProcessRegistry, builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes, shutdownCodebaseIndexHost, TIER2_TOOLS, TIER3_TOOLS, TIER1_TOOLS, resetIndexCircuitBreaker } from '@wrongstack/tools';
|
|
16
19
|
import { DefaultSessionStore } from '@wrongstack/core/storage';
|
|
17
20
|
import { probeLocalLlm } from '@wrongstack/runtime/probe';
|
|
18
21
|
import * as fs3 from 'fs';
|
|
19
22
|
import { watch, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
20
23
|
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
21
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
22
24
|
import { spawn, execFile, execFileSync } from 'child_process';
|
|
23
25
|
import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
|
|
24
26
|
import { fileURLToPath } from 'url';
|
|
@@ -28,7 +30,6 @@ import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, runEnsemble, renderEnsembleT
|
|
|
28
30
|
import { parseNextSteps } from '@wrongstack/tui';
|
|
29
31
|
import { WrongStackACPServer } from '@wrongstack/acp/agent';
|
|
30
32
|
import { SubagentBudget } from '@wrongstack/core/coordination';
|
|
31
|
-
import { createServer } from 'http';
|
|
32
33
|
import { loadBenchConfig, reportHeaderLine, readSummary, renderMarkdownReport, createPolyglotSuite, createSwebenchSuite, runBenchmark, writeJsonArtifacts, collectCellPredictions, writePredictionsJsonl, gradePolyglot, gradeSwebench } from '@wrongstack/bench';
|
|
33
34
|
import { allServers } from '@wrongstack/core/infrastructure';
|
|
34
35
|
import { ToolExecutor } from '@wrongstack/core/execution';
|
|
@@ -655,26 +656,26 @@ function fmtDuration(ms) {
|
|
|
655
656
|
const remMin = m - h * 60;
|
|
656
657
|
return `${h}h${remMin}m`;
|
|
657
658
|
}
|
|
658
|
-
function fmtTaskResultLine(r,
|
|
659
|
+
function fmtTaskResultLine(r, color78) {
|
|
659
660
|
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
660
661
|
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
661
662
|
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
662
663
|
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
663
|
-
const errKindChip = errKind ?
|
|
664
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
664
|
+
const errKindChip = errKind ? color78.dim(` [${errKind}]`) : "";
|
|
665
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color78.dim(errTail)}` : "";
|
|
665
666
|
switch (r.status) {
|
|
666
667
|
case "success":
|
|
667
|
-
return { mark:
|
|
668
|
+
return { mark: color78.green("\u2713"), stats, tail: "" };
|
|
668
669
|
case "timeout":
|
|
669
670
|
return {
|
|
670
|
-
mark:
|
|
671
|
-
stats: `${
|
|
671
|
+
mark: color78.yellow("\u23F1"),
|
|
672
|
+
stats: `${color78.yellow("timeout")} ${stats}`,
|
|
672
673
|
tail: errSnip
|
|
673
674
|
};
|
|
674
675
|
case "stopped":
|
|
675
|
-
return { mark:
|
|
676
|
+
return { mark: color78.dim("\u2298"), stats: `${color78.dim("stopped")} ${stats}`, tail: errSnip };
|
|
676
677
|
case "failed":
|
|
677
|
-
return { mark:
|
|
678
|
+
return { mark: color78.red("\u2717"), stats: `${color78.red("failed")} ${stats}`, tail: errSnip };
|
|
678
679
|
}
|
|
679
680
|
}
|
|
680
681
|
var init_utils = __esm({
|
|
@@ -2663,7 +2664,7 @@ async function runProjectPicker(opts) {
|
|
|
2663
2664
|
const reservedBottom = 3;
|
|
2664
2665
|
const headerHeight = reservedTop + reservedBottom;
|
|
2665
2666
|
const baseVisibleHeight = Math.max(5, terminalHeight() - headerHeight);
|
|
2666
|
-
return new Promise((
|
|
2667
|
+
return new Promise((resolve12) => {
|
|
2667
2668
|
const wasRaw = stdin.isRaw;
|
|
2668
2669
|
const wasPaused = stdin.isPaused();
|
|
2669
2670
|
let filter = "";
|
|
@@ -2787,7 +2788,7 @@ async function runProjectPicker(opts) {
|
|
|
2787
2788
|
cleanup();
|
|
2788
2789
|
out.write(CURSOR_SHOW);
|
|
2789
2790
|
out.write("\n");
|
|
2790
|
-
|
|
2791
|
+
resolve12(void 0);
|
|
2791
2792
|
return;
|
|
2792
2793
|
}
|
|
2793
2794
|
if (ch === ESC) {
|
|
@@ -2800,7 +2801,7 @@ async function runProjectPicker(opts) {
|
|
|
2800
2801
|
cleanup();
|
|
2801
2802
|
out.write(CURSOR_SHOW);
|
|
2802
2803
|
out.write("\n");
|
|
2803
|
-
|
|
2804
|
+
resolve12(void 0);
|
|
2804
2805
|
return;
|
|
2805
2806
|
}
|
|
2806
2807
|
if (ch === BS || ch === "\b") {
|
|
@@ -2817,22 +2818,22 @@ async function runProjectPicker(opts) {
|
|
|
2817
2818
|
out.write(CURSOR_SHOW);
|
|
2818
2819
|
out.write("\n");
|
|
2819
2820
|
if (!item || item.key === "__divider__") {
|
|
2820
|
-
|
|
2821
|
+
resolve12(void 0);
|
|
2821
2822
|
return;
|
|
2822
2823
|
}
|
|
2823
2824
|
if (item.key === "quit") {
|
|
2824
|
-
|
|
2825
|
+
resolve12(void 0);
|
|
2825
2826
|
return;
|
|
2826
2827
|
}
|
|
2827
2828
|
if (item.key === "new-session") {
|
|
2828
|
-
|
|
2829
|
+
resolve12({ kind: "action", key: "new-session", action: "new-session" });
|
|
2829
2830
|
return;
|
|
2830
2831
|
}
|
|
2831
2832
|
if (item.key === "prev-sessions") {
|
|
2832
|
-
|
|
2833
|
+
resolve12({ kind: "action", key: "prev-sessions", action: "prev-sessions" });
|
|
2833
2834
|
return;
|
|
2834
2835
|
}
|
|
2835
|
-
|
|
2836
|
+
resolve12({ kind: "project", key: item.key });
|
|
2836
2837
|
return;
|
|
2837
2838
|
}
|
|
2838
2839
|
if (filter.length === 0) {
|
|
@@ -2840,7 +2841,7 @@ async function runProjectPicker(opts) {
|
|
|
2840
2841
|
cleanup();
|
|
2841
2842
|
out.write(CURSOR_SHOW);
|
|
2842
2843
|
out.write("\n");
|
|
2843
|
-
|
|
2844
|
+
resolve12(void 0);
|
|
2844
2845
|
return;
|
|
2845
2846
|
}
|
|
2846
2847
|
if (ch === "j") {
|
|
@@ -2869,7 +2870,7 @@ async function runProjectPicker(opts) {
|
|
|
2869
2870
|
try {
|
|
2870
2871
|
stdin.setRawMode(true);
|
|
2871
2872
|
} catch {
|
|
2872
|
-
|
|
2873
|
+
resolve12(void 0);
|
|
2873
2874
|
return;
|
|
2874
2875
|
}
|
|
2875
2876
|
stdin.resume();
|
|
@@ -2880,7 +2881,7 @@ async function runProjectPicker(opts) {
|
|
|
2880
2881
|
stdin.once("close", () => {
|
|
2881
2882
|
cleanup();
|
|
2882
2883
|
out.write(CURSOR_SHOW);
|
|
2883
|
-
|
|
2884
|
+
resolve12(void 0);
|
|
2884
2885
|
});
|
|
2885
2886
|
});
|
|
2886
2887
|
}
|
|
@@ -2906,6 +2907,1108 @@ var init_project_picker = __esm({
|
|
|
2906
2907
|
}
|
|
2907
2908
|
});
|
|
2908
2909
|
|
|
2910
|
+
// src/hq-server.ts
|
|
2911
|
+
var hq_server_exports = {};
|
|
2912
|
+
__export(hq_server_exports, {
|
|
2913
|
+
startHqServer: () => startHqServer
|
|
2914
|
+
});
|
|
2915
|
+
function displayHost(host) {
|
|
2916
|
+
return host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2917
|
+
}
|
|
2918
|
+
function buildHttpUrl(host, port, token) {
|
|
2919
|
+
const url = new URL(`http://${displayHost(host)}:${port}/`);
|
|
2920
|
+
if (token) url.searchParams.set("token", token);
|
|
2921
|
+
return url.toString();
|
|
2922
|
+
}
|
|
2923
|
+
function buildClientWsUrl(host, port, token) {
|
|
2924
|
+
const url = new URL(`ws://${displayHost(host)}:${port}/ws/client`);
|
|
2925
|
+
if (token) url.searchParams.set("token", token);
|
|
2926
|
+
return url.toString();
|
|
2927
|
+
}
|
|
2928
|
+
function writeHqStartupInfo(write, handle) {
|
|
2929
|
+
const firstRun = handle.firstRunSetup;
|
|
2930
|
+
write(`WrongStack HQ listening on http://${handle.host}:${handle.port}
|
|
2931
|
+
`);
|
|
2932
|
+
if (firstRun) {
|
|
2933
|
+
write(`Browser endpoint: ${firstRun.browserUrl}
|
|
2934
|
+
`);
|
|
2935
|
+
write(`Client endpoint: ${buildClientWsUrl(handle.host, handle.port, firstRun.clientEnv.WRONGSTACK_HQ_TOKEN)}
|
|
2936
|
+
`);
|
|
2937
|
+
write(`
|
|
2938
|
+
First-run HQ auth created in ${firstRun.dataDir}
|
|
2939
|
+
`);
|
|
2940
|
+
write(`Start clients with:
|
|
2941
|
+
`);
|
|
2942
|
+
write(` WRONGSTACK_HQ_URL=${firstRun.clientEnv.WRONGSTACK_HQ_URL}
|
|
2943
|
+
`);
|
|
2944
|
+
write(` WRONGSTACK_HQ_TOKEN=${firstRun.clientEnv.WRONGSTACK_HQ_TOKEN}
|
|
2945
|
+
`);
|
|
2946
|
+
} else {
|
|
2947
|
+
write(`Client endpoint: ws://${handle.host}:${handle.port}/ws/client
|
|
2948
|
+
`);
|
|
2949
|
+
write(`Browser endpoint: http://${handle.host}:${handle.port}
|
|
2950
|
+
`);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
async function startHqServer(options = {}) {
|
|
2954
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
2955
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
2956
|
+
const dataDir = resolveHqDataDir(options.dataDir);
|
|
2957
|
+
const firstRunAuth = await ensureHqFirstRunAuthFile(dataDir, {
|
|
2958
|
+
warn: (msg) => console.warn(JSON.stringify({ level: "warn", event: "hq.auth_load_failed", message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
2959
|
+
});
|
|
2960
|
+
return startHqServerWithAuth(options, host, port, dataDir, firstRunAuth);
|
|
2961
|
+
}
|
|
2962
|
+
function extractBrowserToken(req2, url) {
|
|
2963
|
+
const queryToken = url.searchParams.get("token");
|
|
2964
|
+
if (queryToken) return queryToken;
|
|
2965
|
+
const auth = req2.headers.authorization;
|
|
2966
|
+
if (typeof auth === "string" && auth.toLowerCase().startsWith("bearer ")) {
|
|
2967
|
+
return auth.slice(7).trim();
|
|
2968
|
+
}
|
|
2969
|
+
return void 0;
|
|
2970
|
+
}
|
|
2971
|
+
function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth) {
|
|
2972
|
+
const authFile = firstRunAuth.authFile;
|
|
2973
|
+
const mutableAuth = {
|
|
2974
|
+
operatorPolicy: {
|
|
2975
|
+
...DEFAULT_HQ_REDACTION_POLICY,
|
|
2976
|
+
...authFile.redactionPolicy ?? {}
|
|
2977
|
+
},
|
|
2978
|
+
browserTokens: new Set((authFile.browserTokens ?? []).map((t) => t.token)),
|
|
2979
|
+
clientTokens: new Set((authFile.clientTokens ?? []).map((t) => t.token))
|
|
2980
|
+
};
|
|
2981
|
+
console.warn(JSON.stringify({
|
|
2982
|
+
level: "info",
|
|
2983
|
+
event: "hq.startup",
|
|
2984
|
+
message: "WrongStack HQ starting",
|
|
2985
|
+
dataDir,
|
|
2986
|
+
host,
|
|
2987
|
+
port,
|
|
2988
|
+
operatorPolicyActive: authFile.redactionPolicy !== void 0,
|
|
2989
|
+
browserTokenMode: mutableAuth.browserTokens.size > 0,
|
|
2990
|
+
clientTokenMode: mutableAuth.clientTokens.size > 0,
|
|
2991
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2992
|
+
}));
|
|
2993
|
+
return new Promise((resolve12, reject) => {
|
|
2994
|
+
const clients = /* @__PURE__ */ new Map();
|
|
2995
|
+
const browsers = /* @__PURE__ */ new Set();
|
|
2996
|
+
const eventLog = [];
|
|
2997
|
+
const httpServer = http.createServer((req2, res) => {
|
|
2998
|
+
const url = new URL(req2.url ?? "/", `http://${host}:${port}`);
|
|
2999
|
+
if (mutableAuth.browserTokens.size > 0) {
|
|
3000
|
+
const supplied = extractBrowserToken(req2, url);
|
|
3001
|
+
if (!supplied || !mutableAuth.browserTokens.has(supplied)) {
|
|
3002
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
3003
|
+
res.end(
|
|
3004
|
+
JSON.stringify({
|
|
3005
|
+
error: {
|
|
3006
|
+
code: "INVALID_TOKEN",
|
|
3007
|
+
message: "A valid ?token= or Authorization: Bearer is required for HTTP access in browser token mode."
|
|
3008
|
+
}
|
|
3009
|
+
})
|
|
3010
|
+
);
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
3015
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3016
|
+
res.end(HQ_HTML);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
if (url.pathname === "/api/snapshot") {
|
|
3020
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3021
|
+
res.end(JSON.stringify(buildSnapshot(clients)));
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
if (url.pathname.startsWith("/api/projects/")) {
|
|
3025
|
+
const projectId = decodeURIComponent(url.pathname.slice("/api/projects/".length));
|
|
3026
|
+
if (!projectId) {
|
|
3027
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3028
|
+
res.end(
|
|
3029
|
+
JSON.stringify({ error: { code: "BAD_REQUEST", message: "projectId is required" } })
|
|
3030
|
+
);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const detail = buildProjectDetail(clients, projectId);
|
|
3034
|
+
if (!detail) {
|
|
3035
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3036
|
+
res.end(
|
|
3037
|
+
JSON.stringify({
|
|
3038
|
+
error: { code: "NOT_FOUND", message: `Unknown project: ${projectId}` }
|
|
3039
|
+
})
|
|
3040
|
+
);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3044
|
+
res.end(JSON.stringify(detail));
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3048
|
+
res.end("Not found");
|
|
3049
|
+
});
|
|
3050
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 1 * 1024 * 1024 });
|
|
3051
|
+
httpServer.on("upgrade", (req2, socket, head) => {
|
|
3052
|
+
const url = new URL(req2.url ?? "/", `http://${host}:${port}`);
|
|
3053
|
+
const pathname = url.pathname;
|
|
3054
|
+
if (pathname !== "/ws/client" && pathname !== "/ws/browser") {
|
|
3055
|
+
socket.destroy();
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
const tokenSet = pathname === "/ws/browser" ? mutableAuth.browserTokens : mutableAuth.clientTokens;
|
|
3059
|
+
if (tokenSet.size > 0) {
|
|
3060
|
+
const supplied = url.searchParams.get("token") ?? "";
|
|
3061
|
+
if (!supplied || !tokenSet.has(supplied)) {
|
|
3062
|
+
socket.write(
|
|
3063
|
+
"HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
|
|
3064
|
+
error: {
|
|
3065
|
+
code: "INVALID_TOKEN",
|
|
3066
|
+
message: `A valid ?token= is required for ${pathname} connections in token mode.`
|
|
3067
|
+
}
|
|
3068
|
+
})
|
|
3069
|
+
);
|
|
3070
|
+
socket.destroy();
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
wss.handleUpgrade(req2, socket, head, (ws) => {
|
|
3075
|
+
wss.emit("connection", ws, req2, pathname);
|
|
3076
|
+
});
|
|
3077
|
+
});
|
|
3078
|
+
wss.on("connection", (ws, _req, pathname) => {
|
|
3079
|
+
if (pathname === "/ws/browser") {
|
|
3080
|
+
handleBrowser(ws, clients, browsers);
|
|
3081
|
+
} else {
|
|
3082
|
+
handleClient(ws, clients, browsers, eventLog, mutableAuth.operatorPolicy);
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
const authWatcher = watchHqAuthFile(
|
|
3086
|
+
dataDir,
|
|
3087
|
+
(next) => {
|
|
3088
|
+
mutableAuth.operatorPolicy = {
|
|
3089
|
+
...DEFAULT_HQ_REDACTION_POLICY,
|
|
3090
|
+
...next.redactionPolicy ?? {}
|
|
3091
|
+
};
|
|
3092
|
+
mutableAuth.browserTokens = new Set((next.browserTokens ?? []).map((t) => t.token));
|
|
3093
|
+
mutableAuth.clientTokens = new Set((next.clientTokens ?? []).map((t) => t.token));
|
|
3094
|
+
console.warn(JSON.stringify({
|
|
3095
|
+
level: "info",
|
|
3096
|
+
event: "hq.auth.reloaded",
|
|
3097
|
+
message: "HQ auth.json reloaded",
|
|
3098
|
+
browserTokenCount: mutableAuth.browserTokens.size,
|
|
3099
|
+
clientTokenCount: mutableAuth.clientTokens.size,
|
|
3100
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3101
|
+
}));
|
|
3102
|
+
},
|
|
3103
|
+
{
|
|
3104
|
+
warn: (msg) => console.warn(JSON.stringify({
|
|
3105
|
+
level: "warn",
|
|
3106
|
+
event: "hq.auth.reload_failed",
|
|
3107
|
+
message: msg,
|
|
3108
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3109
|
+
}))
|
|
3110
|
+
}
|
|
3111
|
+
);
|
|
3112
|
+
const onError = (err) => {
|
|
3113
|
+
if (err.code === "EADDRINUSE" && !options.strictPort) {
|
|
3114
|
+
httpServer.listen(port + 1, host);
|
|
3115
|
+
} else {
|
|
3116
|
+
reject(err);
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3119
|
+
httpServer.once("error", onError);
|
|
3120
|
+
httpServer.listen(port, host, () => {
|
|
3121
|
+
httpServer.removeListener("error", onError);
|
|
3122
|
+
const addr = httpServer.address();
|
|
3123
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
3124
|
+
const firstRunSetup = firstRunAuth.created && firstRunAuth.browserToken && firstRunAuth.clientToken ? {
|
|
3125
|
+
dataDir,
|
|
3126
|
+
browserUrl: buildHttpUrl(host, actualPort, firstRunAuth.browserToken.token),
|
|
3127
|
+
clientEnv: {
|
|
3128
|
+
WRONGSTACK_HQ_URL: `http://${displayHost(host)}:${actualPort}`,
|
|
3129
|
+
WRONGSTACK_HQ_TOKEN: firstRunAuth.clientToken.token
|
|
3130
|
+
}
|
|
3131
|
+
} : void 0;
|
|
3132
|
+
const handle = {
|
|
3133
|
+
host,
|
|
3134
|
+
port: actualPort,
|
|
3135
|
+
...firstRunSetup ? { firstRunSetup } : {},
|
|
3136
|
+
close: () => new Promise((res) => {
|
|
3137
|
+
authWatcher.close();
|
|
3138
|
+
for (const ws of browsers) ws.close(1001, "HQ shutting down");
|
|
3139
|
+
for (const ws of clients.keys()) ws.close(1001, "HQ shutting down");
|
|
3140
|
+
wss.close();
|
|
3141
|
+
httpServer.close(() => res());
|
|
3142
|
+
})
|
|
3143
|
+
};
|
|
3144
|
+
writeHqStartupInfo((line) => console.log(line.trimEnd()), handle);
|
|
3145
|
+
resolve12(handle);
|
|
3146
|
+
});
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
function handleBrowser(ws, clients, browsers) {
|
|
3150
|
+
browsers.add(ws);
|
|
3151
|
+
const snapshotMsg = { type: "hq.snapshot", snapshot: buildSnapshot(clients) };
|
|
3152
|
+
ws.send(JSON.stringify(snapshotMsg));
|
|
3153
|
+
ws.on("close", () => {
|
|
3154
|
+
browsers.delete(ws);
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
function handleClient(ws, clients, browsers, eventLog, operatorPolicy) {
|
|
3158
|
+
let registered = false;
|
|
3159
|
+
ws.on("message", (data) => {
|
|
3160
|
+
const raw = typeof data === "string" ? data : Buffer.isBuffer(data) ? data : new TextDecoder().decode(data);
|
|
3161
|
+
const parsed = parseHqFrame(raw);
|
|
3162
|
+
if (!parsed.ok) {
|
|
3163
|
+
const code = parsed.reason === "invalid-json" ? 1003 : 1008;
|
|
3164
|
+
ws.close(code, `invalid frame: ${parsed.reason}`);
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
const frame = parsed.frame;
|
|
3168
|
+
if (frame.type === "client.hello") {
|
|
3169
|
+
const payload = frame.payload;
|
|
3170
|
+
if (payload.protocolVersion !== HQ_PROTOCOL_VERSION) {
|
|
3171
|
+
ws.close(1008, "protocol version mismatch");
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
const client = {
|
|
3175
|
+
ws,
|
|
3176
|
+
clientId: payload.client.clientId,
|
|
3177
|
+
projectId: payload.project.projectId,
|
|
3178
|
+
kind: payload.client.kind,
|
|
3179
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3180
|
+
lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3181
|
+
...payload.client.hostname ? { hostname: payload.client.hostname } : {},
|
|
3182
|
+
...payload.client.pid ? { pid: payload.client.pid } : {},
|
|
3183
|
+
...payload.client.version ? { version: payload.client.version } : {},
|
|
3184
|
+
capabilities: payload.capabilities,
|
|
3185
|
+
mailboxes: /* @__PURE__ */ new Map()
|
|
3186
|
+
};
|
|
3187
|
+
clients.set(ws, client);
|
|
3188
|
+
registered = true;
|
|
3189
|
+
const welcome = {
|
|
3190
|
+
type: "hq.welcome",
|
|
3191
|
+
protocolVersion: HQ_PROTOCOL_VERSION,
|
|
3192
|
+
serverTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3193
|
+
acceptedCapabilities: payload.capabilities,
|
|
3194
|
+
// The operator-configured override (from <dataDir>/auth.json) wins
|
|
3195
|
+
// over the default. The client learns the *effective* policy.
|
|
3196
|
+
redactionPolicy: operatorPolicy
|
|
3197
|
+
};
|
|
3198
|
+
ws.send(JSON.stringify(welcome));
|
|
3199
|
+
const event = {
|
|
3200
|
+
id: randomUUID(),
|
|
3201
|
+
type: "client.hello",
|
|
3202
|
+
schemaVersion: HQ_PROTOCOL_VERSION,
|
|
3203
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3204
|
+
clientId: payload.client.clientId,
|
|
3205
|
+
projectId: payload.project.projectId,
|
|
3206
|
+
seq: 0,
|
|
3207
|
+
payload: { client: payload.client, project: payload.project }
|
|
3208
|
+
};
|
|
3209
|
+
eventLog.push(event);
|
|
3210
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
|
|
3211
|
+
broadcastSnapshot(clients, browsers);
|
|
3212
|
+
broadcastEvent(event, browsers);
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
if (!registered) return;
|
|
3216
|
+
if (frame.type === "client.event") {
|
|
3217
|
+
const event = frame.event;
|
|
3218
|
+
const client = clients.get(ws);
|
|
3219
|
+
if (client) client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3220
|
+
if (event.type === "mailbox.snapshot" && client !== void 0) {
|
|
3221
|
+
const payloadResult = parseHqEventPayload(event.type, event.payload);
|
|
3222
|
+
if (payloadResult.ok) {
|
|
3223
|
+
const payload = payloadResult.payload;
|
|
3224
|
+
client.mailboxes.set(payload.mailboxId, payload);
|
|
3225
|
+
eventLog.push(event);
|
|
3226
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
|
|
3227
|
+
broadcastSnapshot(clients, browsers);
|
|
3228
|
+
broadcastEvent(event, browsers);
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
if (event.type === "mailbox.event") {
|
|
3234
|
+
const payloadResult = parseHqEventPayload(event.type, event.payload);
|
|
3235
|
+
if (!payloadResult.ok) {
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
const payload = payloadResult.payload;
|
|
3239
|
+
const sanitizedSummary = scrubAndTruncateHqPreview(payload.summary, 280);
|
|
3240
|
+
const sanitizedEvent = sanitizedSummary === void 0 ? event : { ...event, payload: { ...payload, summary: sanitizedSummary } };
|
|
3241
|
+
eventLog.push(sanitizedEvent);
|
|
3242
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
|
|
3243
|
+
broadcastEvent(sanitizedEvent, browsers);
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
eventLog.push(event);
|
|
3247
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
|
|
3248
|
+
broadcastEvent(event, browsers);
|
|
3249
|
+
}
|
|
3250
|
+
});
|
|
3251
|
+
ws.on("close", () => {
|
|
3252
|
+
clients.delete(ws);
|
|
3253
|
+
broadcastSnapshot(clients, browsers);
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
function buildSnapshot(clients) {
|
|
3257
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3258
|
+
const clientRecords = [];
|
|
3259
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
3260
|
+
const mailboxSummaries = [];
|
|
3261
|
+
for (const client of clients.values()) {
|
|
3262
|
+
clientRecords.push({
|
|
3263
|
+
clientId: client.clientId,
|
|
3264
|
+
kind: client.kind,
|
|
3265
|
+
machineId: "",
|
|
3266
|
+
...client.hostname ? { hostname: client.hostname } : {},
|
|
3267
|
+
...client.pid ? { pid: client.pid } : {},
|
|
3268
|
+
...client.version ? { version: client.version } : {},
|
|
3269
|
+
connected: true,
|
|
3270
|
+
connectedAt: client.connectedAt,
|
|
3271
|
+
lastSeenAt: client.lastSeenAt,
|
|
3272
|
+
projectId: client.projectId,
|
|
3273
|
+
capabilities: client.capabilities
|
|
3274
|
+
});
|
|
3275
|
+
let project = projectMap.get(client.projectId);
|
|
3276
|
+
if (!project) {
|
|
3277
|
+
project = {
|
|
3278
|
+
projectId: client.projectId,
|
|
3279
|
+
projectName: client.projectId,
|
|
3280
|
+
projectRootDisplay: "",
|
|
3281
|
+
machineIds: [],
|
|
3282
|
+
activeClients: 0,
|
|
3283
|
+
activeSessions: 0,
|
|
3284
|
+
activeSubagents: 0,
|
|
3285
|
+
totalCostUsd: 0,
|
|
3286
|
+
lastActivityAt: now,
|
|
3287
|
+
status: "active"
|
|
3288
|
+
};
|
|
3289
|
+
projectMap.set(client.projectId, project);
|
|
3290
|
+
}
|
|
3291
|
+
project.activeClients++;
|
|
3292
|
+
for (const snapshot of client.mailboxes.values()) {
|
|
3293
|
+
mailboxSummaries.push({
|
|
3294
|
+
mailboxId: snapshot.mailboxId,
|
|
3295
|
+
projectId: client.projectId,
|
|
3296
|
+
scope: snapshot.scope,
|
|
3297
|
+
messageCount: snapshot.totals.messages,
|
|
3298
|
+
unreadCount: snapshot.totals.unread,
|
|
3299
|
+
incompleteCount: snapshot.totals.incomplete,
|
|
3300
|
+
highPriorityCount: snapshot.totals.highPriority,
|
|
3301
|
+
onlineAgentCount: snapshot.totals.onlineAgents,
|
|
3302
|
+
lastActivityAt: now
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
const projects = Array.from(projectMap.values());
|
|
3307
|
+
const totals = computeTotals({
|
|
3308
|
+
projects: projects.length,
|
|
3309
|
+
clients: clientRecords.length,
|
|
3310
|
+
mailboxes: mailboxSummaries
|
|
3311
|
+
});
|
|
3312
|
+
return {
|
|
3313
|
+
generatedAt: now,
|
|
3314
|
+
clients: clientRecords,
|
|
3315
|
+
projects,
|
|
3316
|
+
sessions: [],
|
|
3317
|
+
fleets: [],
|
|
3318
|
+
mailboxes: mailboxSummaries,
|
|
3319
|
+
totals
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
function computeTotals(input) {
|
|
3323
|
+
let unread = 0;
|
|
3324
|
+
let incomplete = 0;
|
|
3325
|
+
for (const m of input.mailboxes) {
|
|
3326
|
+
unread += m.unreadCount;
|
|
3327
|
+
incomplete += m.incompleteCount;
|
|
3328
|
+
}
|
|
3329
|
+
return {
|
|
3330
|
+
activeProjects: input.projects,
|
|
3331
|
+
activeClients: input.clients,
|
|
3332
|
+
activeSessions: 0,
|
|
3333
|
+
activeSubagents: 0,
|
|
3334
|
+
unreadMailboxMessages: unread,
|
|
3335
|
+
incompleteMailboxMessages: incomplete,
|
|
3336
|
+
totalCostUsd: 0
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
function buildProjectDetail(clients, projectId) {
|
|
3340
|
+
const projectClients = [];
|
|
3341
|
+
for (const c of clients.values()) {
|
|
3342
|
+
if (c.projectId === projectId) projectClients.push(c);
|
|
3343
|
+
}
|
|
3344
|
+
if (projectClients.length === 0) return null;
|
|
3345
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3346
|
+
const clientRecords = projectClients.map((c) => ({
|
|
3347
|
+
clientId: c.clientId,
|
|
3348
|
+
kind: c.kind,
|
|
3349
|
+
machineId: "",
|
|
3350
|
+
...c.hostname ? { hostname: c.hostname } : {},
|
|
3351
|
+
...c.pid ? { pid: c.pid } : {},
|
|
3352
|
+
...c.version ? { version: c.version } : {},
|
|
3353
|
+
connected: true,
|
|
3354
|
+
connectedAt: c.connectedAt,
|
|
3355
|
+
lastSeenAt: c.lastSeenAt,
|
|
3356
|
+
projectId: c.projectId,
|
|
3357
|
+
capabilities: c.capabilities
|
|
3358
|
+
}));
|
|
3359
|
+
const mailboxPayloads = [];
|
|
3360
|
+
let latestActivity = now;
|
|
3361
|
+
for (const c of projectClients) {
|
|
3362
|
+
for (const snap of c.mailboxes.values()) {
|
|
3363
|
+
mailboxPayloads.push(snap);
|
|
3364
|
+
if (snap.totals.messages > 0) latestActivity = now;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
const project = {
|
|
3368
|
+
projectId,
|
|
3369
|
+
projectName: projectId,
|
|
3370
|
+
projectRootDisplay: "",
|
|
3371
|
+
machineIds: [],
|
|
3372
|
+
activeClients: projectClients.length,
|
|
3373
|
+
activeSessions: 0,
|
|
3374
|
+
activeSubagents: 0,
|
|
3375
|
+
totalCostUsd: 0,
|
|
3376
|
+
lastActivityAt: latestActivity,
|
|
3377
|
+
status: "active"
|
|
3378
|
+
};
|
|
3379
|
+
return {
|
|
3380
|
+
generatedAt: now,
|
|
3381
|
+
project,
|
|
3382
|
+
clients: clientRecords,
|
|
3383
|
+
mailboxes: mailboxPayloads
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
function broadcastSnapshot(clients, browsers) {
|
|
3387
|
+
const snapshot = buildSnapshot(clients);
|
|
3388
|
+
const msg = { type: "hq.snapshot", snapshot };
|
|
3389
|
+
const data = JSON.stringify(msg);
|
|
3390
|
+
for (const ws of browsers) {
|
|
3391
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
function broadcastEvent(event, browsers) {
|
|
3395
|
+
const msg = { type: "hq.event", event };
|
|
3396
|
+
const data = JSON.stringify(msg);
|
|
3397
|
+
for (const ws of browsers) {
|
|
3398
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
var DEFAULT_HOST, DEFAULT_PORT, MAX_EVENT_LOG, HQ_HTML;
|
|
3402
|
+
var init_hq_server = __esm({
|
|
3403
|
+
"src/hq-server.ts"() {
|
|
3404
|
+
DEFAULT_HOST = "127.0.0.1";
|
|
3405
|
+
DEFAULT_PORT = 3499;
|
|
3406
|
+
MAX_EVENT_LOG = 500;
|
|
3407
|
+
HQ_HTML = `<!DOCTYPE html>
|
|
3408
|
+
<html lang="en">
|
|
3409
|
+
<head>
|
|
3410
|
+
<meta charset="UTF-8" />
|
|
3411
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
3412
|
+
<title>WrongStack HQ</title>
|
|
3413
|
+
<style>
|
|
3414
|
+
:root {
|
|
3415
|
+
--bg: #0d1117;
|
|
3416
|
+
--panel: #161b22;
|
|
3417
|
+
--border: #30363d;
|
|
3418
|
+
--text: #c9d1d9;
|
|
3419
|
+
--muted: #8b949e;
|
|
3420
|
+
--dim: #6e7681;
|
|
3421
|
+
--accent: #58a6ff;
|
|
3422
|
+
--live: #3fb950;
|
|
3423
|
+
--warn: #d29922;
|
|
3424
|
+
--high: #f85149;
|
|
3425
|
+
}
|
|
3426
|
+
* { box-sizing: border-box; }
|
|
3427
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 24px; }
|
|
3428
|
+
h1 { margin: 0 0 4px; color: var(--accent); font-size: 22px; }
|
|
3429
|
+
.hq-sub { color: var(--muted); font-size: 13px; margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
|
|
3430
|
+
.hq-led { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
|
|
3431
|
+
.hq-led.live { background: var(--live); box-shadow: 0 0 6px var(--live); }
|
|
3432
|
+
.hq-led.dead { background: var(--dim); }
|
|
3433
|
+
.hq-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
|
3434
|
+
.hq-stat { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; }
|
|
3435
|
+
.hq-stat .num { font-size: 26px; font-weight: 700; color: #f0f6fc; line-height: 1.1; }
|
|
3436
|
+
.hq-stat .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); margin-top: 4px; }
|
|
3437
|
+
.hq-stat.warn .num { color: var(--warn); }
|
|
3438
|
+
.hq-stat.high .num { color: var(--high); }
|
|
3439
|
+
section { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
3440
|
+
section h2 { margin: 0 0 12px; font-size: 14px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--muted); font-weight: 600; }
|
|
3441
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3442
|
+
th { text-align: left; font-weight: 600; color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 10px; border-bottom: 1px solid var(--border); }
|
|
3443
|
+
td { padding: 10px; border-bottom: 1px solid #21262d; }
|
|
3444
|
+
tr:last-child td { border-bottom: none; }
|
|
3445
|
+
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
3446
|
+
td .pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: #21262d; color: var(--muted); }
|
|
3447
|
+
td .pill.project { background: rgba(88,166,255,0.15); color: var(--accent); }
|
|
3448
|
+
td .pill.global { background: rgba(63,185,80,0.15); color: var(--live); }
|
|
3449
|
+
.empty { color: var(--dim); font-style: italic; padding: 12px 0; font-size: 13px; }
|
|
3450
|
+
.badge { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 11px; background: #21262d; color: var(--muted); margin-right: 4px; }
|
|
3451
|
+
.project-link { color: var(--accent); cursor: pointer; text-decoration: none; }
|
|
3452
|
+
.project-link:hover { text-decoration: underline; }
|
|
3453
|
+
.drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: none; z-index: 50; }
|
|
3454
|
+
.drawer-backdrop.open { display: block; }
|
|
3455
|
+
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(720px, 90vw); background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,0.5); transform: translateX(100%); transition: transform 0.18s ease; overflow-y: auto; z-index: 51; padding: 24px; }
|
|
3456
|
+
.drawer.open { transform: translateX(0); }
|
|
3457
|
+
.drawer h2 { margin: 0 0 4px; color: var(--accent); font-size: 18px; }
|
|
3458
|
+
.drawer .drawer-meta { color: var(--muted); font-size: 12px; margin-bottom: 20px; }
|
|
3459
|
+
.drawer .drawer-close { float: right; background: transparent; border: 1px solid var(--border); color: var(--text); padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 12px; }
|
|
3460
|
+
.drawer .drawer-close:hover { background: #21262d; }
|
|
3461
|
+
.msg-row { padding: 10px; border-bottom: 1px solid #21262d; font-size: 13px; }
|
|
3462
|
+
.msg-row:last-child { border-bottom: none; }
|
|
3463
|
+
.msg-row .msg-subject { font-weight: 600; color: #f0f6fc; }
|
|
3464
|
+
.msg-row .msg-meta { color: var(--dim); font-size: 11px; margin-top: 2px; }
|
|
3465
|
+
.msg-row .msg-preview { color: var(--muted); font-size: 12px; margin-top: 4px; font-style: italic; }
|
|
3466
|
+
.pill.priority-high { background: rgba(248,81,73,0.18); color: var(--high); }
|
|
3467
|
+
.pill.priority-normal { background: rgba(88,166,255,0.15); color: var(--accent); }
|
|
3468
|
+
.pill.priority-low { background: #21262d; color: var(--dim); }
|
|
3469
|
+
.hq-toolbar { display: flex; align-items: center; gap: 8px; margin: 12px 0 16px; padding: 8px 12px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; }
|
|
3470
|
+
.hq-toolbar label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); }
|
|
3471
|
+
.hq-toolbar select { background: #0d1117; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; font-size: 13px; min-width: 280px; }
|
|
3472
|
+
.hq-toolbar select:focus { outline: none; border-color: var(--accent); }
|
|
3473
|
+
.feed-status { float: right; font-size: 10px; text-transform: none; letter-spacing: 0; color: var(--dim); }
|
|
3474
|
+
.feed-status.live { color: var(--live); }
|
|
3475
|
+
.feed-row { padding: 8px 10px; border-bottom: 1px solid #21262d; font-size: 12px; animation: feed-flash 0.6s ease-out; }
|
|
3476
|
+
.feed-row:last-child { border-bottom: none; }
|
|
3477
|
+
.feed-row .feed-action { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; margin-right: 6px; background: #21262d; color: var(--muted); font-family: ui-monospace, monospace; }
|
|
3478
|
+
.feed-row .feed-action.message-sent { background: rgba(88,166,255,0.18); color: var(--accent); }
|
|
3479
|
+
.feed-row .feed-action.message-completed { background: rgba(63,185,80,0.18); color: var(--live); }
|
|
3480
|
+
.feed-row .feed-action.message-read { background: rgba(139,148,158,0.18); color: var(--muted); }
|
|
3481
|
+
.feed-row .feed-action.agent-offline { background: rgba(248,81,73,0.18); color: var(--high); }
|
|
3482
|
+
.feed-row .feed-action.agent-registered { background: rgba(63,185,80,0.18); color: var(--live); }
|
|
3483
|
+
.feed-row .feed-meta { color: var(--dim); font-size: 10px; margin-top: 2px; }
|
|
3484
|
+
@keyframes feed-flash { 0% { background: rgba(88,166,255,0.25); } 100% { background: transparent; } }
|
|
3485
|
+
</style>
|
|
3486
|
+
</head>
|
|
3487
|
+
<body>
|
|
3488
|
+
<h1>\u{1F4CB} WrongStack HQ</h1>
|
|
3489
|
+
<p class="hq-sub" id="hq-conn"><span class="hq-led dead" id="hq-led"></span>Connecting\u2026</p>
|
|
3490
|
+
<div class="hq-toolbar">
|
|
3491
|
+
<label for="project-picker">Project:</label>
|
|
3492
|
+
<select id="project-picker" aria-label="Select project">
|
|
3493
|
+
<option value="">\u2014 Select project \u2014</option>
|
|
3494
|
+
</select>
|
|
3495
|
+
</div>
|
|
3496
|
+
|
|
3497
|
+
<div class="hq-grid">
|
|
3498
|
+
<div class="hq-stat"><span class="num" id="stat-clients">0</span><div class="label">Active clients</div></div>
|
|
3499
|
+
<div class="hq-stat"><span class="num" id="stat-projects">0</span><div class="label">Projects</div></div>
|
|
3500
|
+
<div class="hq-stat warn"><span class="num" id="stat-mailboxes">0</span><div class="label">Mailboxes</div></div>
|
|
3501
|
+
<div class="hq-stat warn"><span class="num" id="stat-unread">0</span><div class="label">Unread messages</div></div>
|
|
3502
|
+
<div class="hq-stat warn"><span class="num" id="stat-incomplete">0</span><div class="label">Open messages</div></div>
|
|
3503
|
+
<div class="hq-stat high"><span class="num" id="stat-high">0</span><div class="label">High priority</div></div>
|
|
3504
|
+
<div class="hq-stat"><span class="num" id="stat-agents">0</span><div class="label">Online agents</div></div>
|
|
3505
|
+
</div>
|
|
3506
|
+
|
|
3507
|
+
<section>
|
|
3508
|
+
<h2>\u{1F4EC} Mailboxes</h2>
|
|
3509
|
+
<table>
|
|
3510
|
+
<thead>
|
|
3511
|
+
<tr>
|
|
3512
|
+
<th>Mailbox</th>
|
|
3513
|
+
<th>Scope</th>
|
|
3514
|
+
<th>Project</th>
|
|
3515
|
+
<th class="num">Messages</th>
|
|
3516
|
+
<th class="num">Unread</th>
|
|
3517
|
+
<th class="num">Open</th>
|
|
3518
|
+
<th class="num">High</th>
|
|
3519
|
+
<th class="num">Agents</th>
|
|
3520
|
+
</tr>
|
|
3521
|
+
</thead>
|
|
3522
|
+
<tbody id="tbody-mailboxes">
|
|
3523
|
+
<tr><td colspan="8" class="empty">No mailboxes yet. Connect a TUI/REPL/WebUI client with WRONGSTACK_HQ_URL set.</td></tr>
|
|
3524
|
+
</tbody>
|
|
3525
|
+
</table>
|
|
3526
|
+
</section>
|
|
3527
|
+
|
|
3528
|
+
<section>
|
|
3529
|
+
<h2>\u{1F465} Clients</h2>
|
|
3530
|
+
<table>
|
|
3531
|
+
<thead>
|
|
3532
|
+
<tr>
|
|
3533
|
+
<th>Client ID</th>
|
|
3534
|
+
<th>Kind</th>
|
|
3535
|
+
<th>Project</th>
|
|
3536
|
+
<th>Capabilities</th>
|
|
3537
|
+
<th>Last seen</th>
|
|
3538
|
+
</tr>
|
|
3539
|
+
</thead>
|
|
3540
|
+
<tbody id="tbody-clients">
|
|
3541
|
+
<tr><td colspan="5" class="empty">No clients connected yet.</td></tr>
|
|
3542
|
+
</tbody>
|
|
3543
|
+
</table>
|
|
3544
|
+
</section>
|
|
3545
|
+
|
|
3546
|
+
<div class="drawer-backdrop" id="drawer-backdrop"></div>
|
|
3547
|
+
<aside class="drawer" id="drawer" aria-hidden="true">
|
|
3548
|
+
<button class="drawer-close" id="drawer-close">Close</button>
|
|
3549
|
+
<h2 id="drawer-title">Project</h2>
|
|
3550
|
+
<p class="drawer-meta" id="drawer-meta"></p>
|
|
3551
|
+
<section>
|
|
3552
|
+
<h2>\u{1F4EC} Mailboxes</h2>
|
|
3553
|
+
<table>
|
|
3554
|
+
<thead>
|
|
3555
|
+
<tr>
|
|
3556
|
+
<th>Mailbox</th>
|
|
3557
|
+
<th>Scope</th>
|
|
3558
|
+
<th class="num">Messages</th>
|
|
3559
|
+
<th class="num">Unread</th>
|
|
3560
|
+
<th class="num">Agents</th>
|
|
3561
|
+
</tr>
|
|
3562
|
+
</thead>
|
|
3563
|
+
<tbody id="drawer-mailboxes">
|
|
3564
|
+
<tr><td colspan="5" class="empty">Loading\u2026</td></tr>
|
|
3565
|
+
</tbody>
|
|
3566
|
+
</table>
|
|
3567
|
+
</section>
|
|
3568
|
+
<section>
|
|
3569
|
+
<h2>\u{1F4E8} Recent messages</h2>
|
|
3570
|
+
<div id="drawer-messages">
|
|
3571
|
+
<p class="empty">Loading\u2026</p>
|
|
3572
|
+
</div>
|
|
3573
|
+
</section>
|
|
3574
|
+
<section>
|
|
3575
|
+
<h2>\u{1F4E1} Live mailbox events
|
|
3576
|
+
<span class="feed-status" id="feed-status">(idle)</span>
|
|
3577
|
+
</h2>
|
|
3578
|
+
<div id="drawer-event-feed">
|
|
3579
|
+
<p class="empty">No mailbox events yet for this project.</p>
|
|
3580
|
+
</div>
|
|
3581
|
+
</section>
|
|
3582
|
+
<section>
|
|
3583
|
+
<h2>\u{1F465} Clients</h2>
|
|
3584
|
+
<table>
|
|
3585
|
+
<thead>
|
|
3586
|
+
<tr>
|
|
3587
|
+
<th>Client ID</th>
|
|
3588
|
+
<th>Kind</th>
|
|
3589
|
+
<th>Last seen</th>
|
|
3590
|
+
</tr>
|
|
3591
|
+
</thead>
|
|
3592
|
+
<tbody id="drawer-clients">
|
|
3593
|
+
<tr><td colspan="3" class="empty">Loading\u2026</td></tr>
|
|
3594
|
+
</tbody>
|
|
3595
|
+
</table>
|
|
3596
|
+
</section>
|
|
3597
|
+
</aside>
|
|
3598
|
+
|
|
3599
|
+
<script>
|
|
3600
|
+
const led = document.getElementById('hq-led');
|
|
3601
|
+
const connText = document.getElementById('hq-conn');
|
|
3602
|
+
|
|
3603
|
+
function el(id) { return document.getElementById(id); }
|
|
3604
|
+
|
|
3605
|
+
function fmtTime(iso) {
|
|
3606
|
+
if (!iso) return '\u2014';
|
|
3607
|
+
const d = new Date(iso);
|
|
3608
|
+
if (isNaN(d.getTime())) return '\u2014';
|
|
3609
|
+
return d.toLocaleTimeString();
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function shortId(s) {
|
|
3613
|
+
if (!s) return '\u2014';
|
|
3614
|
+
return s.length > 12 ? s.slice(0, 6) + '\u2026' + s.slice(-4) : s;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
function renderMailboxes(mailboxes) {
|
|
3618
|
+
const tbody = el('tbody-mailboxes');
|
|
3619
|
+
if (!mailboxes || mailboxes.length === 0) {
|
|
3620
|
+
tbody.innerHTML = '<tr><td colspan="8" class="empty">No mailboxes yet. Connect a TUI/REPL/WebUI client with WRONGSTACK_HQ_URL set.</td></tr>';
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
tbody.innerHTML = mailboxes.map((m) => {
|
|
3624
|
+
const scopeClass = m.scope === 'global' ? 'global' : 'project';
|
|
3625
|
+
const projectCell = '<a href="#' + encodeURIComponent(m.projectId) + '" class="project-link" data-project="' + escapeHtml(m.projectId) + '">' + escapeHtml(shortId(m.projectId)) + '</a>';
|
|
3626
|
+
return '<tr>' +
|
|
3627
|
+
'<td><code>' + escapeHtml(shortId(m.mailboxId)) + '</code></td>' +
|
|
3628
|
+
'<td><span class="pill ' + scopeClass + '">' + escapeHtml(m.scope) + '</span></td>' +
|
|
3629
|
+
'<td>' + projectCell + '</td>' +
|
|
3630
|
+
'<td class="num">' + m.messageCount + '</td>' +
|
|
3631
|
+
'<td class="num">' + (m.unreadCount > 0 ? '<strong>' + m.unreadCount + '</strong>' : '0') + '</td>' +
|
|
3632
|
+
'<td class="num">' + m.incompleteCount + '</td>' +
|
|
3633
|
+
'<td class="num">' + (m.highPriorityCount > 0 ? '<strong style="color:var(--high)">' + m.highPriorityCount + '</strong>' : '0') + '</td>' +
|
|
3634
|
+
'<td class="num">' + m.onlineAgentCount + '</td>' +
|
|
3635
|
+
'</tr>';
|
|
3636
|
+
}).join('');
|
|
3637
|
+
wireProjectLinks();
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function renderClients(clients) {
|
|
3641
|
+
const tbody = el('tbody-clients');
|
|
3642
|
+
if (!clients || clients.length === 0) {
|
|
3643
|
+
tbody.innerHTML = '<tr><td colspan="5" class="empty">No clients connected yet.</td></tr>';
|
|
3644
|
+
return;
|
|
3645
|
+
}
|
|
3646
|
+
tbody.innerHTML = clients.map((c) => {
|
|
3647
|
+
const caps = (c.capabilities || []).map((cap) => '<span class="badge">' + escapeHtml(cap) + '</span>').join('');
|
|
3648
|
+
return '<tr>' +
|
|
3649
|
+
'<td><code>' + escapeHtml(shortId(c.clientId)) + '</code></td>' +
|
|
3650
|
+
'<td>' + escapeHtml(c.kind) + '</td>' +
|
|
3651
|
+
'<td>' + escapeHtml(shortId(c.projectId)) + '</td>' +
|
|
3652
|
+
'<td>' + caps + '</td>' +
|
|
3653
|
+
'<td>' + fmtTime(c.lastSeenAt) + '</td>' +
|
|
3654
|
+
'</tr>';
|
|
3655
|
+
}).join('');
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
function escapeHtml(s) {
|
|
3659
|
+
if (s === null || s === undefined) return '';
|
|
3660
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
3661
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
function applySnapshot(s) {
|
|
3666
|
+
el('stat-clients').textContent = s.totals.activeClients;
|
|
3667
|
+
el('stat-projects').textContent = s.totals.activeProjects;
|
|
3668
|
+
el('stat-unread').textContent = s.totals.unreadMailboxMessages;
|
|
3669
|
+
el('stat-incomplete').textContent = s.totals.incompleteMailboxMessages;
|
|
3670
|
+
|
|
3671
|
+
let totalMessages = 0;
|
|
3672
|
+
let highPriority = 0;
|
|
3673
|
+
let onlineAgents = 0;
|
|
3674
|
+
for (const m of (s.mailboxes || [])) {
|
|
3675
|
+
totalMessages += m.messageCount;
|
|
3676
|
+
highPriority += m.highPriorityCount;
|
|
3677
|
+
onlineAgents += m.onlineAgentCount;
|
|
3678
|
+
}
|
|
3679
|
+
el('stat-mailboxes').textContent = (s.mailboxes || []).length;
|
|
3680
|
+
el('stat-high').textContent = highPriority;
|
|
3681
|
+
el('stat-agents').textContent = onlineAgents;
|
|
3682
|
+
|
|
3683
|
+
renderMailboxes(s.mailboxes || []);
|
|
3684
|
+
renderClients(s.clients || []);
|
|
3685
|
+
renderProjectPicker(s.projects || []);
|
|
3686
|
+
|
|
3687
|
+
// Auto-refresh the open drawer if the open project is still active in
|
|
3688
|
+
// the live snapshot. Debounced so rapid burst updates trigger one fetch.
|
|
3689
|
+
if (currentDetailProjectId) {
|
|
3690
|
+
const stillActive = (s.projects || []).some((p) => p.projectId === currentDetailProjectId);
|
|
3691
|
+
if (stillActive) scheduleAutoRefresh();
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// ---------- Project drilldown drawer ----------
|
|
3696
|
+
|
|
3697
|
+
// Current detail request token; if the URL changes (e.g. user picks a
|
|
3698
|
+
// different project) we discard stale responses.
|
|
3699
|
+
let currentDetailToken = 0;
|
|
3700
|
+
let currentDetailProjectId = null;
|
|
3701
|
+
let autoRefreshTimer = null;
|
|
3702
|
+
let lastAutoRefreshAt = null;
|
|
3703
|
+
// Per-project live event feed (ring buffer per project). Keyed by
|
|
3704
|
+
// projectId so switching drawers keeps each project's history.
|
|
3705
|
+
const eventFeeds = new Map();
|
|
3706
|
+
const FEED_MAX = 50;
|
|
3707
|
+
let feedIdleTimer = null;
|
|
3708
|
+
|
|
3709
|
+
function parseInitialProject() {
|
|
3710
|
+
// Prefer ?project=ID over #ID so URL copy/paste stays predictable even
|
|
3711
|
+
// when fragments would otherwise be lost on server round-trips.
|
|
3712
|
+
const url = new URL(location.href);
|
|
3713
|
+
const fromQuery = url.searchParams.get('project');
|
|
3714
|
+
if (fromQuery) return fromQuery;
|
|
3715
|
+
if (location.hash.length > 1) {
|
|
3716
|
+
try { return decodeURIComponent(location.hash.slice(1)); } catch { return null; }
|
|
3717
|
+
}
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
function pushProjectUrl(projectId) {
|
|
3722
|
+
const url = new URL(location.href);
|
|
3723
|
+
url.searchParams.set('project', projectId);
|
|
3724
|
+
url.hash = '';
|
|
3725
|
+
history.replaceState(null, '', url.pathname + url.search);
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
function clearProjectUrl() {
|
|
3729
|
+
const url = new URL(location.href);
|
|
3730
|
+
url.searchParams.delete('project');
|
|
3731
|
+
url.hash = '';
|
|
3732
|
+
history.replaceState(null, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams.toString() : ''));
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function renderProjectPicker(projects) {
|
|
3736
|
+
const sel = el('project-picker');
|
|
3737
|
+
if (!sel) return;
|
|
3738
|
+
const current = currentDetailProjectId || '';
|
|
3739
|
+
sel.innerHTML =
|
|
3740
|
+
'<option value="">\u2014 Select project \u2014</option>' +
|
|
3741
|
+
(projects || []).map((p) =>
|
|
3742
|
+
'<option value="' + escapeHtml(p.projectId) + '"' +
|
|
3743
|
+
(p.projectId === current ? ' selected' : '') +
|
|
3744
|
+
'>' + escapeHtml(p.projectName || p.projectId) + ' (' + (p.activeClients || 0) + ')</option>'
|
|
3745
|
+
).join('');
|
|
3746
|
+
sel.onchange = () => {
|
|
3747
|
+
const v = sel.value;
|
|
3748
|
+
if (v) openProject(v);
|
|
3749
|
+
else closeProject();
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
function setDrawerMeta(detail) {
|
|
3754
|
+
if (!detail || !detail.project) return;
|
|
3755
|
+
const p = detail.project;
|
|
3756
|
+
const refreshed = lastAutoRefreshAt ? ' \xB7 Last refreshed ' + fmtTime(lastAutoRefreshAt) : '';
|
|
3757
|
+
el('drawer-meta').textContent =
|
|
3758
|
+
'Active clients: ' + (p.activeClients || 0) +
|
|
3759
|
+
' \xB7 Generated: ' + fmtTime(detail.generatedAt) +
|
|
3760
|
+
refreshed;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
function fetchProjectDetail(projectId, opts) {
|
|
3764
|
+
const token = ++currentDetailToken;
|
|
3765
|
+
const silent = opts && opts.silent;
|
|
3766
|
+
if (!silent) {
|
|
3767
|
+
el('drawer-meta').textContent = 'Refreshing\u2026';
|
|
3768
|
+
}
|
|
3769
|
+
fetch('/api/projects/' + encodeURIComponent(projectId))
|
|
3770
|
+
.then((r) => {
|
|
3771
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
3772
|
+
return r.json();
|
|
3773
|
+
})
|
|
3774
|
+
.then((d) => {
|
|
3775
|
+
if (token !== currentDetailToken) return; // stale
|
|
3776
|
+
renderProjectDetail(d);
|
|
3777
|
+
lastAutoRefreshAt = new Date();
|
|
3778
|
+
setDrawerMeta(d);
|
|
3779
|
+
})
|
|
3780
|
+
.catch((err) => {
|
|
3781
|
+
if (token !== currentDetailToken) return;
|
|
3782
|
+
if (!silent) {
|
|
3783
|
+
el('drawer-meta').textContent = 'Failed to load: ' + escapeHtml(String(err.message || err));
|
|
3784
|
+
}
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
function scheduleAutoRefresh() {
|
|
3789
|
+
if (autoRefreshTimer) clearTimeout(autoRefreshTimer);
|
|
3790
|
+
autoRefreshTimer = setTimeout(() => {
|
|
3791
|
+
autoRefreshTimer = null;
|
|
3792
|
+
if (currentDetailProjectId) fetchProjectDetail(currentDetailProjectId, { silent: true });
|
|
3793
|
+
}, 250);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
function openProject(projectId) {
|
|
3797
|
+
if (!projectId) return;
|
|
3798
|
+
const drawer = el('drawer');
|
|
3799
|
+
const backdrop = el('drawer-backdrop');
|
|
3800
|
+
el('drawer-title').textContent = projectId;
|
|
3801
|
+
el('drawer-meta').textContent = 'Loading\u2026';
|
|
3802
|
+
el('drawer-mailboxes').innerHTML = '<tr><td colspan="5" class="empty">Loading\u2026</td></tr>';
|
|
3803
|
+
el('drawer-messages').innerHTML = '<p class="empty">Loading\u2026</p>';
|
|
3804
|
+
el('drawer-clients').innerHTML = '<tr><td colspan="3" class="empty">Loading\u2026</td></tr>';
|
|
3805
|
+
drawer.classList.add('open');
|
|
3806
|
+
backdrop.classList.add('open');
|
|
3807
|
+
drawer.setAttribute('aria-hidden', 'false');
|
|
3808
|
+
pushProjectUrl(projectId);
|
|
3809
|
+
currentDetailProjectId = projectId;
|
|
3810
|
+
lastAutoRefreshAt = null;
|
|
3811
|
+
// Render any mailbox events the project received while the drawer was
|
|
3812
|
+
// closed so the live feed ring buffer is visible immediately on open.
|
|
3813
|
+
const existingFeed = eventFeeds.get(projectId);
|
|
3814
|
+
if (existingFeed) renderEventFeed(existingFeed);
|
|
3815
|
+
fetchProjectDetail(projectId, { silent: false });
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
function closeProject() {
|
|
3819
|
+
const drawer = el('drawer');
|
|
3820
|
+
const backdrop = el('drawer-backdrop');
|
|
3821
|
+
drawer.classList.remove('open');
|
|
3822
|
+
backdrop.classList.remove('open');
|
|
3823
|
+
drawer.setAttribute('aria-hidden', 'true');
|
|
3824
|
+
currentDetailProjectId = null;
|
|
3825
|
+
currentDetailToken++;
|
|
3826
|
+
if (autoRefreshTimer) {
|
|
3827
|
+
clearTimeout(autoRefreshTimer);
|
|
3828
|
+
autoRefreshTimer = null;
|
|
3829
|
+
}
|
|
3830
|
+
lastAutoRefreshAt = null;
|
|
3831
|
+
clearProjectUrl();
|
|
3832
|
+
const sel = el('project-picker');
|
|
3833
|
+
if (sel) sel.value = '';
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
function renderProjectDetail(detail) {
|
|
3837
|
+
if (!detail || !detail.project) {
|
|
3838
|
+
el('drawer-meta').textContent = 'No data.';
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
setDrawerMeta(detail);
|
|
3842
|
+
|
|
3843
|
+
// Mailboxes
|
|
3844
|
+
const mbs = detail.mailboxes || [];
|
|
3845
|
+
el('drawer-mailboxes').innerHTML = mbs.length === 0
|
|
3846
|
+
? '<tr><td colspan="5" class="empty">No mailboxes reported for this project yet.</td></tr>'
|
|
3847
|
+
: mbs.map((m) => {
|
|
3848
|
+
const scopeClass = m.scope === 'global' ? 'global' : 'project';
|
|
3849
|
+
return '<tr>' +
|
|
3850
|
+
'<td><code>' + escapeHtml(shortId(m.mailboxId)) + '</code></td>' +
|
|
3851
|
+
'<td><span class="pill ' + scopeClass + '">' + escapeHtml(m.scope) + '</span></td>' +
|
|
3852
|
+
'<td class="num">' + m.totals.messages + '</td>' +
|
|
3853
|
+
'<td class="num">' + (m.totals.unread > 0 ? '<strong>' + m.totals.unread + '</strong>' : '0') + '</td>' +
|
|
3854
|
+
'<td class="num">' + m.totals.onlineAgents + '</td>' +
|
|
3855
|
+
'</tr>';
|
|
3856
|
+
}).join('');
|
|
3857
|
+
|
|
3858
|
+
// Recent messages \u2014 flatten + sort by timestamp desc, take 20
|
|
3859
|
+
const allMessages = [];
|
|
3860
|
+
for (const m of mbs) {
|
|
3861
|
+
for (const msg of (m.messages || [])) allMessages.push(msg);
|
|
3862
|
+
}
|
|
3863
|
+
allMessages.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
3864
|
+
const recent = allMessages.slice(0, 20);
|
|
3865
|
+
el('drawer-messages').innerHTML = recent.length === 0
|
|
3866
|
+
? '<p class="empty">No messages in any mailbox snapshot yet.</p>'
|
|
3867
|
+
: recent.map((m) => {
|
|
3868
|
+
const priorityClass = 'priority-' + (m.priority || 'normal');
|
|
3869
|
+
const preview = m.bodyPreview ? '<div class="msg-preview">' + escapeHtml(m.bodyPreview) + '</div>' : '';
|
|
3870
|
+
const task = m.task ? ' \xB7 task: ' + escapeHtml(m.task.status || '?') : '';
|
|
3871
|
+
return '<div class="msg-row">' +
|
|
3872
|
+
'<span class="pill ' + priorityClass + '">' + escapeHtml(m.priority || 'normal') + '</span>' +
|
|
3873
|
+
'<span class="pill">' + escapeHtml(m.type || '?') + '</span>' +
|
|
3874
|
+
'<span class="msg-subject"> ' + escapeHtml(m.subject || '(no subject)') + '</span>' +
|
|
3875
|
+
'<div class="msg-meta">' + escapeHtml(m.from || '?') + ' \u2192 ' + escapeHtml(m.to || '?') + ' \xB7 ' + fmtTime(m.timestamp) + (m.completed ? ' \xB7 \u2713 completed' : '') + task + '</div>' +
|
|
3876
|
+
preview +
|
|
3877
|
+
'</div>';
|
|
3878
|
+
}).join('');
|
|
3879
|
+
|
|
3880
|
+
// Clients
|
|
3881
|
+
const cs = detail.clients || [];
|
|
3882
|
+
el('drawer-clients').innerHTML = cs.length === 0
|
|
3883
|
+
? '<tr><td colspan="3" class="empty">No clients for this project.</td></tr>'
|
|
3884
|
+
: cs.map((c) =>
|
|
3885
|
+
'<tr>' +
|
|
3886
|
+
'<td><code>' + escapeHtml(shortId(c.clientId)) + '</code></td>' +
|
|
3887
|
+
'<td>' + escapeHtml(c.kind) + '</td>' +
|
|
3888
|
+
'<td>' + fmtTime(c.lastSeenAt) + '</td>' +
|
|
3889
|
+
'</tr>'
|
|
3890
|
+
).join('');
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
function wireProjectLinks() {
|
|
3894
|
+
const links = document.querySelectorAll('a.project-link');
|
|
3895
|
+
links.forEach((a) => {
|
|
3896
|
+
a.onclick = (ev) => {
|
|
3897
|
+
ev.preventDefault();
|
|
3898
|
+
const pid = a.getAttribute('data-project') || '';
|
|
3899
|
+
openProject(pid);
|
|
3900
|
+
};
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
el('drawer-close').onclick = closeProject;
|
|
3905
|
+
el('drawer-backdrop').onclick = closeProject;
|
|
3906
|
+
document.addEventListener('keydown', (ev) => {
|
|
3907
|
+
if (ev.key === 'Escape') closeProject();
|
|
3908
|
+
});
|
|
3909
|
+
|
|
3910
|
+
// Respond to browser back/forward to switch projects.
|
|
3911
|
+
window.addEventListener('popstate', () => {
|
|
3912
|
+
const pid = parseInitialProject();
|
|
3913
|
+
if (pid) {
|
|
3914
|
+
if (pid !== currentDetailProjectId) openProject(pid);
|
|
3915
|
+
} else if (currentDetailProjectId) {
|
|
3916
|
+
closeProject();
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
|
|
3920
|
+
// Open drawer automatically if URL has ?project=ID or #projectId.
|
|
3921
|
+
const initialProject = parseInitialProject();
|
|
3922
|
+
if (initialProject) openProject(initialProject);
|
|
3923
|
+
|
|
3924
|
+
function connect() {
|
|
3925
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
3926
|
+
const ws = new WebSocket(proto + '//' + location.host + '/ws/browser');
|
|
3927
|
+
ws.onopen = () => {
|
|
3928
|
+
led.className = 'hq-led live';
|
|
3929
|
+
connText.innerHTML = '<span class="hq-led live"></span>Connected to HQ';
|
|
3930
|
+
};
|
|
3931
|
+
ws.onmessage = (ev) => {
|
|
3932
|
+
try {
|
|
3933
|
+
const msg = JSON.parse(ev.data);
|
|
3934
|
+
if (msg.type === 'hq.snapshot') applySnapshot(msg.snapshot);
|
|
3935
|
+
else if (msg.type === 'hq.event') handleHqEvent(msg.event);
|
|
3936
|
+
} catch {}
|
|
3937
|
+
};
|
|
3938
|
+
ws.onclose = () => {
|
|
3939
|
+
led.className = 'hq-led dead';
|
|
3940
|
+
connText.innerHTML = '<span class="hq-led dead"></span>Disconnected \u2014 reconnecting\u2026';
|
|
3941
|
+
setTimeout(connect, 2000);
|
|
3942
|
+
};
|
|
3943
|
+
ws.onerror = () => ws.close();
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
// ---------- Live mailbox event feed ----------
|
|
3947
|
+
|
|
3948
|
+
function handleHqEvent(event) {
|
|
3949
|
+
if (!event || event.type !== 'mailbox.event') return;
|
|
3950
|
+
const projectId = event.projectId;
|
|
3951
|
+
if (!projectId) return;
|
|
3952
|
+
const list = eventFeeds.get(projectId) || [];
|
|
3953
|
+
list.unshift(event); // newest first
|
|
3954
|
+
if (list.length > FEED_MAX) list.length = FEED_MAX;
|
|
3955
|
+
eventFeeds.set(projectId, list);
|
|
3956
|
+
|
|
3957
|
+
// Only re-render if the open drawer matches this project's id.
|
|
3958
|
+
if (projectId === currentDetailProjectId) {
|
|
3959
|
+
renderEventFeed(list);
|
|
3960
|
+
flashFeedStatus();
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
function renderEventFeed(list) {
|
|
3965
|
+
const elFeed = el('drawer-event-feed');
|
|
3966
|
+
if (!elFeed) return;
|
|
3967
|
+
if (!list || list.length === 0) {
|
|
3968
|
+
elFeed.innerHTML = '<p class="empty">No mailbox events yet for this project.</p>';
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
elFeed.innerHTML = list.map((evt) => {
|
|
3972
|
+
const p = evt.payload || {};
|
|
3973
|
+
const action = escapeHtml(p.action || '?');
|
|
3974
|
+
let detail = '';
|
|
3975
|
+
if (p.summary) detail = escapeHtml(p.summary);
|
|
3976
|
+
else if (p.message) detail = escapeHtml((p.message.subject || '(no subject)') + ' \xB7 ' + (p.message.from || '?') + ' \u2192 ' + (p.message.to || '?'));
|
|
3977
|
+
else if (p.agent) detail = escapeHtml((p.agent.name || p.agent.agentId || '?') + ' (' + (p.agent.status || '?') + ')');
|
|
3978
|
+
else detail = '<em>no detail</em>';
|
|
3979
|
+
const mailboxShort = p.mailboxId ? ' \xB7 ' + escapeHtml(shortId(p.mailboxId)) : '';
|
|
3980
|
+
return '<div class="feed-row">' +
|
|
3981
|
+
'<span class="feed-action ' + action + '">' + action + '</span>' +
|
|
3982
|
+
'<span>' + detail + '</span>' +
|
|
3983
|
+
'<div class="feed-meta">' + fmtTime(evt.timestamp) + mailboxShort + '</div>' +
|
|
3984
|
+
'</div>';
|
|
3985
|
+
}).join('');
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function clearEventFeed(projectId) {
|
|
3989
|
+
eventFeeds.delete(projectId);
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
function flashFeedStatus() {
|
|
3993
|
+
const status = el('feed-status');
|
|
3994
|
+
if (!status) return;
|
|
3995
|
+
status.textContent = '(live)';
|
|
3996
|
+
status.className = 'feed-status live';
|
|
3997
|
+
if (feedIdleTimer) clearTimeout(feedIdleTimer);
|
|
3998
|
+
feedIdleTimer = setTimeout(() => {
|
|
3999
|
+
status.textContent = '(idle)';
|
|
4000
|
+
status.className = 'feed-status';
|
|
4001
|
+
feedIdleTimer = null;
|
|
4002
|
+
}, 1500);
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
connect();
|
|
4006
|
+
</script>
|
|
4007
|
+
</body>
|
|
4008
|
+
</html>`;
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
|
|
2909
4012
|
// src/update-check.ts
|
|
2910
4013
|
var update_check_exports = {};
|
|
2911
4014
|
__export(update_check_exports, {
|
|
@@ -3457,10 +4560,10 @@ function handlePing(ctx, ws) {
|
|
|
3457
4560
|
ctx.send(ws, { type: "pong", payload: {} });
|
|
3458
4561
|
}
|
|
3459
4562
|
function handleToolConfirmResult(ctx, id, decision) {
|
|
3460
|
-
const
|
|
3461
|
-
if (
|
|
4563
|
+
const resolve12 = ctx.pendingConfirms.get(id);
|
|
4564
|
+
if (resolve12) {
|
|
3462
4565
|
ctx.pendingConfirms.delete(id);
|
|
3463
|
-
|
|
4566
|
+
resolve12(decision);
|
|
3464
4567
|
}
|
|
3465
4568
|
}
|
|
3466
4569
|
var init_connection = __esm({
|
|
@@ -3872,8 +4975,8 @@ async function handleProjectsSelect(ctx, ws, payload) {
|
|
|
3872
4975
|
const { root, name: projectName } = payload;
|
|
3873
4976
|
try {
|
|
3874
4977
|
const resolved = path4.resolve(root);
|
|
3875
|
-
const
|
|
3876
|
-
if (!
|
|
4978
|
+
const stat8 = await fsp5.stat(resolved).catch(() => null);
|
|
4979
|
+
if (!stat8?.isDirectory()) {
|
|
3877
4980
|
ctx.send(ws, {
|
|
3878
4981
|
type: "projects.selected",
|
|
3879
4982
|
payload: {
|
|
@@ -3964,8 +5067,8 @@ async function handleProjectsAdd(ctx, ws, payload) {
|
|
|
3964
5067
|
const { root: addRoot, name: addName } = payload;
|
|
3965
5068
|
try {
|
|
3966
5069
|
const resolved = path4.resolve(addRoot);
|
|
3967
|
-
const
|
|
3968
|
-
if (!
|
|
5070
|
+
const stat8 = await fsp5.stat(resolved).catch(() => null);
|
|
5071
|
+
if (!stat8?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
3969
5072
|
const manifest = await loadManifest(ctx.opts.globalConfigPath);
|
|
3970
5073
|
const existing = manifest.projects.find((p) => path4.resolve(p.root) === resolved);
|
|
3971
5074
|
if (existing) {
|
|
@@ -4010,8 +5113,8 @@ async function handleWorkingDirSet(ctx, ws, newPath) {
|
|
|
4010
5113
|
sendResult6(ctx, ws, false, `Path must stay inside the project root: ${wdRoot}`);
|
|
4011
5114
|
return;
|
|
4012
5115
|
}
|
|
4013
|
-
const
|
|
4014
|
-
if (!
|
|
5116
|
+
const stat8 = await fsp5.stat(resolved).catch(() => null);
|
|
5117
|
+
if (!stat8?.isDirectory()) {
|
|
4015
5118
|
sendResult6(ctx, ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
4016
5119
|
return;
|
|
4017
5120
|
}
|
|
@@ -4750,6 +5853,10 @@ async function runWebUI(opts) {
|
|
|
4750
5853
|
"contextStrategy",
|
|
4751
5854
|
"logLevel",
|
|
4752
5855
|
"auditLevel",
|
|
5856
|
+
"hqEnabled",
|
|
5857
|
+
"hqUrl",
|
|
5858
|
+
"hqToken",
|
|
5859
|
+
"hqRawContent",
|
|
4753
5860
|
// Telegram plugin notification settings (parity with the standalone server).
|
|
4754
5861
|
"tgConfigured",
|
|
4755
5862
|
"tgSessionEnd",
|
|
@@ -4793,6 +5900,11 @@ async function runWebUI(opts) {
|
|
|
4793
5900
|
meta["logLevel"] = cfg.log?.["level"] ?? "info";
|
|
4794
5901
|
meta["auditLevel"] = cfg.session?.["auditLevel"] ?? "standard";
|
|
4795
5902
|
meta["maxIterations"] = cfg.tools?.["maxIterations"] ?? 500;
|
|
5903
|
+
const hqCfg = cfg.hq ?? {};
|
|
5904
|
+
meta["hqEnabled"] = hqCfg["enabled"] === true;
|
|
5905
|
+
meta["hqUrl"] = typeof hqCfg["url"] === "string" ? hqCfg["url"] : "";
|
|
5906
|
+
meta["hqToken"] = typeof hqCfg["token"] === "string" ? hqCfg["token"] : "";
|
|
5907
|
+
meta["hqRawContent"] = hqCfg["rawContent"] === true;
|
|
4796
5908
|
const tgExt = cfg.extensions?.["telegram"];
|
|
4797
5909
|
meta["tgConfigured"] = typeof tgExt?.["botToken"] === "string" && tgExt["botToken"].length > 0;
|
|
4798
5910
|
meta["tgSessionEnd"] = tgExt?.["notifyOnSessionEnd"] === true;
|
|
@@ -4893,6 +6005,15 @@ async function runWebUI(opts) {
|
|
|
4893
6005
|
toolsCfg.maxIterations = payload["maxIterations"];
|
|
4894
6006
|
decrypted.tools = toolsCfg;
|
|
4895
6007
|
}
|
|
6008
|
+
const hqTouched = typeof payload["hqEnabled"] === "boolean" || typeof payload["hqUrl"] === "string" || typeof payload["hqToken"] === "string" || typeof payload["hqRawContent"] === "boolean";
|
|
6009
|
+
if (hqTouched) {
|
|
6010
|
+
const hqCfg = decrypted.hq ?? {};
|
|
6011
|
+
if (typeof payload["hqEnabled"] === "boolean") hqCfg.enabled = payload["hqEnabled"];
|
|
6012
|
+
if (typeof payload["hqUrl"] === "string") hqCfg.url = payload["hqUrl"];
|
|
6013
|
+
if (typeof payload["hqToken"] === "string") hqCfg.token = payload["hqToken"];
|
|
6014
|
+
if (typeof payload["hqRawContent"] === "boolean") hqCfg.rawContent = payload["hqRawContent"];
|
|
6015
|
+
decrypted.hq = hqCfg;
|
|
6016
|
+
}
|
|
4896
6017
|
const tgTouched = typeof payload["tgSessionEnd"] === "boolean" || typeof payload["tgDelegate"] === "boolean" || typeof payload["tgLongToolMs"] === "number";
|
|
4897
6018
|
if (tgTouched) {
|
|
4898
6019
|
const ext = decrypted.extensions ?? {};
|
|
@@ -4979,7 +6100,9 @@ async function runWebUI(opts) {
|
|
|
4979
6100
|
if (!opts.projectRoot) return null;
|
|
4980
6101
|
try {
|
|
4981
6102
|
const projectDir = resolveProjectDir(opts.projectRoot, wstackGlobalRoot());
|
|
4982
|
-
const
|
|
6103
|
+
const hqPublisher = createHqPublisherFromEnv({ clientKind: "webui", projectRoot: opts.projectRoot, projectName: path4.basename(opts.projectRoot), appConfig: opts.appConfig });
|
|
6104
|
+
hqPublisher?.connect();
|
|
6105
|
+
const mailbox = new GlobalMailbox(projectDir, opts.events, hqPublisher);
|
|
4983
6106
|
webuiClientId = `webui@${crypto3.randomUUID().slice(0, 8)}`;
|
|
4984
6107
|
const projectName = opts.projectRoot ? path4.basename(opts.projectRoot) : "unknown";
|
|
4985
6108
|
await mailbox.registerClient({
|
|
@@ -5053,6 +6176,113 @@ async function runWebUI(opts) {
|
|
|
5053
6176
|
type: "fleet.concurrency_update",
|
|
5054
6177
|
payload: { fleetConcurrency, fleetConcurrencyMax: FLEET_CONCURRENCY_MAX }
|
|
5055
6178
|
});
|
|
6179
|
+
const STREAM_COALESCE_MS = 16;
|
|
6180
|
+
const STREAM_COALESCE_MAX_CHARS = 8 * 1024;
|
|
6181
|
+
let textDeltaBuffer = "";
|
|
6182
|
+
let textDeltaTimer = null;
|
|
6183
|
+
let thinkingDeltaBuffer = "";
|
|
6184
|
+
let thinkingDeltaTimer = null;
|
|
6185
|
+
const toolProgressBuffers = /* @__PURE__ */ new Map();
|
|
6186
|
+
const flushTextDelta = () => {
|
|
6187
|
+
if (textDeltaTimer) {
|
|
6188
|
+
clearTimeout(textDeltaTimer);
|
|
6189
|
+
textDeltaTimer = null;
|
|
6190
|
+
}
|
|
6191
|
+
if (!textDeltaBuffer) return;
|
|
6192
|
+
const text = textDeltaBuffer;
|
|
6193
|
+
textDeltaBuffer = "";
|
|
6194
|
+
broadcast({
|
|
6195
|
+
type: "provider.text_delta",
|
|
6196
|
+
payload: { text, messageId: "current" }
|
|
6197
|
+
});
|
|
6198
|
+
};
|
|
6199
|
+
const flushThinkingDelta = () => {
|
|
6200
|
+
if (thinkingDeltaTimer) {
|
|
6201
|
+
clearTimeout(thinkingDeltaTimer);
|
|
6202
|
+
thinkingDeltaTimer = null;
|
|
6203
|
+
}
|
|
6204
|
+
if (!thinkingDeltaBuffer) return;
|
|
6205
|
+
const text = thinkingDeltaBuffer;
|
|
6206
|
+
thinkingDeltaBuffer = "";
|
|
6207
|
+
broadcast({
|
|
6208
|
+
type: "provider.thinking_delta",
|
|
6209
|
+
payload: { text }
|
|
6210
|
+
});
|
|
6211
|
+
};
|
|
6212
|
+
const queueTextDelta = (text) => {
|
|
6213
|
+
if (!text) return;
|
|
6214
|
+
textDeltaBuffer += text;
|
|
6215
|
+
if (textDeltaBuffer.length >= STREAM_COALESCE_MAX_CHARS) {
|
|
6216
|
+
flushTextDelta();
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
6219
|
+
if (!textDeltaTimer) {
|
|
6220
|
+
textDeltaTimer = setTimeout(flushTextDelta, STREAM_COALESCE_MS);
|
|
6221
|
+
textDeltaTimer.unref?.();
|
|
6222
|
+
}
|
|
6223
|
+
};
|
|
6224
|
+
const queueThinkingDelta = (text) => {
|
|
6225
|
+
if (!text) return;
|
|
6226
|
+
thinkingDeltaBuffer += text;
|
|
6227
|
+
if (thinkingDeltaBuffer.length >= STREAM_COALESCE_MAX_CHARS) {
|
|
6228
|
+
flushThinkingDelta();
|
|
6229
|
+
return;
|
|
6230
|
+
}
|
|
6231
|
+
if (!thinkingDeltaTimer) {
|
|
6232
|
+
thinkingDeltaTimer = setTimeout(flushThinkingDelta, STREAM_COALESCE_MS);
|
|
6233
|
+
thinkingDeltaTimer.unref?.();
|
|
6234
|
+
}
|
|
6235
|
+
};
|
|
6236
|
+
const flushToolProgress = (id) => {
|
|
6237
|
+
const buffered = toolProgressBuffers.get(id);
|
|
6238
|
+
if (!buffered) return;
|
|
6239
|
+
if (buffered.timer) clearTimeout(buffered.timer);
|
|
6240
|
+
toolProgressBuffers.delete(id);
|
|
6241
|
+
if (!buffered.text) return;
|
|
6242
|
+
broadcast({
|
|
6243
|
+
type: "tool.progress",
|
|
6244
|
+
payload: {
|
|
6245
|
+
name: buffered.name,
|
|
6246
|
+
id: buffered.id,
|
|
6247
|
+
event: { type: buffered.eventType, text: buffered.text }
|
|
6248
|
+
}
|
|
6249
|
+
});
|
|
6250
|
+
};
|
|
6251
|
+
const flushAllStreamBuffers = () => {
|
|
6252
|
+
flushTextDelta();
|
|
6253
|
+
flushThinkingDelta();
|
|
6254
|
+
for (const id of [...toolProgressBuffers.keys()]) flushToolProgress(id);
|
|
6255
|
+
};
|
|
6256
|
+
const queueToolProgress = (payload) => {
|
|
6257
|
+
const text = payload.event.text;
|
|
6258
|
+
if (!text) {
|
|
6259
|
+
flushToolProgress(payload.id);
|
|
6260
|
+
broadcast({ type: "tool.progress", payload });
|
|
6261
|
+
return;
|
|
6262
|
+
}
|
|
6263
|
+
const eventType = payload.event.type ?? "progress";
|
|
6264
|
+
const existing = toolProgressBuffers.get(payload.id);
|
|
6265
|
+
if (existing && existing.eventType !== eventType) flushToolProgress(payload.id);
|
|
6266
|
+
const buffered = toolProgressBuffers.get(payload.id) ?? {
|
|
6267
|
+
id: payload.id,
|
|
6268
|
+
name: payload.name,
|
|
6269
|
+
eventType,
|
|
6270
|
+
text: "",
|
|
6271
|
+
timer: null
|
|
6272
|
+
};
|
|
6273
|
+
buffered.name = payload.name;
|
|
6274
|
+
buffered.text += buffered.text ? `
|
|
6275
|
+
${text}` : text;
|
|
6276
|
+
toolProgressBuffers.set(payload.id, buffered);
|
|
6277
|
+
if (buffered.text.length >= STREAM_COALESCE_MAX_CHARS) {
|
|
6278
|
+
flushToolProgress(payload.id);
|
|
6279
|
+
return;
|
|
6280
|
+
}
|
|
6281
|
+
if (!buffered.timer) {
|
|
6282
|
+
buffered.timer = setTimeout(() => flushToolProgress(payload.id), STREAM_COALESCE_MS);
|
|
6283
|
+
buffered.timer.unref?.();
|
|
6284
|
+
}
|
|
6285
|
+
};
|
|
5056
6286
|
function setupEvents() {
|
|
5057
6287
|
for (const unsub of eventUnsubscribers) unsub();
|
|
5058
6288
|
eventUnsubscribers.length = 0;
|
|
@@ -5081,22 +6311,18 @@ async function runWebUI(opts) {
|
|
|
5081
6311
|
);
|
|
5082
6312
|
eventUnsubscribers.push(
|
|
5083
6313
|
opts.events.on("provider.text_delta", (e) => {
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
payload: { text: e.text, messageId: "current" }
|
|
5087
|
-
});
|
|
6314
|
+
flushThinkingDelta();
|
|
6315
|
+
queueTextDelta(e.text);
|
|
5088
6316
|
})
|
|
5089
6317
|
);
|
|
5090
6318
|
eventUnsubscribers.push(
|
|
5091
6319
|
opts.events.on("provider.thinking_delta", (e) => {
|
|
5092
|
-
|
|
5093
|
-
type: "provider.thinking_delta",
|
|
5094
|
-
payload: { text: e.text }
|
|
5095
|
-
});
|
|
6320
|
+
queueThinkingDelta(e.text);
|
|
5096
6321
|
})
|
|
5097
6322
|
);
|
|
5098
6323
|
eventUnsubscribers.push(
|
|
5099
6324
|
opts.events.on("tool.started", (e) => {
|
|
6325
|
+
flushAllStreamBuffers();
|
|
5100
6326
|
broadcast({
|
|
5101
6327
|
type: "tool.started",
|
|
5102
6328
|
payload: {
|
|
@@ -5110,18 +6336,16 @@ async function runWebUI(opts) {
|
|
|
5110
6336
|
);
|
|
5111
6337
|
eventUnsubscribers.push(
|
|
5112
6338
|
opts.events.on("tool.progress", (e) => {
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
id: e.id,
|
|
5118
|
-
event: e.event
|
|
5119
|
-
}
|
|
6339
|
+
queueToolProgress({
|
|
6340
|
+
name: e.name,
|
|
6341
|
+
id: e.id,
|
|
6342
|
+
event: e.event
|
|
5120
6343
|
});
|
|
5121
6344
|
})
|
|
5122
6345
|
);
|
|
5123
6346
|
eventUnsubscribers.push(
|
|
5124
6347
|
opts.events.on("tool.executed", (e) => {
|
|
6348
|
+
flushAllStreamBuffers();
|
|
5125
6349
|
broadcast({
|
|
5126
6350
|
type: "tool.executed",
|
|
5127
6351
|
payload: {
|
|
@@ -5178,6 +6402,7 @@ async function runWebUI(opts) {
|
|
|
5178
6402
|
);
|
|
5179
6403
|
eventUnsubscribers.push(
|
|
5180
6404
|
opts.events.on("provider.response", (e) => {
|
|
6405
|
+
flushAllStreamBuffers();
|
|
5181
6406
|
broadcast({
|
|
5182
6407
|
type: "provider.response",
|
|
5183
6408
|
payload: {
|
|
@@ -5392,6 +6617,19 @@ async function runWebUI(opts) {
|
|
|
5392
6617
|
log: (m) => console.log(m)
|
|
5393
6618
|
};
|
|
5394
6619
|
const wsCommon = { send, broadcast, log: (m) => console.log(m) };
|
|
6620
|
+
const mailboxCache = /* @__PURE__ */ new Map();
|
|
6621
|
+
const getWebuiMailbox = () => {
|
|
6622
|
+
const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
|
|
6623
|
+
const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
|
|
6624
|
+
if (!projectRoot || !globalRoot) return null;
|
|
6625
|
+
const mbDir = resolveProjectDir(projectRoot, globalRoot);
|
|
6626
|
+
let mailbox = mailboxCache.get(mbDir);
|
|
6627
|
+
if (!mailbox) {
|
|
6628
|
+
mailbox = new GlobalMailbox(mbDir, opts.events);
|
|
6629
|
+
mailboxCache.set(mbDir, mailbox);
|
|
6630
|
+
}
|
|
6631
|
+
return mailbox;
|
|
6632
|
+
};
|
|
5395
6633
|
const sessionsCtx = {
|
|
5396
6634
|
opts,
|
|
5397
6635
|
buildSessionStart: (overrides) => buildSessionStartPayload(overrides),
|
|
@@ -5407,7 +6645,7 @@ async function runWebUI(opts) {
|
|
|
5407
6645
|
broadcast,
|
|
5408
6646
|
log: (m) => console.log(m)
|
|
5409
6647
|
};
|
|
5410
|
-
return new Promise((
|
|
6648
|
+
return new Promise((resolve12) => {
|
|
5411
6649
|
wss.on("listening", () => {
|
|
5412
6650
|
console.log(`[WebUI] WebSocket server running on ws://${host}:${port}`);
|
|
5413
6651
|
setupEvents();
|
|
@@ -5525,8 +6763,8 @@ async function runWebUI(opts) {
|
|
|
5525
6763
|
clients.delete(ws);
|
|
5526
6764
|
abortControllers.delete(ws);
|
|
5527
6765
|
if (clients.size === 0 && pendingConfirms.size > 0) {
|
|
5528
|
-
for (const [id,
|
|
5529
|
-
|
|
6766
|
+
for (const [id, resolve13] of pendingConfirms) {
|
|
6767
|
+
resolve13("no");
|
|
5530
6768
|
pendingConfirms.delete(id);
|
|
5531
6769
|
}
|
|
5532
6770
|
}
|
|
@@ -5553,6 +6791,7 @@ async function runWebUI(opts) {
|
|
|
5553
6791
|
abortControllers.clear();
|
|
5554
6792
|
},
|
|
5555
6793
|
unsubscribeEvents: () => {
|
|
6794
|
+
flushAllStreamBuffers();
|
|
5556
6795
|
for (const unsub of eventUnsubscribers) unsub();
|
|
5557
6796
|
},
|
|
5558
6797
|
closeClients: () => {
|
|
@@ -5565,7 +6804,7 @@ async function runWebUI(opts) {
|
|
|
5565
6804
|
wss,
|
|
5566
6805
|
pid: process.pid,
|
|
5567
6806
|
registryBaseDir,
|
|
5568
|
-
onStopped:
|
|
6807
|
+
onStopped: resolve12
|
|
5569
6808
|
});
|
|
5570
6809
|
registerWebuiSignalHandlers(signalShutdown);
|
|
5571
6810
|
});
|
|
@@ -6005,6 +7244,16 @@ async function runWebUI(opts) {
|
|
|
6005
7244
|
case "collab.request_pause":
|
|
6006
7245
|
case "collab.resume":
|
|
6007
7246
|
break;
|
|
7247
|
+
// Integrated terminal — the CLI embedded server doesn't run a pty
|
|
7248
|
+
// transport (it already has its own terminal); silently acknowledge
|
|
7249
|
+
// and ignore so the browser client's terminal panel doesn't trip the
|
|
7250
|
+
// "Unhandled message type" warning. The standalone webui server wires
|
|
7251
|
+
// the real TerminalWebSocketHandler.
|
|
7252
|
+
case "terminal.create":
|
|
7253
|
+
case "terminal.input":
|
|
7254
|
+
case "terminal.resize":
|
|
7255
|
+
case "terminal.close":
|
|
7256
|
+
break;
|
|
6008
7257
|
case "projects.list": {
|
|
6009
7258
|
await handleProjectsList(projectsCtx, ws);
|
|
6010
7259
|
break;
|
|
@@ -6066,9 +7315,8 @@ async function runWebUI(opts) {
|
|
|
6066
7315
|
break;
|
|
6067
7316
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
6068
7317
|
case "mailbox.messages": {
|
|
6069
|
-
const
|
|
6070
|
-
|
|
6071
|
-
if (!projectRoot || !globalRoot) {
|
|
7318
|
+
const mb = getWebuiMailbox();
|
|
7319
|
+
if (!mb) {
|
|
6072
7320
|
send(ws, {
|
|
6073
7321
|
type: "mailbox.messages",
|
|
6074
7322
|
payload: { messages: [], error: "No project root available" }
|
|
@@ -6076,13 +7324,12 @@ async function runWebUI(opts) {
|
|
|
6076
7324
|
break;
|
|
6077
7325
|
}
|
|
6078
7326
|
try {
|
|
6079
|
-
const mbDir = resolveProjectDir(projectRoot, globalRoot);
|
|
6080
|
-
const mb = new GlobalMailbox(mbDir);
|
|
6081
7327
|
const payload = msg.payload;
|
|
6082
7328
|
const messages = await mb.query({
|
|
6083
7329
|
limit: payload?.limit ?? 30,
|
|
6084
7330
|
to: payload?.agentId,
|
|
6085
|
-
unreadBy: payload?.unreadOnly ? payload.agentId : void 0
|
|
7331
|
+
unreadBy: payload?.unreadOnly ? payload.agentId : void 0,
|
|
7332
|
+
incompleteOnly: payload?.incompleteOnly
|
|
6086
7333
|
});
|
|
6087
7334
|
send(ws, {
|
|
6088
7335
|
type: "mailbox.messages",
|
|
@@ -6115,9 +7362,8 @@ async function runWebUI(opts) {
|
|
|
6115
7362
|
break;
|
|
6116
7363
|
}
|
|
6117
7364
|
case "mailbox.agents": {
|
|
6118
|
-
const
|
|
6119
|
-
|
|
6120
|
-
if (!projectRoot || !globalRoot) {
|
|
7365
|
+
const mb = getWebuiMailbox();
|
|
7366
|
+
if (!mb) {
|
|
6121
7367
|
send(ws, {
|
|
6122
7368
|
type: "mailbox.agents",
|
|
6123
7369
|
payload: { agents: [], error: "No project root available" }
|
|
@@ -6125,8 +7371,6 @@ async function runWebUI(opts) {
|
|
|
6125
7371
|
break;
|
|
6126
7372
|
}
|
|
6127
7373
|
try {
|
|
6128
|
-
const mbDir = resolveProjectDir(projectRoot, globalRoot);
|
|
6129
|
-
const mb = new GlobalMailbox(mbDir);
|
|
6130
7374
|
const payload = msg.payload;
|
|
6131
7375
|
const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
|
|
6132
7376
|
send(ws, {
|
|
@@ -6158,15 +7402,12 @@ async function runWebUI(opts) {
|
|
|
6158
7402
|
break;
|
|
6159
7403
|
}
|
|
6160
7404
|
case "mailbox.clear": {
|
|
6161
|
-
const
|
|
6162
|
-
|
|
6163
|
-
if (!projectRoot || !globalRoot) {
|
|
7405
|
+
const mb = getWebuiMailbox();
|
|
7406
|
+
if (!mb) {
|
|
6164
7407
|
send(ws, { type: "mailbox.cleared", payload: { error: "No project root available" } });
|
|
6165
7408
|
break;
|
|
6166
7409
|
}
|
|
6167
7410
|
try {
|
|
6168
|
-
const mbDir = resolveProjectDir(projectRoot, globalRoot);
|
|
6169
|
-
const mb = new GlobalMailbox(mbDir);
|
|
6170
7411
|
await mb.clearAll();
|
|
6171
7412
|
send(ws, { type: "mailbox.cleared", payload: {} });
|
|
6172
7413
|
} catch (err) {
|
|
@@ -6178,15 +7419,12 @@ async function runWebUI(opts) {
|
|
|
6178
7419
|
break;
|
|
6179
7420
|
}
|
|
6180
7421
|
case "mailbox.purge": {
|
|
6181
|
-
const
|
|
6182
|
-
|
|
6183
|
-
if (!projectRoot || !globalRoot) {
|
|
7422
|
+
const mb = getWebuiMailbox();
|
|
7423
|
+
if (!mb) {
|
|
6184
7424
|
send(ws, { type: "mailbox.purged", payload: { error: "No project root available" } });
|
|
6185
7425
|
break;
|
|
6186
7426
|
}
|
|
6187
7427
|
try {
|
|
6188
|
-
const mbDir = resolveProjectDir(projectRoot, globalRoot);
|
|
6189
|
-
const mb = new GlobalMailbox(mbDir);
|
|
6190
7428
|
const payload = msg;
|
|
6191
7429
|
const result = await mb.purgeStale(payload.payload);
|
|
6192
7430
|
send(ws, { type: "mailbox.purged", payload: result });
|
|
@@ -6223,6 +7461,7 @@ async function runWebUI(opts) {
|
|
|
6223
7461
|
}
|
|
6224
7462
|
function shutdown() {
|
|
6225
7463
|
console.log("[WebUI] Shutting down...");
|
|
7464
|
+
flushAllStreamBuffers();
|
|
6226
7465
|
unregisterWebuiClient();
|
|
6227
7466
|
httpServer?.server.close();
|
|
6228
7467
|
opts.onExit?.();
|
|
@@ -6258,7 +7497,7 @@ init_provider_config_utils();
|
|
|
6258
7497
|
var WORKTREE_PHASE_CONCURRENCY = 4;
|
|
6259
7498
|
var MAX_CMD_OUTPUT = 2e5;
|
|
6260
7499
|
function gitText(args, cwd) {
|
|
6261
|
-
return new Promise((
|
|
7500
|
+
return new Promise((resolve12, reject) => {
|
|
6262
7501
|
let child;
|
|
6263
7502
|
try {
|
|
6264
7503
|
child = spawn("git", args, {
|
|
@@ -6278,8 +7517,8 @@ function gitText(args, cwd) {
|
|
|
6278
7517
|
};
|
|
6279
7518
|
child.stdout?.on("data", emit);
|
|
6280
7519
|
child.stderr?.on("data", emit);
|
|
6281
|
-
child.on("error", () =>
|
|
6282
|
-
child.on("close", (code) =>
|
|
7520
|
+
child.on("error", () => resolve12({ code: 1, out: chunks.join("") }));
|
|
7521
|
+
child.on("close", (code) => resolve12({ code: code ?? 1, out: chunks.join("").trim() }));
|
|
6283
7522
|
});
|
|
6284
7523
|
}
|
|
6285
7524
|
async function isGitRepo(cwd) {
|
|
@@ -6315,7 +7554,7 @@ function runCmd(cmd, args, cwd, shell = false) {
|
|
|
6315
7554
|
});
|
|
6316
7555
|
}
|
|
6317
7556
|
}
|
|
6318
|
-
return new Promise((
|
|
7557
|
+
return new Promise((resolve12, reject) => {
|
|
6319
7558
|
const chunks = [];
|
|
6320
7559
|
let child;
|
|
6321
7560
|
try {
|
|
@@ -6338,11 +7577,11 @@ function runCmd(cmd, args, cwd, shell = false) {
|
|
|
6338
7577
|
};
|
|
6339
7578
|
child.stdout?.on("data", append);
|
|
6340
7579
|
child.stderr?.on("data", append);
|
|
6341
|
-
child.on("error", (e) =>
|
|
7580
|
+
child.on("error", (e) => resolve12({ code: 1, out: `${chunks.join("")}${String(e)}` }));
|
|
6342
7581
|
child.on("close", (code) => {
|
|
6343
7582
|
let out = chunks.join("");
|
|
6344
7583
|
if (out.length > MAX_CMD_OUTPUT) out = out.slice(-MAX_CMD_OUTPUT);
|
|
6345
|
-
|
|
7584
|
+
resolve12({ code: code ?? 1, out: out.trim() });
|
|
6346
7585
|
});
|
|
6347
7586
|
});
|
|
6348
7587
|
}
|
|
@@ -6698,7 +7937,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
6698
7937
|
"skip-index",
|
|
6699
7938
|
"mouse",
|
|
6700
7939
|
"no-interactive",
|
|
6701
|
-
"token-saving-mode"
|
|
7940
|
+
"token-saving-mode",
|
|
7941
|
+
"hq"
|
|
6702
7942
|
]);
|
|
6703
7943
|
function parseArgs(argv) {
|
|
6704
7944
|
const flags = {};
|
|
@@ -6849,31 +8089,31 @@ var ReadlineInputReader = class {
|
|
|
6849
8089
|
async readLine(prompt) {
|
|
6850
8090
|
if (this.history.length === 0) await this.loadHistory();
|
|
6851
8091
|
while (this.pending) {
|
|
6852
|
-
await new Promise((
|
|
8092
|
+
await new Promise((resolve12) => setTimeout(resolve12, 50));
|
|
6853
8093
|
}
|
|
6854
8094
|
this.pending = true;
|
|
6855
8095
|
try {
|
|
6856
8096
|
if (this.rl) {
|
|
6857
8097
|
const old = this.rl;
|
|
6858
8098
|
this.rl = void 0;
|
|
6859
|
-
await new Promise((
|
|
8099
|
+
await new Promise((resolve12) => {
|
|
6860
8100
|
if (old.closed) {
|
|
6861
|
-
|
|
8101
|
+
resolve12();
|
|
6862
8102
|
} else {
|
|
6863
|
-
old.once("close",
|
|
8103
|
+
old.once("close", resolve12);
|
|
6864
8104
|
old.close();
|
|
6865
8105
|
}
|
|
6866
8106
|
});
|
|
6867
8107
|
}
|
|
6868
8108
|
const fresh = this.ensure();
|
|
6869
8109
|
this.installPromptGuard(fresh);
|
|
6870
|
-
return new Promise((
|
|
8110
|
+
return new Promise((resolve12) => {
|
|
6871
8111
|
let settled = false;
|
|
6872
8112
|
const settle = (line) => {
|
|
6873
8113
|
if (settled) return;
|
|
6874
8114
|
settled = true;
|
|
6875
8115
|
setOutputLineGuard(null);
|
|
6876
|
-
|
|
8116
|
+
resolve12(line);
|
|
6877
8117
|
};
|
|
6878
8118
|
fresh.question(prompt ?? "> ", (line) => {
|
|
6879
8119
|
if (line.trim()) {
|
|
@@ -6926,7 +8166,7 @@ var ReadlineInputReader = class {
|
|
|
6926
8166
|
async readKey(prompt, options) {
|
|
6927
8167
|
setOutputLineGuard(null);
|
|
6928
8168
|
writeOut(prompt);
|
|
6929
|
-
return new Promise((
|
|
8169
|
+
return new Promise((resolve12) => {
|
|
6930
8170
|
const stdin = process.stdin;
|
|
6931
8171
|
const wasRaw = stdin.isRaw;
|
|
6932
8172
|
const wasPaused = stdin.isPaused();
|
|
@@ -6937,7 +8177,7 @@ var ReadlineInputReader = class {
|
|
|
6937
8177
|
if (key === "") {
|
|
6938
8178
|
cleanup();
|
|
6939
8179
|
writeOut("\n");
|
|
6940
|
-
|
|
8180
|
+
resolve12("");
|
|
6941
8181
|
return;
|
|
6942
8182
|
}
|
|
6943
8183
|
const opt = options.find(
|
|
@@ -6947,12 +8187,12 @@ var ReadlineInputReader = class {
|
|
|
6947
8187
|
cleanup();
|
|
6948
8188
|
writeOut(`${opt.key}
|
|
6949
8189
|
`);
|
|
6950
|
-
|
|
8190
|
+
resolve12(opt.value);
|
|
6951
8191
|
}
|
|
6952
8192
|
};
|
|
6953
8193
|
const onClose = () => {
|
|
6954
8194
|
cleanup();
|
|
6955
|
-
|
|
8195
|
+
resolve12("");
|
|
6956
8196
|
};
|
|
6957
8197
|
const cleanup = () => {
|
|
6958
8198
|
stdin.off("data", onData);
|
|
@@ -6981,7 +8221,7 @@ var ReadlineInputReader = class {
|
|
|
6981
8221
|
this.rl?.close();
|
|
6982
8222
|
this.rl = void 0;
|
|
6983
8223
|
writeOut(prompt);
|
|
6984
|
-
return new Promise((
|
|
8224
|
+
return new Promise((resolve12) => {
|
|
6985
8225
|
let buf = "";
|
|
6986
8226
|
const wasRaw = stdin.isRaw;
|
|
6987
8227
|
setRawMode(stdin, true);
|
|
@@ -6999,7 +8239,7 @@ var ReadlineInputReader = class {
|
|
|
6999
8239
|
cleanup();
|
|
7000
8240
|
writeOut(` ${dim(`[${buf.length} chars]`)}
|
|
7001
8241
|
`);
|
|
7002
|
-
|
|
8242
|
+
resolve12(buf);
|
|
7003
8243
|
return;
|
|
7004
8244
|
}
|
|
7005
8245
|
if (ch === "") {
|
|
@@ -7194,8 +8434,8 @@ function printLaunchHints(renderer, flags, opts = {}) {
|
|
|
7194
8434
|
var defaultUidFn = () => os__default.userInfo().uid;
|
|
7195
8435
|
async function getFileUid(filePath) {
|
|
7196
8436
|
try {
|
|
7197
|
-
const
|
|
7198
|
-
return
|
|
8437
|
+
const stat8 = await fsp5.stat(filePath);
|
|
8438
|
+
return stat8.uid;
|
|
7199
8439
|
} catch {
|
|
7200
8440
|
return void 0;
|
|
7201
8441
|
}
|
|
@@ -7530,11 +8770,11 @@ async function buildPickableProviders(modelsRegistry, config) {
|
|
|
7530
8770
|
var theme = { primary: color.amber };
|
|
7531
8771
|
async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os__default.homedir()) {
|
|
7532
8772
|
try {
|
|
7533
|
-
const { atomicWrite:
|
|
7534
|
-
const
|
|
8773
|
+
const { atomicWrite: atomicWrite15 } = await import('@wrongstack/core');
|
|
8774
|
+
const fs36 = await import('fs/promises');
|
|
7535
8775
|
let existing = {};
|
|
7536
8776
|
try {
|
|
7537
|
-
const raw = await
|
|
8777
|
+
const raw = await fs36.readFile(configPath2, "utf8");
|
|
7538
8778
|
existing = JSON.parse(raw);
|
|
7539
8779
|
} catch {
|
|
7540
8780
|
}
|
|
@@ -7553,7 +8793,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
|
|
|
7553
8793
|
})
|
|
7554
8794
|
);
|
|
7555
8795
|
}
|
|
7556
|
-
await
|
|
8796
|
+
await atomicWrite15(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
7557
8797
|
try {
|
|
7558
8798
|
await appendHistory(
|
|
7559
8799
|
oldCfg,
|
|
@@ -9220,7 +10460,7 @@ The following characters are not allowed: ; & | < > ^ $ , ( ) { } [ ] ! # % ' "
|
|
|
9220
10460
|
}
|
|
9221
10461
|
}
|
|
9222
10462
|
function runCommand(cmd, cwd, timeout) {
|
|
9223
|
-
return new Promise((
|
|
10463
|
+
return new Promise((resolve12) => {
|
|
9224
10464
|
validateCommand(cmd);
|
|
9225
10465
|
const opts = {
|
|
9226
10466
|
cwd,
|
|
@@ -9233,7 +10473,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
9233
10473
|
shell: process.platform === "win32" ? true : false
|
|
9234
10474
|
};
|
|
9235
10475
|
execFile(cmd, [], opts, (error, stdout, stderr) => {
|
|
9236
|
-
|
|
10476
|
+
resolve12({
|
|
9237
10477
|
stdout,
|
|
9238
10478
|
stderr,
|
|
9239
10479
|
exitCode: typeof error?.code === "number" ? error.code : 0,
|
|
@@ -9679,6 +10919,7 @@ function formatDelay(ms) {
|
|
|
9679
10919
|
|
|
9680
10920
|
// src/settings-menu.ts
|
|
9681
10921
|
function resolvePersistPath(deps) {
|
|
10922
|
+
if (deps.forceGlobal) return deps.globalConfigPath;
|
|
9682
10923
|
const scope = deps.configStore.get().configScope;
|
|
9683
10924
|
if (scope === "project" && deps.inProjectConfigPath) {
|
|
9684
10925
|
return deps.inProjectConfigPath;
|
|
@@ -12770,7 +14011,7 @@ function parseMcpArgs(args) {
|
|
|
12770
14011
|
}
|
|
12771
14012
|
async function runMcpManagementCommand(parsed, deps) {
|
|
12772
14013
|
const { config, configPath: configPath2, mcpRegistry, allServerPresets } = deps;
|
|
12773
|
-
const diskConfig = await
|
|
14014
|
+
const diskConfig = await readJsonObjectFile(configPath2);
|
|
12774
14015
|
const configured = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : config.mcpServers ?? {};
|
|
12775
14016
|
switch (parsed.action) {
|
|
12776
14017
|
case "list":
|
|
@@ -12835,13 +14076,10 @@ async function runAdd(name, enable, configured, configPath2, mcpRegistry, all) {
|
|
|
12835
14076
|
}
|
|
12836
14077
|
const existing = configured[name];
|
|
12837
14078
|
const nextCfg = existing ? { ...preset, ...existing, enabled: enable } : { ...preset, enabled: enable };
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
};
|
|
12843
|
-
full.mcpServers = mcpServers;
|
|
12844
|
-
await writeConfig(configPath2, full);
|
|
14079
|
+
await updateJsonObjectFile(configPath2, (full) => {
|
|
14080
|
+
const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
|
|
14081
|
+
setJsonPath(full, ["mcpServers", name], { ...current[name], ...nextCfg });
|
|
14082
|
+
});
|
|
12845
14083
|
if (!enable) {
|
|
12846
14084
|
const verb = existing ? "Updated" : "Added (disabled \u2014 /mcp enable to start)";
|
|
12847
14085
|
return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
|
|
@@ -12875,13 +14113,11 @@ async function runRemove(name, configured, configPath2, mcpRegistry) {
|
|
|
12875
14113
|
})
|
|
12876
14114
|
);
|
|
12877
14115
|
}
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
full.mcpServers = mcpServers;
|
|
12884
|
-
await writeConfig(configPath2, full);
|
|
14116
|
+
await updateJsonObjectFile(configPath2, (full) => {
|
|
14117
|
+
const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : configured;
|
|
14118
|
+
setJsonPath(full, ["mcpServers"], { ...current });
|
|
14119
|
+
removeJsonPath(full, ["mcpServers", name]);
|
|
14120
|
+
});
|
|
12885
14121
|
return `${color.yellow("Removed")} "${name}" from config.`;
|
|
12886
14122
|
}
|
|
12887
14123
|
async function runEnable(name, configured, configPath2, mcpRegistry) {
|
|
@@ -12896,13 +14132,10 @@ async function runEnable(name, configured, configPath2, mcpRegistry) {
|
|
|
12896
14132
|
return `${color.green("Enabled")} "${name}" and started.`;
|
|
12897
14133
|
}
|
|
12898
14134
|
}
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
12902
|
-
};
|
|
12903
|
-
mcpServers[name] = { ...cfg, ...mcpServers[name] ?? {}, enabled: true };
|
|
12904
|
-
full.mcpServers = mcpServers;
|
|
12905
|
-
await writeConfig(configPath2, full);
|
|
14135
|
+
await updateJsonObjectFile(configPath2, (full) => {
|
|
14136
|
+
const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
|
|
14137
|
+
setJsonPath(full, ["mcpServers", name], { ...cfg, ...current[name], enabled: true });
|
|
14138
|
+
});
|
|
12906
14139
|
try {
|
|
12907
14140
|
await mcpRegistry.restart(name);
|
|
12908
14141
|
} catch {
|
|
@@ -12927,13 +14160,10 @@ async function runDisable(name, configured, configPath2, mcpRegistry) {
|
|
|
12927
14160
|
})
|
|
12928
14161
|
);
|
|
12929
14162
|
}
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
};
|
|
12934
|
-
mcpServers[name] = { ...cfg, ...mcpServers[name] ?? {}, enabled: false };
|
|
12935
|
-
full.mcpServers = mcpServers;
|
|
12936
|
-
await writeConfig(configPath2, full);
|
|
14163
|
+
await updateJsonObjectFile(configPath2, (full) => {
|
|
14164
|
+
const current = isMcpServerRecord(full.mcpServers) ? full.mcpServers : {};
|
|
14165
|
+
setJsonPath(full, ["mcpServers", name], { ...cfg, ...current[name], enabled: false });
|
|
14166
|
+
});
|
|
12937
14167
|
return `${color.yellow("Disabled")} "${name}" and stopped.`;
|
|
12938
14168
|
}
|
|
12939
14169
|
async function runRestart(name, mcpRegistry) {
|
|
@@ -12964,22 +14194,9 @@ function stateBadge(state) {
|
|
|
12964
14194
|
return color.dim(state);
|
|
12965
14195
|
}
|
|
12966
14196
|
}
|
|
12967
|
-
async function readConfig(path39) {
|
|
12968
|
-
try {
|
|
12969
|
-
return JSON.parse(await fsp5.readFile(path39, "utf8"));
|
|
12970
|
-
} catch {
|
|
12971
|
-
return {};
|
|
12972
|
-
}
|
|
12973
|
-
}
|
|
12974
14197
|
function isMcpServerRecord(value) {
|
|
12975
14198
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
12976
14199
|
}
|
|
12977
|
-
async function writeConfig(path39, cfg) {
|
|
12978
|
-
const raw = JSON.stringify(cfg, null, 2);
|
|
12979
|
-
const tmp = path39 + ".tmp";
|
|
12980
|
-
await fsp5.writeFile(tmp, raw, "utf8");
|
|
12981
|
-
await fsp5.rename(tmp, path39);
|
|
12982
|
-
}
|
|
12983
14200
|
|
|
12984
14201
|
// src/slash-commands/mcp.ts
|
|
12985
14202
|
function buildMcpSlashCommand(opts) {
|
|
@@ -14335,12 +15552,12 @@ function buildLoadCommand(opts) {
|
|
|
14335
15552
|
const badge = s.outcome === "completed" ? color.green("\u2713") : s.outcome === "aborted" ? color.yellow("\u26A0") : s.outcome === "error" ? color.red("\u2717") : color.dim("?");
|
|
14336
15553
|
parts2.push(badge);
|
|
14337
15554
|
}
|
|
14338
|
-
const
|
|
15555
|
+
const stat8 = parts2.join(" ");
|
|
14339
15556
|
const date = color.dim(s.startedAt.slice(0, 16).replace("T", " "));
|
|
14340
15557
|
const isCurrent = s.id === currentId;
|
|
14341
15558
|
const marker = isCurrent ? color.cyan(" (current)") : "";
|
|
14342
15559
|
return ` ${color.bold(s.id)}${marker}
|
|
14343
|
-
${date} ${
|
|
15560
|
+
${date} ${stat8}
|
|
14344
15561
|
${color.dim(s.title)}`;
|
|
14345
15562
|
});
|
|
14346
15563
|
const msg = [
|
|
@@ -15099,15 +16316,26 @@ function formatSuggestions(suggestions) {
|
|
|
15099
16316
|
|
|
15100
16317
|
// src/slash-commands/coordinator.ts
|
|
15101
16318
|
function buildCoordinatorCommand(opts) {
|
|
16319
|
+
const getStart = () => opts.onCoordinatorStart ?? opts.coordinatorController?.onCoordinatorStart;
|
|
16320
|
+
const getStop = () => opts.onCoordinatorStop ?? opts.coordinatorController?.onCoordinatorStop;
|
|
16321
|
+
const getTasks = () => opts.onCoordinatorTasks ?? opts.coordinatorController?.onCoordinatorTasks;
|
|
16322
|
+
const getClaim = () => opts.onCoordinatorClaim ?? opts.coordinatorController?.onCoordinatorClaim;
|
|
16323
|
+
const getComplete = () => opts.onCoordinatorComplete ?? opts.coordinatorController?.onCoordinatorComplete;
|
|
16324
|
+
const getFail = () => opts.onCoordinatorFail ?? opts.coordinatorController?.onCoordinatorFail;
|
|
16325
|
+
const getStatus = () => opts.onCoordinatorStatus ?? opts.coordinatorController?.onCoordinatorStatus;
|
|
15102
16326
|
return {
|
|
15103
16327
|
name: "coordinator",
|
|
15104
16328
|
category: "Agent",
|
|
15105
16329
|
description: "Start, stop, or inspect the AutonomousCoordinator \u2014 the fleet brain that auctions tasks and consults Brain for risky decisions.",
|
|
15106
16330
|
help: [
|
|
15107
16331
|
"Usage:",
|
|
15108
|
-
" /coordinator start <goal>
|
|
15109
|
-
" /coordinator stop
|
|
15110
|
-
" /coordinator status
|
|
16332
|
+
" /coordinator start <goal> Start the coordinator with a goal",
|
|
16333
|
+
" /coordinator stop Stop the running coordinator",
|
|
16334
|
+
" /coordinator status Show current coordinator status",
|
|
16335
|
+
" /coordinator tasks List available tasks the current terminal can claim",
|
|
16336
|
+
" /coordinator claim <id> Claim a task and inject its description as the next prompt",
|
|
16337
|
+
" /coordinator done <id> [note] Mark a claimed task as completed",
|
|
16338
|
+
" /coordinator fail <id> <reason> Mark a claimed task as failed",
|
|
15111
16339
|
"",
|
|
15112
16340
|
"The AutonomousCoordinator runs alongside the agent loop and:",
|
|
15113
16341
|
" \u2022 Maintains a shared knowledge graph of facts and decisions",
|
|
@@ -15115,7 +16343,10 @@ function buildCoordinatorCommand(opts) {
|
|
|
15115
16343
|
" \u2022 Consults the Brain for risky decisions",
|
|
15116
16344
|
" \u2022 Uses ConsensusProtocol to vote on multi-agent changes",
|
|
15117
16345
|
"",
|
|
15118
|
-
"It is separate from /autonomy eternal \u2014 both can run concurrently."
|
|
16346
|
+
"It is separate from /autonomy eternal \u2014 both can run concurrently.",
|
|
16347
|
+
"",
|
|
16348
|
+
"Terminals are eligible workers: an open terminal can discover and claim",
|
|
16349
|
+
"pending tasks without spawning a subagent."
|
|
15119
16350
|
].join("\n"),
|
|
15120
16351
|
async run(args) {
|
|
15121
16352
|
const trimmed = args.trim();
|
|
@@ -15126,19 +16357,108 @@ function buildCoordinatorCommand(opts) {
|
|
|
15126
16357
|
if (!goal) {
|
|
15127
16358
|
return { message: "Usage: /coordinator start <goal>\nA goal is required to start the coordinator." };
|
|
15128
16359
|
}
|
|
15129
|
-
|
|
16360
|
+
getStart()?.(goal);
|
|
15130
16361
|
return {
|
|
15131
16362
|
message: `AutonomousCoordinator started with goal: "${goal}"
|
|
15132
16363
|
Use /coordinator status to monitor progress.`
|
|
15133
16364
|
};
|
|
15134
16365
|
}
|
|
15135
16366
|
if (verb === "stop") {
|
|
15136
|
-
|
|
16367
|
+
getStop()?.();
|
|
15137
16368
|
return { message: "AutonomousCoordinator stop signal sent." };
|
|
15138
16369
|
}
|
|
16370
|
+
if (verb === "tasks") {
|
|
16371
|
+
const tasksFn = getTasks();
|
|
16372
|
+
if (!tasksFn) {
|
|
16373
|
+
return { message: "Coordinator task listing is not wired in this surface." };
|
|
16374
|
+
}
|
|
16375
|
+
const tasks = await tasksFn();
|
|
16376
|
+
if (!tasks) {
|
|
16377
|
+
return { message: "No coordinator is active. Start one with /coordinator start <goal>." };
|
|
16378
|
+
}
|
|
16379
|
+
if (tasks.length === 0) {
|
|
16380
|
+
return { message: "No pending coordinator tasks. Use /coordinator status for overall progress." };
|
|
16381
|
+
}
|
|
16382
|
+
const lines = ["Pending coordinator tasks available to claim:"];
|
|
16383
|
+
for (const task of tasks) {
|
|
16384
|
+
lines.push(` ${task.id} [${task.priority}] ${task.title}${task.tags.length > 0 ? ` \xB7 ${task.tags.join(", ")}` : ""}`);
|
|
16385
|
+
}
|
|
16386
|
+
lines.push("", "Claim one with /coordinator claim <id> (id prefix allowed).");
|
|
16387
|
+
return { message: lines.join("\n") };
|
|
16388
|
+
}
|
|
16389
|
+
if (verb === "claim") {
|
|
16390
|
+
const target = rest.join(" ").trim();
|
|
16391
|
+
if (!target) {
|
|
16392
|
+
return { message: "Usage: /coordinator claim <taskId>" };
|
|
16393
|
+
}
|
|
16394
|
+
const claimFn = getClaim();
|
|
16395
|
+
if (!claimFn) {
|
|
16396
|
+
return { message: "Coordinator task claiming is not wired in this surface." };
|
|
16397
|
+
}
|
|
16398
|
+
const tasks = await getTasks()?.();
|
|
16399
|
+
const matched = tasks?.find((task) => task.id === target || task.id.startsWith(target));
|
|
16400
|
+
if (!matched) {
|
|
16401
|
+
return { message: `No pending coordinator task matched "${target}".` };
|
|
16402
|
+
}
|
|
16403
|
+
const result = await claimFn(matched.id);
|
|
16404
|
+
if (typeof result === "string") return { message: result };
|
|
16405
|
+
if (result === null) return { message: "No coordinator is active." };
|
|
16406
|
+
const description = result.description ?? matched.title;
|
|
16407
|
+
return {
|
|
16408
|
+
message: `Claimed task ${matched.id.slice(0, 8)}: ${matched.title}`,
|
|
16409
|
+
runText: `Work on this coordinator task (id: ${matched.id}):
|
|
16410
|
+
|
|
16411
|
+
${description}`
|
|
16412
|
+
};
|
|
16413
|
+
}
|
|
16414
|
+
if (verb === "done" || verb === "complete") {
|
|
16415
|
+
const taskId = rest[0]?.trim() ?? "";
|
|
16416
|
+
if (!taskId) {
|
|
16417
|
+
return { message: "Usage: /coordinator done <taskId> [note]" };
|
|
16418
|
+
}
|
|
16419
|
+
const completeFn = getComplete();
|
|
16420
|
+
if (!completeFn) {
|
|
16421
|
+
return { message: "Coordinator task completion is not wired in this surface." };
|
|
16422
|
+
}
|
|
16423
|
+
const note = rest.slice(1).join(" ").trim();
|
|
16424
|
+
const err = await completeFn(taskId, note || void 0);
|
|
16425
|
+
if (err) return { message: err };
|
|
16426
|
+
return { message: `Task ${taskId.slice(0, 8)} marked completed.` };
|
|
16427
|
+
}
|
|
16428
|
+
if (verb === "fail") {
|
|
16429
|
+
const taskId = rest[0]?.trim() ?? "";
|
|
16430
|
+
if (!taskId) {
|
|
16431
|
+
return { message: "Usage: /coordinator fail <taskId> <reason>" };
|
|
16432
|
+
}
|
|
16433
|
+
const failFn = getFail();
|
|
16434
|
+
if (!failFn) {
|
|
16435
|
+
return { message: "Coordinator task failure reporting is not wired in this surface." };
|
|
16436
|
+
}
|
|
16437
|
+
const reason = rest.slice(1).join(" ").trim() || "Terminal worker reported failure";
|
|
16438
|
+
const err = await failFn(taskId, reason);
|
|
16439
|
+
if (err) return { message: err };
|
|
16440
|
+
return { message: `Task ${taskId.slice(0, 8)} marked failed: ${reason}` };
|
|
16441
|
+
}
|
|
15139
16442
|
if (verb === "status") {
|
|
15140
|
-
const
|
|
15141
|
-
|
|
16443
|
+
const statusFn = getStatus();
|
|
16444
|
+
if (statusFn) {
|
|
16445
|
+
const stats = await statusFn();
|
|
16446
|
+
if (!stats) {
|
|
16447
|
+
return { message: "No coordinator is active. Start one with /coordinator start <goal>." };
|
|
16448
|
+
}
|
|
16449
|
+
const lines = [
|
|
16450
|
+
"Coordinator Status:",
|
|
16451
|
+
` Goals: ${stats.goals.total} total \xB7 ${stats.goals.done} done \xB7 ${stats.goals.pending} pending \xB7 ${stats.goals.failed} failed`,
|
|
16452
|
+
` DAG: ${stats.dag.running} running \xB7 ${stats.dag.ready} ready \xB7 ${stats.dag.done} done \xB7 ${stats.dag.failed} failed`,
|
|
16453
|
+
` Auction: ${stats.auction.pending} pending \xB7 ${stats.auction.inProgress} in progress`
|
|
16454
|
+
];
|
|
16455
|
+
if (stats.goals.pending > 0 || stats.auction.pending > 0) {
|
|
16456
|
+
lines.push("", "Use /coordinator tasks to list claimable work.");
|
|
16457
|
+
}
|
|
16458
|
+
return { message: lines.join("\n") };
|
|
16459
|
+
}
|
|
16460
|
+
const canStart = getStart() != null;
|
|
16461
|
+
const canStop = getStop() != null;
|
|
15142
16462
|
return {
|
|
15143
16463
|
message: [
|
|
15144
16464
|
`Coordinator wired: start=${canStart ? "yes" : "no"}, stop=${canStop ? "yes" : "no"}`,
|
|
@@ -15149,9 +16469,13 @@ Use /coordinator status to monitor progress.`
|
|
|
15149
16469
|
return {
|
|
15150
16470
|
message: [
|
|
15151
16471
|
"Usage:",
|
|
15152
|
-
" /coordinator start <goal>
|
|
15153
|
-
" /coordinator stop
|
|
15154
|
-
" /coordinator status
|
|
16472
|
+
" /coordinator start <goal> Start with a goal",
|
|
16473
|
+
" /coordinator stop Stop the coordinator",
|
|
16474
|
+
" /coordinator status Show status",
|
|
16475
|
+
" /coordinator tasks List tasks this terminal can claim",
|
|
16476
|
+
" /coordinator claim <id> Claim a task and inject its description",
|
|
16477
|
+
" /coordinator done <id> [note] Mark a claimed task as completed",
|
|
16478
|
+
" /coordinator fail <id> <reason> Mark a claimed task as failed",
|
|
15155
16479
|
"",
|
|
15156
16480
|
"The coordinator is a fleet brain that:",
|
|
15157
16481
|
" \u2022 Auctions tasks to subagents via TaskAuctioneer",
|
|
@@ -15350,8 +16674,8 @@ async function addProjectCommand(opts, ctx, targetPath, displayName) {
|
|
|
15350
16674
|
} catch {
|
|
15351
16675
|
return { message: color.red(`Directory not found: ${resolved}`) };
|
|
15352
16676
|
}
|
|
15353
|
-
const
|
|
15354
|
-
if (!
|
|
16677
|
+
const stat8 = await fsp5.stat(resolved);
|
|
16678
|
+
if (!stat8.isDirectory()) {
|
|
15355
16679
|
return { message: color.red(`Not a directory: ${resolved}`) };
|
|
15356
16680
|
}
|
|
15357
16681
|
const manifest = await loadManifest(opts.paths?.globalConfig);
|
|
@@ -15420,8 +16744,8 @@ async function switchProjectCommand(opts, ctx, target, displayName) {
|
|
|
15420
16744
|
} catch {
|
|
15421
16745
|
return { message: color.red(`Directory not found: ${resolved}`) };
|
|
15422
16746
|
}
|
|
15423
|
-
const
|
|
15424
|
-
if (!
|
|
16747
|
+
const stat8 = await fsp5.stat(resolved);
|
|
16748
|
+
if (!stat8.isDirectory()) {
|
|
15425
16749
|
return { message: color.red(`Not a directory: ${resolved}`) };
|
|
15426
16750
|
}
|
|
15427
16751
|
let cliPath;
|
|
@@ -15677,7 +17001,7 @@ async function handlePrevSessions(opts, _ctx) {
|
|
|
15677
17001
|
return { message: lines.join("\n") };
|
|
15678
17002
|
}
|
|
15679
17003
|
async function runGit(args, cwd) {
|
|
15680
|
-
return new Promise((
|
|
17004
|
+
return new Promise((resolve12) => {
|
|
15681
17005
|
const child = spawn("git", args, {
|
|
15682
17006
|
cwd,
|
|
15683
17007
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -15688,8 +17012,8 @@ async function runGit(args, cwd) {
|
|
|
15688
17012
|
child.stdout?.on("data", (d) => {
|
|
15689
17013
|
stdout += d;
|
|
15690
17014
|
});
|
|
15691
|
-
child.on("error", () =>
|
|
15692
|
-
child.on("close", (code) =>
|
|
17015
|
+
child.on("error", () => resolve12({ stdout, code: 1 }));
|
|
17016
|
+
child.on("close", (code) => resolve12({ stdout, code: code ?? 0 }));
|
|
15693
17017
|
});
|
|
15694
17018
|
}
|
|
15695
17019
|
async function getChangedFiles(cwd) {
|
|
@@ -15788,6 +17112,14 @@ function buildSettingsCommand(opts) {
|
|
|
15788
17112
|
" /settings token-saving off|minimal|light|medium|aggressive Token-saving mode",
|
|
15789
17113
|
" /settings max-concurrent <n> Max concurrent subagents (0 = unlimited)",
|
|
15790
17114
|
" /settings title-animation on|off Terminal title animation",
|
|
17115
|
+
" /settings reasoning auto|on|off Reasoning mode (auto = provider default)",
|
|
17116
|
+
" /settings reasoning-effort none|minimal|low|medium|high|xhigh|max Reasoning effort",
|
|
17117
|
+
" /settings reasoning-preserve on|off Preserve thinking across turns",
|
|
17118
|
+
" /settings cache-ttl 5m|1h Prompt cache TTL (Anthropic)",
|
|
17119
|
+
" /settings hq on|off Enable/disable HQ client publishing",
|
|
17120
|
+
" /settings hq-url <url> HQ URL for remote clients (http://host:3499)",
|
|
17121
|
+
" /settings hq-token <token> HQ client token for remote clients",
|
|
17122
|
+
" /settings hq-raw on|off Send raw content previews to HQ",
|
|
15791
17123
|
" /settings defaults Show built-in default values",
|
|
15792
17124
|
"",
|
|
15793
17125
|
"Settings are persisted to ~/.wrongstack/config.json."
|
|
@@ -15814,6 +17146,15 @@ function buildSettingsCommand(opts) {
|
|
|
15814
17146
|
const features = opts.configStore.get().features;
|
|
15815
17147
|
const tokenSavingTier = features?.tokenSavingMode ?? "off";
|
|
15816
17148
|
const maxConcurrent = opts.configStore.get().maxConcurrent ?? 0;
|
|
17149
|
+
const modelRuntime = opts.configStore.get().modelRuntime;
|
|
17150
|
+
const reasoningMode = modelRuntime?.reasoning?.mode ?? "auto";
|
|
17151
|
+
const reasoningEffort = modelRuntime?.reasoning?.effort ?? "(unset)";
|
|
17152
|
+
const reasoningPreserve = modelRuntime?.reasoning?.preserve === true;
|
|
17153
|
+
const cacheTtl = modelRuntime?.cache?.ttl ?? "default";
|
|
17154
|
+
const hq = opts.configStore.get().hq;
|
|
17155
|
+
const hqEnabled = hq?.enabled === true;
|
|
17156
|
+
const hqUrl = hq?.url ?? "(auto/local)";
|
|
17157
|
+
const hqToken = hq?.token ? `${hq.token.slice(0, 6)}\u2026${hq.token.slice(-4)} (${hq.token.length} chars)` : "(auto/local)";
|
|
15817
17158
|
return [
|
|
15818
17159
|
`${color.bold("WrongStack")} ${color.dim("\u2014 Settings")}`,
|
|
15819
17160
|
"",
|
|
@@ -15833,6 +17174,14 @@ function buildSettingsCommand(opts) {
|
|
|
15833
17174
|
` context auto-compact: ${contextAutoCompact ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings context-auto-compact on|off")}`,
|
|
15834
17175
|
` token-saving: ${color.cyan(tokenSavingTier)} ${color.dim("change: /settings token-saving off|minimal|light|medium|aggressive")}`,
|
|
15835
17176
|
` max-concurrent: ${color.cyan(maxConcurrent === 0 ? "unlimited" : String(maxConcurrent))} ${color.dim("change: /settings max-concurrent <n>")}`,
|
|
17177
|
+
` reasoning mode: ${color.cyan(reasoningMode)} ${color.dim("change: /settings reasoning auto|on|off")}`,
|
|
17178
|
+
` reasoning effort: ${color.cyan(reasoningEffort)} ${color.dim("change: /settings reasoning-effort <level>")}`,
|
|
17179
|
+
` reasoning preserve: ${reasoningPreserve ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings reasoning-preserve on|off")}`,
|
|
17180
|
+
` cache TTL: ${color.cyan(cacheTtl)} ${color.dim("change: /settings cache-ttl 5m|1h")}`,
|
|
17181
|
+
` HQ publishing: ${hqEnabled ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings hq on|off")}`,
|
|
17182
|
+
` HQ URL: ${color.cyan(hqUrl)} ${color.dim("change: /settings hq-url <url>")}`,
|
|
17183
|
+
` HQ token: ${color.cyan(hqToken)} ${color.dim("change: /settings hq-token <token>")}`,
|
|
17184
|
+
` HQ raw content: ${hq?.rawContent === true ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings hq-raw on|off")}`,
|
|
15836
17185
|
"",
|
|
15837
17186
|
color.dim(" Persisted to ~/.wrongstack/config.json \xB7 /settings help for more")
|
|
15838
17187
|
].join("\n");
|
|
@@ -15876,6 +17225,60 @@ function buildSettingsCommand(opts) {
|
|
|
15876
17225
|
vault: noOpVault
|
|
15877
17226
|
};
|
|
15878
17227
|
try {
|
|
17228
|
+
if (sub === "hq") {
|
|
17229
|
+
const raw = (rest[0] ?? "").toLowerCase();
|
|
17230
|
+
if (!["on", "off"].includes(raw)) {
|
|
17231
|
+
return { message: `${color.amber("Usage:")} /settings hq on|off` };
|
|
17232
|
+
}
|
|
17233
|
+
const on = raw === "on";
|
|
17234
|
+
await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
|
|
17235
|
+
const hq = cfg.hq ?? {};
|
|
17236
|
+
hq.enabled = on;
|
|
17237
|
+
cfg.hq = hq;
|
|
17238
|
+
});
|
|
17239
|
+
return { message: `${color.green("\u2713")} HQ publishing \u2192 ${on ? color.cyan("on") : color.dim("off")}` };
|
|
17240
|
+
}
|
|
17241
|
+
if (sub === "hq-url") {
|
|
17242
|
+
const raw = rest.join(" ").trim();
|
|
17243
|
+
if (!raw) return { message: `${color.amber("Usage:")} /settings hq-url <http://host:3499>` };
|
|
17244
|
+
try {
|
|
17245
|
+
const url = new URL(raw);
|
|
17246
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("bad protocol");
|
|
17247
|
+
} catch {
|
|
17248
|
+
return { message: `${color.red("Invalid URL")}: ${raw}` };
|
|
17249
|
+
}
|
|
17250
|
+
await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
|
|
17251
|
+
const hq = cfg.hq ?? {};
|
|
17252
|
+
hq.url = raw;
|
|
17253
|
+
hq.enabled = true;
|
|
17254
|
+
cfg.hq = hq;
|
|
17255
|
+
});
|
|
17256
|
+
return { message: `${color.green("\u2713")} HQ URL \u2192 ${color.cyan(raw)}` };
|
|
17257
|
+
}
|
|
17258
|
+
if (sub === "hq-token") {
|
|
17259
|
+
const token = rest.join(" ").trim();
|
|
17260
|
+
if (!token) return { message: `${color.amber("Usage:")} /settings hq-token <client-token>` };
|
|
17261
|
+
await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
|
|
17262
|
+
const hq = cfg.hq ?? {};
|
|
17263
|
+
hq.token = token;
|
|
17264
|
+
hq.enabled = true;
|
|
17265
|
+
cfg.hq = hq;
|
|
17266
|
+
});
|
|
17267
|
+
return { message: `${color.green("\u2713")} HQ token saved ${color.dim("(global config)")}` };
|
|
17268
|
+
}
|
|
17269
|
+
if (sub === "hq-raw") {
|
|
17270
|
+
const raw = (rest[0] ?? "").toLowerCase();
|
|
17271
|
+
if (!["on", "off"].includes(raw)) {
|
|
17272
|
+
return { message: `${color.amber("Usage:")} /settings hq-raw on|off` };
|
|
17273
|
+
}
|
|
17274
|
+
const on = raw === "on";
|
|
17275
|
+
await persistConfigSetting({ ...persistDeps, forceGlobal: true }, (cfg) => {
|
|
17276
|
+
const hq = cfg.hq ?? {};
|
|
17277
|
+
hq.rawContent = on;
|
|
17278
|
+
cfg.hq = hq;
|
|
17279
|
+
});
|
|
17280
|
+
return { message: `${color.green("\u2713")} HQ raw content \u2192 ${on ? color.cyan("on") : color.dim("off")}` };
|
|
17281
|
+
}
|
|
15879
17282
|
if (sub === "delay") {
|
|
15880
17283
|
const raw = rest[0];
|
|
15881
17284
|
if (raw === void 0) {
|
|
@@ -16155,8 +17558,65 @@ function buildSettingsCommand(opts) {
|
|
|
16155
17558
|
message: `${color.green("\u2713")} title animation \u2192 ${on ? color.cyan("on") : color.dim("off")} ${color.dim("terminal title animation")}`
|
|
16156
17559
|
};
|
|
16157
17560
|
}
|
|
17561
|
+
if (sub === "reasoning") {
|
|
17562
|
+
const raw = (rest[0] ?? "").toLowerCase();
|
|
17563
|
+
const modes = ["auto", "on", "off"];
|
|
17564
|
+
if (!modes.includes(raw)) {
|
|
17565
|
+
return { message: `${color.amber("Usage:")} /settings reasoning auto|on|off` };
|
|
17566
|
+
}
|
|
17567
|
+
await persistConfigSetting(persistDeps, (cfg) => {
|
|
17568
|
+
const mr = cfg.modelRuntime;
|
|
17569
|
+
const reasoning = mr?.reasoning ?? {};
|
|
17570
|
+
reasoning.mode = raw;
|
|
17571
|
+
cfg.modelRuntime = { ...mr, reasoning };
|
|
17572
|
+
});
|
|
17573
|
+
return { message: `${color.green("\u2713")} reasoning mode \u2192 ${color.bold(raw)}` };
|
|
17574
|
+
}
|
|
17575
|
+
if (sub === "reasoning-effort") {
|
|
17576
|
+
const raw = (rest[0] ?? "").toLowerCase();
|
|
17577
|
+
const efforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
17578
|
+
if (!efforts.includes(raw)) {
|
|
17579
|
+
return {
|
|
17580
|
+
message: `${color.amber("Usage:")} /settings reasoning-effort none|minimal|low|medium|high|xhigh|max`
|
|
17581
|
+
};
|
|
17582
|
+
}
|
|
17583
|
+
await persistConfigSetting(persistDeps, (cfg) => {
|
|
17584
|
+
const mr = cfg.modelRuntime;
|
|
17585
|
+
const reasoning = mr?.reasoning ?? {};
|
|
17586
|
+
reasoning.effort = raw;
|
|
17587
|
+
cfg.modelRuntime = { ...mr, reasoning };
|
|
17588
|
+
});
|
|
17589
|
+
return { message: `${color.green("\u2713")} reasoning effort \u2192 ${color.bold(raw)}` };
|
|
17590
|
+
}
|
|
17591
|
+
if (sub === "reasoning-preserve") {
|
|
17592
|
+
const raw = (rest[0] ?? "").toLowerCase();
|
|
17593
|
+
if (!["on", "off"].includes(raw)) {
|
|
17594
|
+
return { message: `${color.amber("Usage:")} /settings reasoning-preserve on|off` };
|
|
17595
|
+
}
|
|
17596
|
+
const on = raw === "on";
|
|
17597
|
+
await persistConfigSetting(persistDeps, (cfg) => {
|
|
17598
|
+
const mr = cfg.modelRuntime;
|
|
17599
|
+
const reasoning = mr?.reasoning ?? {};
|
|
17600
|
+
reasoning.preserve = on;
|
|
17601
|
+
cfg.modelRuntime = { ...mr, reasoning };
|
|
17602
|
+
});
|
|
17603
|
+
return {
|
|
17604
|
+
message: `${color.green("\u2713")} reasoning preserve \u2192 ${on ? color.cyan("on") : color.dim("off")}`
|
|
17605
|
+
};
|
|
17606
|
+
}
|
|
17607
|
+
if (sub === "cache-ttl") {
|
|
17608
|
+
const raw = (rest[0] ?? "").toLowerCase();
|
|
17609
|
+
if (!["5m", "1h"].includes(raw)) {
|
|
17610
|
+
return { message: `${color.amber("Usage:")} /settings cache-ttl 5m|1h` };
|
|
17611
|
+
}
|
|
17612
|
+
await persistConfigSetting(persistDeps, (cfg) => {
|
|
17613
|
+
const mr = cfg.modelRuntime;
|
|
17614
|
+
cfg.modelRuntime = { ...mr, cache: { ttl: raw } };
|
|
17615
|
+
});
|
|
17616
|
+
return { message: `${color.green("\u2713")} cache TTL \u2192 ${color.bold(raw)}` };
|
|
17617
|
+
}
|
|
16158
17618
|
return {
|
|
16159
|
-
message: `${color.red("Unknown setting")} "${sub}". ${unknownSubcommand(sub, ["delay", "mode", "hints", "debug-stream", "config-scope", "fs-access", "refine", "refine-delay", "refine-language", "semver-part", "breaker", "breaker-timeout", "context-mode", "context-strategy", "context-auto-compact", "token-saving", "max-concurrent", "title-animation", "defaults"], "settings")}`
|
|
17619
|
+
message: `${color.red("Unknown setting")} "${sub}". ${unknownSubcommand(sub, ["delay", "mode", "hints", "debug-stream", "config-scope", "fs-access", "refine", "refine-delay", "refine-language", "semver-part", "breaker", "breaker-timeout", "context-mode", "context-strategy", "context-auto-compact", "token-saving", "max-concurrent", "title-animation", "reasoning", "reasoning-effort", "reasoning-preserve", "cache-ttl", "defaults"], "settings")}`
|
|
16160
17620
|
};
|
|
16161
17621
|
} catch (err) {
|
|
16162
17622
|
return {
|
|
@@ -17392,8 +18852,8 @@ function buildWorkingDirCommand(_opts) {
|
|
|
17392
18852
|
};
|
|
17393
18853
|
}
|
|
17394
18854
|
try {
|
|
17395
|
-
const
|
|
17396
|
-
if (!
|
|
18855
|
+
const stat8 = await fsp5.stat(resolved);
|
|
18856
|
+
if (!stat8.isDirectory()) {
|
|
17397
18857
|
return { message: color.red(`Not a directory: ${resolved}`) };
|
|
17398
18858
|
}
|
|
17399
18859
|
} catch {
|
|
@@ -17695,7 +19155,7 @@ async function runProjectCheck(opts) {
|
|
|
17695
19155
|
if (answer2 === "y" || answer2 === "yes") {
|
|
17696
19156
|
try {
|
|
17697
19157
|
const { spawn: spawn9 } = await import('child_process');
|
|
17698
|
-
await new Promise((
|
|
19158
|
+
await new Promise((resolve12, reject) => {
|
|
17699
19159
|
const child = spawn9("git", ["init"], {
|
|
17700
19160
|
cwd,
|
|
17701
19161
|
signal: AbortSignal.timeout(1e4),
|
|
@@ -17704,7 +19164,7 @@ async function runProjectCheck(opts) {
|
|
|
17704
19164
|
child.on("error", reject);
|
|
17705
19165
|
child.on(
|
|
17706
19166
|
"close",
|
|
17707
|
-
(code) => code === 0 ?
|
|
19167
|
+
(code) => code === 0 ? resolve12() : reject(new Error(`git init failed with ${code}`))
|
|
17708
19168
|
);
|
|
17709
19169
|
});
|
|
17710
19170
|
renderer.write(` ${color.green("\u2713")} Git repository initialized
|
|
@@ -18596,14 +20056,14 @@ var auditCmd = async (args, deps) => {
|
|
|
18596
20056
|
return verify.ok ? 0 : 1;
|
|
18597
20057
|
};
|
|
18598
20058
|
async function listAudits(log, dir, deps) {
|
|
18599
|
-
const
|
|
20059
|
+
const fs36 = await import('fs/promises');
|
|
18600
20060
|
const path39 = await import('path');
|
|
18601
20061
|
const out = [];
|
|
18602
20062
|
let foundRoot = true;
|
|
18603
20063
|
const scan = async (scanDir, prefix, depth) => {
|
|
18604
20064
|
let entries;
|
|
18605
20065
|
try {
|
|
18606
|
-
entries = await
|
|
20066
|
+
entries = await fs36.readdir(scanDir, { withFileTypes: true });
|
|
18607
20067
|
} catch {
|
|
18608
20068
|
if (depth === 0) foundRoot = false;
|
|
18609
20069
|
return;
|
|
@@ -18769,7 +20229,7 @@ ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
|
|
|
18769
20229
|
renderer.write(` ${color.bold("c")} Add a custom provider
|
|
18770
20230
|
`);
|
|
18771
20231
|
renderer.write(
|
|
18772
|
-
` ${color.bold("s")}
|
|
20232
|
+
` ${color.bold("s")} Login with OAuth ${color.dim("(ChatGPT / Claude / Copilot)")}
|
|
18773
20233
|
`
|
|
18774
20234
|
);
|
|
18775
20235
|
if (ids.length > 0) {
|
|
@@ -18865,341 +20325,78 @@ async function runAuthDirect(deps, opts) {
|
|
|
18865
20325
|
|
|
18866
20326
|
// src/auth-menu/add-provider.ts
|
|
18867
20327
|
init_provider_config_utils();
|
|
18868
|
-
|
|
18869
|
-
|
|
20328
|
+
|
|
20329
|
+
// src/auth-menu/anthropic-oauth.ts
|
|
20330
|
+
init_provider_config_utils();
|
|
20331
|
+
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
20332
|
+
var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
20333
|
+
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
20334
|
+
var REDIRECT_PORT = 53692;
|
|
20335
|
+
var REDIRECT_HOST = "127.0.0.1";
|
|
20336
|
+
var REDIRECT_PATH = "/callback";
|
|
20337
|
+
var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
|
|
20338
|
+
var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
20339
|
+
var CLAUDE_PROVIDER_ID = "anthropic-oauth";
|
|
20340
|
+
var CLAUDE_BASE_URL = "https://api.anthropic.com";
|
|
20341
|
+
function base64url(buf) {
|
|
20342
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
20343
|
+
}
|
|
20344
|
+
function generatePkce() {
|
|
20345
|
+
const verifier = base64url(randomBytes(32));
|
|
20346
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
20347
|
+
return { verifier, challenge };
|
|
20348
|
+
}
|
|
20349
|
+
function buildAuthorizeUrl(challenge, verifier) {
|
|
20350
|
+
const params = new URLSearchParams({
|
|
20351
|
+
code: "true",
|
|
20352
|
+
client_id: CLIENT_ID,
|
|
20353
|
+
response_type: "code",
|
|
20354
|
+
redirect_uri: REDIRECT_URI,
|
|
20355
|
+
scope: SCOPES,
|
|
20356
|
+
code_challenge: challenge,
|
|
20357
|
+
code_challenge_method: "S256",
|
|
20358
|
+
state: verifier
|
|
20359
|
+
});
|
|
20360
|
+
return `${AUTHORIZE_URL}?${params.toString()}`;
|
|
20361
|
+
}
|
|
20362
|
+
function parseAuthorizationInput(input) {
|
|
20363
|
+
const value = input.trim();
|
|
20364
|
+
if (!value) return {};
|
|
18870
20365
|
try {
|
|
18871
|
-
|
|
20366
|
+
const url = new URL(value);
|
|
20367
|
+
return {
|
|
20368
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
20369
|
+
state: url.searchParams.get("state") ?? void 0
|
|
20370
|
+
};
|
|
18872
20371
|
} catch {
|
|
18873
|
-
deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.\n");
|
|
18874
|
-
}
|
|
18875
|
-
if (catalog.length === 0) {
|
|
18876
|
-
return addManualEntry(deps);
|
|
18877
|
-
}
|
|
18878
|
-
const saved = new Set(Object.keys(await loadProviders(deps)));
|
|
18879
|
-
deps.renderer.write(
|
|
18880
|
-
color.dim(
|
|
18881
|
-
` Catalog: ${catalog.length} providers. Filter to narrow, "s" for unsaved-only, or enter to show all.
|
|
18882
|
-
`
|
|
18883
|
-
)
|
|
18884
|
-
);
|
|
18885
|
-
const filterRaw = (await deps.reader.readLine(
|
|
18886
|
-
` ${color.amber("?")} Filter ${color.dim('(substring / "s" / q to quit)')}: `
|
|
18887
|
-
)).trim();
|
|
18888
|
-
if (filterRaw === "q") return false;
|
|
18889
|
-
const filterLc = filterRaw.toLowerCase();
|
|
18890
|
-
const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
|
|
18891
|
-
function matches(p) {
|
|
18892
|
-
if (showUnsavedOnly) return !saved.has(p.id);
|
|
18893
|
-
if (!filterLc) return true;
|
|
18894
|
-
return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
|
|
18895
|
-
}
|
|
18896
|
-
const byFamily = /* @__PURE__ */ new Map();
|
|
18897
|
-
let filteredCount = 0;
|
|
18898
|
-
for (const p of catalog) {
|
|
18899
|
-
if (!matches(p)) continue;
|
|
18900
|
-
filteredCount++;
|
|
18901
|
-
const list = byFamily.get(p.family) ?? [];
|
|
18902
|
-
list.push(p);
|
|
18903
|
-
byFamily.set(p.family, list);
|
|
18904
|
-
}
|
|
18905
|
-
if (filteredCount === 0) {
|
|
18906
|
-
deps.renderer.writeError(
|
|
18907
|
-
`No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\`.`
|
|
18908
|
-
);
|
|
18909
|
-
return false;
|
|
18910
20372
|
}
|
|
18911
|
-
if (
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
`)
|
|
18915
|
-
);
|
|
20373
|
+
if (value.includes("#")) {
|
|
20374
|
+
const [code, state] = value.split("#", 2);
|
|
20375
|
+
return { code, state };
|
|
18916
20376
|
}
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
18921
|
-
|
|
18922
|
-
|
|
18923
|
-
if (!list || list.length === 0) continue;
|
|
18924
|
-
deps.renderer.write(` ${color.bold(fam)}
|
|
18925
|
-
`);
|
|
18926
|
-
for (const p of list) {
|
|
18927
|
-
const savedMark = saved.has(p.id) ? color.cyan("\u25C9") : color.dim("\u25CB");
|
|
18928
|
-
const env = p.envVars[0] ? color.dim(`[${p.envVars[0]}]`) : "";
|
|
18929
|
-
deps.renderer.write(
|
|
18930
|
-
` ${color.dim(`${idx}.`.padStart(4))} ${savedMark} ${p.id.padEnd(22)} ${color.dim(p.name)} ${env}
|
|
18931
|
-
`
|
|
18932
|
-
);
|
|
18933
|
-
ordered.push(p);
|
|
18934
|
-
idx++;
|
|
18935
|
-
}
|
|
20377
|
+
if (value.includes("code=")) {
|
|
20378
|
+
const params = new URLSearchParams(value);
|
|
20379
|
+
return {
|
|
20380
|
+
code: params.get("code") ?? void 0,
|
|
20381
|
+
state: params.get("state") ?? void 0
|
|
20382
|
+
};
|
|
18936
20383
|
}
|
|
18937
|
-
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
18942
|
-
${
|
|
18943
|
-
)).trim();
|
|
18944
|
-
if (!answer || answer === "q") return false;
|
|
18945
|
-
let chosen;
|
|
18946
|
-
const num = Number.parseInt(answer, 10);
|
|
18947
|
-
if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
|
|
18948
|
-
chosen = ordered[num - 1];
|
|
18949
|
-
} else {
|
|
18950
|
-
chosen = ordered.find((p) => p.id.toLowerCase() === answer.toLowerCase()) ?? catalog.find((p) => p.id.toLowerCase() === answer.toLowerCase());
|
|
20384
|
+
return { code: value };
|
|
20385
|
+
}
|
|
20386
|
+
async function readTokens(res, op) {
|
|
20387
|
+
if (!res.ok) {
|
|
20388
|
+
const text = await res.text().catch(() => "");
|
|
20389
|
+
throw new Error(`Claude token ${op} failed (${res.status}): ${text || res.statusText}`);
|
|
18951
20390
|
}
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
20391
|
+
const json = await res.json();
|
|
20392
|
+
if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
20393
|
+
throw new Error(`Claude token ${op} response missing fields`);
|
|
18955
20394
|
}
|
|
18956
|
-
return
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
Defaults from models.dev \u2014 press Enter to keep, or type overrides.
|
|
18962
|
-
`)
|
|
18963
|
-
);
|
|
18964
|
-
const famRaw = (await deps.reader.readLine(
|
|
18965
|
-
` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `
|
|
18966
|
-
)).trim();
|
|
18967
|
-
if (famRaw === "q") return false;
|
|
18968
|
-
let family = chosen.family;
|
|
18969
|
-
if (famRaw) {
|
|
18970
|
-
const validated = validateFamily(famRaw);
|
|
18971
|
-
if (!validated) {
|
|
18972
|
-
deps.renderer.writeError(
|
|
18973
|
-
`Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
|
|
18974
|
-
);
|
|
18975
|
-
return false;
|
|
18976
|
-
}
|
|
18977
|
-
family = validated;
|
|
18978
|
-
}
|
|
18979
|
-
const baseRaw = (await deps.reader.readLine(
|
|
18980
|
-
` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
|
|
18981
|
-
)).trim();
|
|
18982
|
-
if (baseRaw === "q") return false;
|
|
18983
|
-
const baseUrl = baseRaw || chosen.apiBase;
|
|
18984
|
-
const providersNow = await loadProviders(deps);
|
|
18985
|
-
let suggestedAlias = chosen.id;
|
|
18986
|
-
if (family !== chosen.family) {
|
|
18987
|
-
let candidate = `${chosen.id}-${family}`;
|
|
18988
|
-
let n = 2;
|
|
18989
|
-
while (providersNow[candidate]) {
|
|
18990
|
-
candidate = `${chosen.id}-${family}-${n}`;
|
|
18991
|
-
n++;
|
|
18992
|
-
}
|
|
18993
|
-
suggestedAlias = candidate;
|
|
18994
|
-
}
|
|
18995
|
-
const aliasRaw = (await deps.reader.readLine(
|
|
18996
|
-
` ${color.amber("?")} Save as alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used with --provider <alias>)")}: `
|
|
18997
|
-
)).trim();
|
|
18998
|
-
const alias = aliasRaw || suggestedAlias;
|
|
18999
|
-
const existing = providersNow[alias];
|
|
19000
|
-
if (existing) {
|
|
19001
|
-
const sameFamily = (existing.family ?? chosen.family) === family;
|
|
19002
|
-
const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
|
|
19003
|
-
if (!sameFamily || !sameBase) {
|
|
19004
|
-
deps.renderer.writeError(
|
|
19005
|
-
`Alias "${alias}" already exists with different family/baseUrl.
|
|
19006
|
-
Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
|
|
19007
|
-
New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
|
|
19008
|
-
Pick a different alias to keep them separate.`
|
|
19009
|
-
);
|
|
19010
|
-
return false;
|
|
19011
|
-
}
|
|
19012
|
-
}
|
|
19013
|
-
return addKeyForProvider(alias, deps, {
|
|
19014
|
-
type: chosen.id,
|
|
19015
|
-
family,
|
|
19016
|
-
baseUrl,
|
|
19017
|
-
envVars: chosen.envVars
|
|
19018
|
-
});
|
|
19019
|
-
}
|
|
19020
|
-
async function addCustomProvider(deps) {
|
|
19021
|
-
deps.renderer.write(
|
|
19022
|
-
`
|
|
19023
|
-
${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the catalog.")}
|
|
19024
|
-
`
|
|
19025
|
-
);
|
|
19026
|
-
const type = (await deps.reader.readLine(
|
|
19027
|
-
` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
|
|
19028
|
-
)).trim();
|
|
19029
|
-
if (!type || type === "q") return false;
|
|
19030
|
-
const existing = (await loadProviders(deps))[type];
|
|
19031
|
-
if (existing) {
|
|
19032
|
-
deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
|
|
19033
|
-
return false;
|
|
19034
|
-
}
|
|
19035
|
-
const familyRaw = (await deps.reader.readLine(
|
|
19036
|
-
` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
|
|
19037
|
-
)).trim();
|
|
19038
|
-
if (familyRaw === "q") return false;
|
|
19039
|
-
const family = validateFamily(familyRaw);
|
|
19040
|
-
if (!family) {
|
|
19041
|
-
deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
|
|
19042
|
-
return false;
|
|
19043
|
-
}
|
|
19044
|
-
const baseUrl = (await deps.reader.readLine(
|
|
19045
|
-
` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, optional)")}: `
|
|
19046
|
-
)).trim();
|
|
19047
|
-
const modelsRaw = (await deps.reader.readLine(
|
|
19048
|
-
` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
|
|
19049
|
-
)).trim();
|
|
19050
|
-
const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
19051
|
-
const envVarsRaw = (await deps.reader.readLine(
|
|
19052
|
-
` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional)")}: `
|
|
19053
|
-
)).trim();
|
|
19054
|
-
const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
19055
|
-
return addKeyForProvider(type, deps, {
|
|
19056
|
-
type,
|
|
19057
|
-
family,
|
|
19058
|
-
...baseUrl ? { baseUrl } : {},
|
|
19059
|
-
...models ? { models } : {},
|
|
19060
|
-
...envVars ? { envVars } : {}
|
|
19061
|
-
});
|
|
19062
|
-
}
|
|
19063
|
-
async function addManualEntry(deps) {
|
|
19064
|
-
const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
|
|
19065
|
-
if (!pid || pid === "q") return false;
|
|
19066
|
-
const famRaw = (await deps.reader.readLine(
|
|
19067
|
-
` ${color.amber("?")} Family ${color.dim("(anthropic/openai/openai-compatible/google)")}: `
|
|
19068
|
-
)).trim();
|
|
19069
|
-
const family = validateFamily(famRaw);
|
|
19070
|
-
if (!family) {
|
|
19071
|
-
deps.renderer.writeError(`Invalid family: "${famRaw}"`);
|
|
19072
|
-
return false;
|
|
19073
|
-
}
|
|
19074
|
-
const baseUrl = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
|
|
19075
|
-
return addKeyForProvider(pid, deps, {
|
|
19076
|
-
type: pid,
|
|
19077
|
-
family,
|
|
19078
|
-
...baseUrl ? { baseUrl } : {}
|
|
19079
|
-
});
|
|
19080
|
-
}
|
|
19081
|
-
async function addKeyForProvider(providerId, deps, template) {
|
|
19082
|
-
const providers = await loadProviders(deps);
|
|
19083
|
-
const existing = providers[providerId];
|
|
19084
|
-
const existingKeys = existing ? normalizeKeys(existing) : [];
|
|
19085
|
-
const usedLabels = new Set(existingKeys.map((k) => k.label));
|
|
19086
|
-
const label = await promptForLabel(deps, usedLabels);
|
|
19087
|
-
if (!label) return false;
|
|
19088
|
-
const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
|
|
19089
|
-
if (!apiKey) return false;
|
|
19090
|
-
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
19091
|
-
const existingProv = all[providerId] ?? {
|
|
19092
|
-
type: providerId,
|
|
19093
|
-
...template
|
|
19094
|
-
};
|
|
19095
|
-
if (!existingProv.type) existingProv.type = providerId;
|
|
19096
|
-
if (!existingProv.family && template.family) {
|
|
19097
|
-
existingProv.family = template.family;
|
|
19098
|
-
}
|
|
19099
|
-
if (!existingProv.baseUrl && template.baseUrl) {
|
|
19100
|
-
existingProv.baseUrl = template.baseUrl;
|
|
19101
|
-
}
|
|
19102
|
-
if (!existingProv.envVars && template.envVars) {
|
|
19103
|
-
existingProv.envVars = template.envVars;
|
|
19104
|
-
}
|
|
19105
|
-
const list = normalizeKeys(existingProv);
|
|
19106
|
-
list.push({ label, apiKey, createdAt: nowIso() });
|
|
19107
|
-
writeKeysBack(existingProv, list);
|
|
19108
|
-
if (!existingProv.activeKey) existingProv.activeKey = label;
|
|
19109
|
-
all[providerId] = existingProv;
|
|
19110
|
-
});
|
|
19111
|
-
deps.renderer.write(
|
|
19112
|
-
` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
|
|
19113
|
-
`
|
|
19114
|
-
);
|
|
19115
|
-
deps.renderer.write(color.dim(` Launch: wstack --provider ${providerId} "<task>"
|
|
19116
|
-
`));
|
|
19117
|
-
return true;
|
|
19118
|
-
}
|
|
19119
|
-
async function promptForLabel(deps, usedLabels) {
|
|
19120
|
-
const defaultLabel = suggestLabel(usedLabels);
|
|
19121
|
-
const labelRaw = (await deps.reader.readLine(
|
|
19122
|
-
` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
|
|
19123
|
-
)).trim();
|
|
19124
|
-
const label = labelRaw || defaultLabel;
|
|
19125
|
-
if (usedLabels.has(label)) {
|
|
19126
|
-
deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
|
|
19127
|
-
return null;
|
|
19128
|
-
}
|
|
19129
|
-
return label;
|
|
19130
|
-
}
|
|
19131
|
-
|
|
19132
|
-
// src/auth-menu/anthropic-oauth.ts
|
|
19133
|
-
init_provider_config_utils();
|
|
19134
|
-
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
19135
|
-
var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
19136
|
-
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
19137
|
-
var REDIRECT_PORT = 53692;
|
|
19138
|
-
var REDIRECT_HOST = "127.0.0.1";
|
|
19139
|
-
var REDIRECT_PATH = "/callback";
|
|
19140
|
-
var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
|
|
19141
|
-
var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
19142
|
-
var CLAUDE_PROVIDER_ID = "anthropic-oauth";
|
|
19143
|
-
var CLAUDE_BASE_URL = "https://api.anthropic.com";
|
|
19144
|
-
function base64url(buf) {
|
|
19145
|
-
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
19146
|
-
}
|
|
19147
|
-
function generatePkce() {
|
|
19148
|
-
const verifier = base64url(randomBytes(32));
|
|
19149
|
-
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
19150
|
-
return { verifier, challenge };
|
|
19151
|
-
}
|
|
19152
|
-
function buildAuthorizeUrl(challenge, verifier) {
|
|
19153
|
-
const params = new URLSearchParams({
|
|
19154
|
-
code: "true",
|
|
19155
|
-
client_id: CLIENT_ID,
|
|
19156
|
-
response_type: "code",
|
|
19157
|
-
redirect_uri: REDIRECT_URI,
|
|
19158
|
-
scope: SCOPES,
|
|
19159
|
-
code_challenge: challenge,
|
|
19160
|
-
code_challenge_method: "S256",
|
|
19161
|
-
state: verifier
|
|
19162
|
-
});
|
|
19163
|
-
return `${AUTHORIZE_URL}?${params.toString()}`;
|
|
19164
|
-
}
|
|
19165
|
-
function parseAuthorizationInput(input) {
|
|
19166
|
-
const value = input.trim();
|
|
19167
|
-
if (!value) return {};
|
|
19168
|
-
try {
|
|
19169
|
-
const url = new URL(value);
|
|
19170
|
-
return {
|
|
19171
|
-
code: url.searchParams.get("code") ?? void 0,
|
|
19172
|
-
state: url.searchParams.get("state") ?? void 0
|
|
19173
|
-
};
|
|
19174
|
-
} catch {
|
|
19175
|
-
}
|
|
19176
|
-
if (value.includes("#")) {
|
|
19177
|
-
const [code, state] = value.split("#", 2);
|
|
19178
|
-
return { code, state };
|
|
19179
|
-
}
|
|
19180
|
-
if (value.includes("code=")) {
|
|
19181
|
-
const params = new URLSearchParams(value);
|
|
19182
|
-
return {
|
|
19183
|
-
code: params.get("code") ?? void 0,
|
|
19184
|
-
state: params.get("state") ?? void 0
|
|
19185
|
-
};
|
|
19186
|
-
}
|
|
19187
|
-
return { code: value };
|
|
19188
|
-
}
|
|
19189
|
-
async function readTokens(res, op) {
|
|
19190
|
-
if (!res.ok) {
|
|
19191
|
-
const text = await res.text().catch(() => "");
|
|
19192
|
-
throw new Error(`Claude token ${op} failed (${res.status}): ${text || res.statusText}`);
|
|
19193
|
-
}
|
|
19194
|
-
const json = await res.json();
|
|
19195
|
-
if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
19196
|
-
throw new Error(`Claude token ${op} response missing fields`);
|
|
19197
|
-
}
|
|
19198
|
-
return {
|
|
19199
|
-
access: json.access_token,
|
|
19200
|
-
refresh: json.refresh_token,
|
|
19201
|
-
expires: Date.now() + json.expires_in * 1e3
|
|
19202
|
-
};
|
|
20395
|
+
return {
|
|
20396
|
+
access: json.access_token,
|
|
20397
|
+
refresh: json.refresh_token,
|
|
20398
|
+
expires: Date.now() + json.expires_in * 1e3
|
|
20399
|
+
};
|
|
19203
20400
|
}
|
|
19204
20401
|
async function exchangeAuthorizationCode(code, state, verifier, signal) {
|
|
19205
20402
|
const res = await fetch(TOKEN_URL, {
|
|
@@ -19243,12 +20440,12 @@ function callbackHtml(ok, message) {
|
|
|
19243
20440
|
function startLoopbackServer(expectedState) {
|
|
19244
20441
|
let resolveCode = () => {
|
|
19245
20442
|
};
|
|
19246
|
-
const codePromise = new Promise((
|
|
20443
|
+
const codePromise = new Promise((resolve12) => {
|
|
19247
20444
|
let settled = false;
|
|
19248
20445
|
resolveCode = (v) => {
|
|
19249
20446
|
if (settled) return;
|
|
19250
20447
|
settled = true;
|
|
19251
|
-
|
|
20448
|
+
resolve12(v);
|
|
19252
20449
|
};
|
|
19253
20450
|
});
|
|
19254
20451
|
const server = createServer((req2, res) => {
|
|
@@ -19291,10 +20488,10 @@ function startLoopbackServer(expectedState) {
|
|
|
19291
20488
|
res.end(callbackHtml(true, "You can close this window and return to the terminal."));
|
|
19292
20489
|
resolveCode({ code, state });
|
|
19293
20490
|
});
|
|
19294
|
-
return new Promise((
|
|
20491
|
+
return new Promise((resolve12) => {
|
|
19295
20492
|
server.on("error", () => {
|
|
19296
20493
|
resolveCode(null);
|
|
19297
|
-
|
|
20494
|
+
resolve12({
|
|
19298
20495
|
bound: false,
|
|
19299
20496
|
waitForCode: () => Promise.resolve(null),
|
|
19300
20497
|
close: () => {
|
|
@@ -19306,7 +20503,7 @@ function startLoopbackServer(expectedState) {
|
|
|
19306
20503
|
});
|
|
19307
20504
|
});
|
|
19308
20505
|
server.listen(REDIRECT_PORT, REDIRECT_HOST, () => {
|
|
19309
|
-
|
|
20506
|
+
resolve12({
|
|
19310
20507
|
bound: true,
|
|
19311
20508
|
waitForCode: () => codePromise,
|
|
19312
20509
|
close: () => {
|
|
@@ -19465,9 +20662,9 @@ function openBrowser2(url) {
|
|
|
19465
20662
|
}
|
|
19466
20663
|
}
|
|
19467
20664
|
function sleep(ms, signal) {
|
|
19468
|
-
return new Promise((
|
|
20665
|
+
return new Promise((resolve12, reject) => {
|
|
19469
20666
|
if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
|
|
19470
|
-
const t = setTimeout(
|
|
20667
|
+
const t = setTimeout(resolve12, ms);
|
|
19471
20668
|
signal.addEventListener(
|
|
19472
20669
|
"abort",
|
|
19473
20670
|
() => {
|
|
@@ -19745,12 +20942,12 @@ function callbackHtml2(ok, message) {
|
|
|
19745
20942
|
function startLoopbackServer2(state) {
|
|
19746
20943
|
let resolveCode = () => {
|
|
19747
20944
|
};
|
|
19748
|
-
const codePromise = new Promise((
|
|
20945
|
+
const codePromise = new Promise((resolve12) => {
|
|
19749
20946
|
let settled = false;
|
|
19750
20947
|
resolveCode = (v) => {
|
|
19751
20948
|
if (settled) return;
|
|
19752
20949
|
settled = true;
|
|
19753
|
-
|
|
20950
|
+
resolve12(v);
|
|
19754
20951
|
};
|
|
19755
20952
|
});
|
|
19756
20953
|
const server = createServer((req2, res) => {
|
|
@@ -19792,10 +20989,10 @@ function startLoopbackServer2(state) {
|
|
|
19792
20989
|
res.end(callbackHtml2(true, "You can close this window and return to the terminal."));
|
|
19793
20990
|
resolveCode(code);
|
|
19794
20991
|
});
|
|
19795
|
-
return new Promise((
|
|
20992
|
+
return new Promise((resolve12) => {
|
|
19796
20993
|
server.on("error", () => {
|
|
19797
20994
|
resolveCode(null);
|
|
19798
|
-
|
|
20995
|
+
resolve12({
|
|
19799
20996
|
bound: false,
|
|
19800
20997
|
waitForCode: () => Promise.resolve(null),
|
|
19801
20998
|
close: () => {
|
|
@@ -19807,7 +21004,7 @@ function startLoopbackServer2(state) {
|
|
|
19807
21004
|
});
|
|
19808
21005
|
});
|
|
19809
21006
|
server.listen(REDIRECT_PORT2, REDIRECT_HOST2, () => {
|
|
19810
|
-
|
|
21007
|
+
resolve12({
|
|
19811
21008
|
bound: true,
|
|
19812
21009
|
waitForCode: () => codePromise,
|
|
19813
21010
|
close: () => {
|
|
@@ -19967,6 +21164,316 @@ async function saveCodexTokens(deps, providerId, tokens, accountId) {
|
|
|
19967
21164
|
}
|
|
19968
21165
|
}
|
|
19969
21166
|
|
|
21167
|
+
// src/auth-menu/oauth-menu.ts
|
|
21168
|
+
function renderOAuthLoginOptions(deps, indent = " ") {
|
|
21169
|
+
deps.renderer.write(
|
|
21170
|
+
`${indent}${color.bold("OAuth login options")} ${color.dim("(subscription sign-in)")}
|
|
21171
|
+
${indent}${color.bold("chatgpt")} ChatGPT Plus/Pro ${color.dim("(\u2192 openai-codex)")}
|
|
21172
|
+
${indent}${color.bold("claude")} Claude Pro/Max ${color.dim("(\u2192 anthropic-oauth)")}
|
|
21173
|
+
${indent}${color.bold("copilot")} GitHub Copilot ${color.dim("(\u2192 github-copilot)")}
|
|
21174
|
+
`
|
|
21175
|
+
);
|
|
21176
|
+
}
|
|
21177
|
+
async function runOAuthLoginChoice(deps, choice, opts = {}) {
|
|
21178
|
+
const pick = choice.trim().toLowerCase();
|
|
21179
|
+
const allowNumeric = opts.allowNumeric ?? true;
|
|
21180
|
+
if (allowNumeric && pick === "1" || pick === "chatgpt" || pick === "openai" || pick === "codex") {
|
|
21181
|
+
await runCodexOAuthLogin(deps);
|
|
21182
|
+
return true;
|
|
21183
|
+
}
|
|
21184
|
+
if (allowNumeric && pick === "2" || pick === "claude" || pick === "anthropic") {
|
|
21185
|
+
await runClaudeOAuthLogin(deps);
|
|
21186
|
+
return true;
|
|
21187
|
+
}
|
|
21188
|
+
if (allowNumeric && pick === "3" || pick === "copilot" || pick === "github" || pick === "github-copilot") {
|
|
21189
|
+
await runCopilotOAuthLogin(deps);
|
|
21190
|
+
return true;
|
|
21191
|
+
}
|
|
21192
|
+
return false;
|
|
21193
|
+
}
|
|
21194
|
+
async function runOAuthLoginMenu(deps) {
|
|
21195
|
+
deps.renderer.write(
|
|
21196
|
+
`
|
|
21197
|
+
${color.bold("Login with OAuth:")}
|
|
21198
|
+
` + color.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color.dim(" sanctioned path for programmatic use.\n") + ` ${color.bold("1")} ChatGPT Plus/Pro ${color.dim("(\u2192 openai-codex)")}
|
|
21199
|
+
${color.bold("2")} Claude Pro/Max ${color.dim("(\u2192 anthropic-oauth)")}
|
|
21200
|
+
${color.bold("3")} GitHub Copilot ${color.dim("(\u2192 github-copilot)")}
|
|
21201
|
+
`
|
|
21202
|
+
);
|
|
21203
|
+
const pick = await deps.reader.readLine(` ${color.amber("?")} Pick ${color.dim("(or b to go back)")}: `);
|
|
21204
|
+
await runOAuthLoginChoice(deps, pick);
|
|
21205
|
+
}
|
|
21206
|
+
|
|
21207
|
+
// src/auth-menu/add-provider.ts
|
|
21208
|
+
async function addFromCatalog(deps) {
|
|
21209
|
+
let catalog = [];
|
|
21210
|
+
try {
|
|
21211
|
+
catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
|
|
21212
|
+
} catch {
|
|
21213
|
+
deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.\n");
|
|
21214
|
+
}
|
|
21215
|
+
if (catalog.length === 0) {
|
|
21216
|
+
return addManualEntry(deps);
|
|
21217
|
+
}
|
|
21218
|
+
const saved = new Set(Object.keys(await loadProviders(deps)));
|
|
21219
|
+
deps.renderer.write(
|
|
21220
|
+
color.dim(
|
|
21221
|
+
` Catalog: ${catalog.length} providers. Filter to narrow, "s" for unsaved-only, or enter to show all.
|
|
21222
|
+
`
|
|
21223
|
+
)
|
|
21224
|
+
);
|
|
21225
|
+
const filterRaw = (await deps.reader.readLine(
|
|
21226
|
+
` ${color.amber("?")} Filter ${color.dim('(substring / "s" / q to quit)')}: `
|
|
21227
|
+
)).trim();
|
|
21228
|
+
if (filterRaw === "q") return false;
|
|
21229
|
+
const filterLc = filterRaw.toLowerCase();
|
|
21230
|
+
const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
|
|
21231
|
+
function matches(p) {
|
|
21232
|
+
if (showUnsavedOnly) return !saved.has(p.id);
|
|
21233
|
+
if (!filterLc) return true;
|
|
21234
|
+
return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
|
|
21235
|
+
}
|
|
21236
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
21237
|
+
let filteredCount = 0;
|
|
21238
|
+
for (const p of catalog) {
|
|
21239
|
+
if (!matches(p)) continue;
|
|
21240
|
+
filteredCount++;
|
|
21241
|
+
const list = byFamily.get(p.family) ?? [];
|
|
21242
|
+
list.push(p);
|
|
21243
|
+
byFamily.set(p.family, list);
|
|
21244
|
+
}
|
|
21245
|
+
if (filteredCount === 0) {
|
|
21246
|
+
deps.renderer.writeError(
|
|
21247
|
+
`No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\`.`
|
|
21248
|
+
);
|
|
21249
|
+
return false;
|
|
21250
|
+
}
|
|
21251
|
+
if (filterRaw && !showUnsavedOnly) {
|
|
21252
|
+
deps.renderer.write(
|
|
21253
|
+
color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
|
|
21254
|
+
`)
|
|
21255
|
+
);
|
|
21256
|
+
}
|
|
21257
|
+
renderOAuthLoginOptions(deps);
|
|
21258
|
+
deps.renderer.write("\n");
|
|
21259
|
+
const ordered = [];
|
|
21260
|
+
const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
|
|
21261
|
+
let idx = 1;
|
|
21262
|
+
deps.renderer.write("\n");
|
|
21263
|
+
for (const fam of familyOrder) {
|
|
21264
|
+
const list = byFamily.get(fam);
|
|
21265
|
+
if (!list || list.length === 0) continue;
|
|
21266
|
+
deps.renderer.write(` ${color.bold(fam)}
|
|
21267
|
+
`);
|
|
21268
|
+
for (const p of list) {
|
|
21269
|
+
const savedMark = saved.has(p.id) ? color.cyan("\u25C9") : color.dim("\u25CB");
|
|
21270
|
+
const env = p.envVars[0] ? color.dim(`[${p.envVars[0]}]`) : "";
|
|
21271
|
+
deps.renderer.write(
|
|
21272
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${savedMark} ${p.id.padEnd(22)} ${color.dim(p.name)} ${env}
|
|
21273
|
+
`
|
|
21274
|
+
);
|
|
21275
|
+
ordered.push(p);
|
|
21276
|
+
idx++;
|
|
21277
|
+
}
|
|
21278
|
+
}
|
|
21279
|
+
deps.renderer.write(`
|
|
21280
|
+
${color.dim("\u25C9 already saved \u25CB no key yet")}
|
|
21281
|
+
`);
|
|
21282
|
+
const answer = (await deps.reader.readLine(
|
|
21283
|
+
`
|
|
21284
|
+
${color.amber("?")} Pick (1-${ordered.length}), type provider id, or OAuth option ${color.dim("[chatgpt/claude/copilot, q to quit]")}: `
|
|
21285
|
+
)).trim();
|
|
21286
|
+
if (!answer || answer === "q") return false;
|
|
21287
|
+
if (await runOAuthLoginChoice(deps, answer, { allowNumeric: false })) {
|
|
21288
|
+
return true;
|
|
21289
|
+
}
|
|
21290
|
+
let chosen;
|
|
21291
|
+
const num = Number.parseInt(answer, 10);
|
|
21292
|
+
if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
|
|
21293
|
+
chosen = ordered[num - 1];
|
|
21294
|
+
} else {
|
|
21295
|
+
chosen = ordered.find((p) => p.id.toLowerCase() === answer.toLowerCase()) ?? catalog.find((p) => p.id.toLowerCase() === answer.toLowerCase());
|
|
21296
|
+
}
|
|
21297
|
+
if (!chosen) {
|
|
21298
|
+
deps.renderer.writeError(`No such provider: "${answer}"`);
|
|
21299
|
+
return false;
|
|
21300
|
+
}
|
|
21301
|
+
return addKeyForCatalogProvider(deps, chosen);
|
|
21302
|
+
}
|
|
21303
|
+
async function addKeyForCatalogProvider(deps, chosen) {
|
|
21304
|
+
deps.renderer.write(
|
|
21305
|
+
color.dim(`
|
|
21306
|
+
Defaults from models.dev \u2014 press Enter to keep, or type overrides.
|
|
21307
|
+
`)
|
|
21308
|
+
);
|
|
21309
|
+
const famRaw = (await deps.reader.readLine(
|
|
21310
|
+
` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `
|
|
21311
|
+
)).trim();
|
|
21312
|
+
if (famRaw === "q") return false;
|
|
21313
|
+
let family = chosen.family;
|
|
21314
|
+
if (famRaw) {
|
|
21315
|
+
const validated = validateFamily(famRaw);
|
|
21316
|
+
if (!validated) {
|
|
21317
|
+
deps.renderer.writeError(
|
|
21318
|
+
`Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
|
|
21319
|
+
);
|
|
21320
|
+
return false;
|
|
21321
|
+
}
|
|
21322
|
+
family = validated;
|
|
21323
|
+
}
|
|
21324
|
+
const baseRaw = (await deps.reader.readLine(
|
|
21325
|
+
` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
|
|
21326
|
+
)).trim();
|
|
21327
|
+
if (baseRaw === "q") return false;
|
|
21328
|
+
const baseUrl = baseRaw || chosen.apiBase;
|
|
21329
|
+
const providersNow = await loadProviders(deps);
|
|
21330
|
+
let suggestedAlias = chosen.id;
|
|
21331
|
+
if (family !== chosen.family) {
|
|
21332
|
+
let candidate = `${chosen.id}-${family}`;
|
|
21333
|
+
let n = 2;
|
|
21334
|
+
while (providersNow[candidate]) {
|
|
21335
|
+
candidate = `${chosen.id}-${family}-${n}`;
|
|
21336
|
+
n++;
|
|
21337
|
+
}
|
|
21338
|
+
suggestedAlias = candidate;
|
|
21339
|
+
}
|
|
21340
|
+
const aliasRaw = (await deps.reader.readLine(
|
|
21341
|
+
` ${color.amber("?")} Save as alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used with --provider <alias>)")}: `
|
|
21342
|
+
)).trim();
|
|
21343
|
+
const alias = aliasRaw || suggestedAlias;
|
|
21344
|
+
const existing = providersNow[alias];
|
|
21345
|
+
if (existing) {
|
|
21346
|
+
const sameFamily = (existing.family ?? chosen.family) === family;
|
|
21347
|
+
const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
|
|
21348
|
+
if (!sameFamily || !sameBase) {
|
|
21349
|
+
deps.renderer.writeError(
|
|
21350
|
+
`Alias "${alias}" already exists with different family/baseUrl.
|
|
21351
|
+
Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
|
|
21352
|
+
New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
|
|
21353
|
+
Pick a different alias to keep them separate.`
|
|
21354
|
+
);
|
|
21355
|
+
return false;
|
|
21356
|
+
}
|
|
21357
|
+
}
|
|
21358
|
+
return addKeyForProvider(alias, deps, {
|
|
21359
|
+
type: chosen.id,
|
|
21360
|
+
family,
|
|
21361
|
+
baseUrl,
|
|
21362
|
+
envVars: chosen.envVars
|
|
21363
|
+
});
|
|
21364
|
+
}
|
|
21365
|
+
async function addCustomProvider(deps) {
|
|
21366
|
+
deps.renderer.write(
|
|
21367
|
+
`
|
|
21368
|
+
${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the catalog.")}
|
|
21369
|
+
`
|
|
21370
|
+
);
|
|
21371
|
+
const type = (await deps.reader.readLine(
|
|
21372
|
+
` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
|
|
21373
|
+
)).trim();
|
|
21374
|
+
if (!type || type === "q") return false;
|
|
21375
|
+
const existing = (await loadProviders(deps))[type];
|
|
21376
|
+
if (existing) {
|
|
21377
|
+
deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
|
|
21378
|
+
return false;
|
|
21379
|
+
}
|
|
21380
|
+
const familyRaw = (await deps.reader.readLine(
|
|
21381
|
+
` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
|
|
21382
|
+
)).trim();
|
|
21383
|
+
if (familyRaw === "q") return false;
|
|
21384
|
+
const family = validateFamily(familyRaw);
|
|
21385
|
+
if (!family) {
|
|
21386
|
+
deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
|
|
21387
|
+
return false;
|
|
21388
|
+
}
|
|
21389
|
+
const baseUrl = (await deps.reader.readLine(
|
|
21390
|
+
` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, optional)")}: `
|
|
21391
|
+
)).trim();
|
|
21392
|
+
const modelsRaw = (await deps.reader.readLine(
|
|
21393
|
+
` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
|
|
21394
|
+
)).trim();
|
|
21395
|
+
const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
21396
|
+
const envVarsRaw = (await deps.reader.readLine(
|
|
21397
|
+
` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional)")}: `
|
|
21398
|
+
)).trim();
|
|
21399
|
+
const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
21400
|
+
return addKeyForProvider(type, deps, {
|
|
21401
|
+
type,
|
|
21402
|
+
family,
|
|
21403
|
+
...baseUrl ? { baseUrl } : {},
|
|
21404
|
+
...models ? { models } : {},
|
|
21405
|
+
...envVars ? { envVars } : {}
|
|
21406
|
+
});
|
|
21407
|
+
}
|
|
21408
|
+
async function addManualEntry(deps) {
|
|
21409
|
+
const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
|
|
21410
|
+
if (!pid || pid === "q") return false;
|
|
21411
|
+
const famRaw = (await deps.reader.readLine(
|
|
21412
|
+
` ${color.amber("?")} Family ${color.dim("(anthropic/openai/openai-compatible/google)")}: `
|
|
21413
|
+
)).trim();
|
|
21414
|
+
const family = validateFamily(famRaw);
|
|
21415
|
+
if (!family) {
|
|
21416
|
+
deps.renderer.writeError(`Invalid family: "${famRaw}"`);
|
|
21417
|
+
return false;
|
|
21418
|
+
}
|
|
21419
|
+
const baseUrl = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
|
|
21420
|
+
return addKeyForProvider(pid, deps, {
|
|
21421
|
+
type: pid,
|
|
21422
|
+
family,
|
|
21423
|
+
...baseUrl ? { baseUrl } : {}
|
|
21424
|
+
});
|
|
21425
|
+
}
|
|
21426
|
+
async function addKeyForProvider(providerId, deps, template) {
|
|
21427
|
+
const providers = await loadProviders(deps);
|
|
21428
|
+
const existing = providers[providerId];
|
|
21429
|
+
const existingKeys = existing ? normalizeKeys(existing) : [];
|
|
21430
|
+
const usedLabels = new Set(existingKeys.map((k) => k.label));
|
|
21431
|
+
const label = await promptForLabel(deps, usedLabels);
|
|
21432
|
+
if (!label) return false;
|
|
21433
|
+
const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
|
|
21434
|
+
if (!apiKey) return false;
|
|
21435
|
+
await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
|
|
21436
|
+
const existingProv = all[providerId] ?? {
|
|
21437
|
+
type: providerId,
|
|
21438
|
+
...template
|
|
21439
|
+
};
|
|
21440
|
+
if (!existingProv.type) existingProv.type = providerId;
|
|
21441
|
+
if (!existingProv.family && template.family) {
|
|
21442
|
+
existingProv.family = template.family;
|
|
21443
|
+
}
|
|
21444
|
+
if (!existingProv.baseUrl && template.baseUrl) {
|
|
21445
|
+
existingProv.baseUrl = template.baseUrl;
|
|
21446
|
+
}
|
|
21447
|
+
if (!existingProv.envVars && template.envVars) {
|
|
21448
|
+
existingProv.envVars = template.envVars;
|
|
21449
|
+
}
|
|
21450
|
+
const list = normalizeKeys(existingProv);
|
|
21451
|
+
list.push({ label, apiKey, createdAt: nowIso() });
|
|
21452
|
+
writeKeysBack(existingProv, list);
|
|
21453
|
+
if (!existingProv.activeKey) existingProv.activeKey = label;
|
|
21454
|
+
all[providerId] = existingProv;
|
|
21455
|
+
});
|
|
21456
|
+
deps.renderer.write(
|
|
21457
|
+
` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
|
|
21458
|
+
`
|
|
21459
|
+
);
|
|
21460
|
+
deps.renderer.write(color.dim(` Launch: wstack --provider ${providerId} "<task>"
|
|
21461
|
+
`));
|
|
21462
|
+
return true;
|
|
21463
|
+
}
|
|
21464
|
+
async function promptForLabel(deps, usedLabels) {
|
|
21465
|
+
const defaultLabel = suggestLabel(usedLabels);
|
|
21466
|
+
const labelRaw = (await deps.reader.readLine(
|
|
21467
|
+
` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
|
|
21468
|
+
)).trim();
|
|
21469
|
+
const label = labelRaw || defaultLabel;
|
|
21470
|
+
if (usedLabels.has(label)) {
|
|
21471
|
+
deps.renderer.writeError(`Label "${label}" is already used. Use update (u) instead.`);
|
|
21472
|
+
return null;
|
|
21473
|
+
}
|
|
21474
|
+
return label;
|
|
21475
|
+
}
|
|
21476
|
+
|
|
19970
21477
|
// src/auth-menu/provider-menu.ts
|
|
19971
21478
|
init_provider_config_utils();
|
|
19972
21479
|
async function manageProvider(providerId, deps) {
|
|
@@ -20142,24 +21649,6 @@ function validKeyIndex(arg, max, deps, verb) {
|
|
|
20142
21649
|
}
|
|
20143
21650
|
|
|
20144
21651
|
// src/auth-menu/top-menu.ts
|
|
20145
|
-
async function runSignInMenu(deps) {
|
|
20146
|
-
deps.renderer.write(
|
|
20147
|
-
`
|
|
20148
|
-
${color.bold("Sign in with a subscription:")}
|
|
20149
|
-
` + color.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color.dim(" sanctioned path for programmatic use.\n") + ` ${color.bold("1")} ChatGPT Plus/Pro ${color.dim("(\u2192 openai-codex)")}
|
|
20150
|
-
${color.bold("2")} Claude Pro/Max ${color.dim("(\u2192 anthropic-oauth)")}
|
|
20151
|
-
${color.bold("3")} GitHub Copilot ${color.dim("(\u2192 github-copilot)")}
|
|
20152
|
-
`
|
|
20153
|
-
);
|
|
20154
|
-
const pick = (await deps.reader.readLine(` ${color.amber("?")} Pick ${color.dim("(or b to go back)")}: `)).trim().toLowerCase();
|
|
20155
|
-
if (pick === "1" || pick === "chatgpt" || pick === "openai" || pick === "codex") {
|
|
20156
|
-
await runCodexOAuthLogin(deps);
|
|
20157
|
-
} else if (pick === "2" || pick === "claude" || pick === "anthropic") {
|
|
20158
|
-
await runClaudeOAuthLogin(deps);
|
|
20159
|
-
} else if (pick === "3" || pick === "copilot" || pick === "github") {
|
|
20160
|
-
await runCopilotOAuthLogin(deps);
|
|
20161
|
-
}
|
|
20162
|
-
}
|
|
20163
21652
|
async function runTopMenu(deps) {
|
|
20164
21653
|
for (; ; ) {
|
|
20165
21654
|
const providers = await loadProviders(deps);
|
|
@@ -20179,8 +21668,8 @@ ${color.amber("?")} Pick: `)).trim().toLowerCase();
|
|
|
20179
21668
|
await addCustomProvider(deps);
|
|
20180
21669
|
continue;
|
|
20181
21670
|
}
|
|
20182
|
-
if (choice === "s" || choice === "signin" || choice === "login") {
|
|
20183
|
-
await
|
|
21671
|
+
if (choice === "s" || choice === "signin" || choice === "login" || choice === "oauth") {
|
|
21672
|
+
await runOAuthLoginMenu(deps);
|
|
20184
21673
|
continue;
|
|
20185
21674
|
}
|
|
20186
21675
|
const idx = Number.parseInt(choice, 10);
|
|
@@ -20843,6 +22332,262 @@ var exportCmd = async (args, deps) => {
|
|
|
20843
22332
|
}
|
|
20844
22333
|
return 0;
|
|
20845
22334
|
};
|
|
22335
|
+
function resolveDataDir(deps) {
|
|
22336
|
+
const override = typeof deps.flags?.["data-dir"] === "string" ? deps.flags["data-dir"] : void 0;
|
|
22337
|
+
return resolveHqDataDir(override);
|
|
22338
|
+
}
|
|
22339
|
+
var hqCmd = async (args, deps) => {
|
|
22340
|
+
const sub = args[0];
|
|
22341
|
+
if (!sub || sub === "serve") {
|
|
22342
|
+
return startServer(deps);
|
|
22343
|
+
}
|
|
22344
|
+
if (sub === "token") {
|
|
22345
|
+
return hqTokenCmd(args.slice(1), deps);
|
|
22346
|
+
}
|
|
22347
|
+
if (sub === "help" || sub === "--help") {
|
|
22348
|
+
printHelp(deps);
|
|
22349
|
+
return 0;
|
|
22350
|
+
}
|
|
22351
|
+
deps.renderer.writeError(`Unknown hq subcommand: ${sub}
|
|
22352
|
+
`);
|
|
22353
|
+
printHelp(deps);
|
|
22354
|
+
return 1;
|
|
22355
|
+
};
|
|
22356
|
+
async function startServer(deps) {
|
|
22357
|
+
const { startHqServer: startHqServer2 } = await Promise.resolve().then(() => (init_hq_server(), hq_server_exports));
|
|
22358
|
+
const dataDir = resolveDataDir(deps);
|
|
22359
|
+
const flags = deps.flags ?? {};
|
|
22360
|
+
const host = typeof flags["host"] === "string" ? flags["host"] : "127.0.0.1";
|
|
22361
|
+
const port = typeof flags["port"] === "string" ? Number.parseInt(flags["port"], 10) : 3499;
|
|
22362
|
+
const strictPort = flags["strict-port"] === true;
|
|
22363
|
+
const open = flags["open"] === true;
|
|
22364
|
+
const handle = await startHqServer2({ host, port, strictPort, dataDir });
|
|
22365
|
+
if (open) {
|
|
22366
|
+
try {
|
|
22367
|
+
const { openBrowser: openBrowser5 } = await import('@wrongstack/webui/server');
|
|
22368
|
+
openBrowser5(handle.firstRunSetup?.browserUrl ?? `http://${handle.host}:${handle.port}`);
|
|
22369
|
+
} catch {
|
|
22370
|
+
}
|
|
22371
|
+
}
|
|
22372
|
+
writeStartupInfo(deps, handle);
|
|
22373
|
+
await new Promise((resolve12) => {
|
|
22374
|
+
const shutdown = () => {
|
|
22375
|
+
void handle.close().then(() => resolve12());
|
|
22376
|
+
};
|
|
22377
|
+
process.on("SIGINT", shutdown);
|
|
22378
|
+
process.on("SIGTERM", shutdown);
|
|
22379
|
+
});
|
|
22380
|
+
return 0;
|
|
22381
|
+
}
|
|
22382
|
+
function writeStartupInfo(deps, handle) {
|
|
22383
|
+
deps.renderer.write(`WrongStack HQ listening on http://${handle.host}:${handle.port}
|
|
22384
|
+
`);
|
|
22385
|
+
if (!handle.firstRunSetup) {
|
|
22386
|
+
deps.renderer.write(`Client endpoint: ws://${handle.host}:${handle.port}/ws/client
|
|
22387
|
+
`);
|
|
22388
|
+
deps.renderer.write(`Browser endpoint: http://${handle.host}:${handle.port}
|
|
22389
|
+
`);
|
|
22390
|
+
return;
|
|
22391
|
+
}
|
|
22392
|
+
deps.renderer.write(`Browser endpoint: ${handle.firstRunSetup.browserUrl}
|
|
22393
|
+
`);
|
|
22394
|
+
deps.renderer.write(`Client endpoint: ws://${handle.host}:${handle.port}/ws/client?token=${handle.firstRunSetup.clientEnv.WRONGSTACK_HQ_TOKEN}
|
|
22395
|
+
`);
|
|
22396
|
+
deps.renderer.write(`
|
|
22397
|
+
First-run HQ auth created in ${handle.firstRunSetup.dataDir}
|
|
22398
|
+
`);
|
|
22399
|
+
deps.renderer.write(`Start clients with:
|
|
22400
|
+
`);
|
|
22401
|
+
deps.renderer.write(` WRONGSTACK_HQ_URL=${handle.firstRunSetup.clientEnv.WRONGSTACK_HQ_URL}
|
|
22402
|
+
`);
|
|
22403
|
+
deps.renderer.write(` WRONGSTACK_HQ_TOKEN=${handle.firstRunSetup.clientEnv.WRONGSTACK_HQ_TOKEN}
|
|
22404
|
+
`);
|
|
22405
|
+
}
|
|
22406
|
+
async function hqTokenCmd(args, deps) {
|
|
22407
|
+
const action = args[0];
|
|
22408
|
+
if (action === "create") {
|
|
22409
|
+
return tokenCreate(args.slice(1), deps);
|
|
22410
|
+
}
|
|
22411
|
+
if (action === "list" || action === "ls" || !action) {
|
|
22412
|
+
return tokenList(args.slice(1), deps);
|
|
22413
|
+
}
|
|
22414
|
+
if (action === "revoke" || action === "rm" || action === "remove") {
|
|
22415
|
+
return tokenRevoke(args.slice(1), deps);
|
|
22416
|
+
}
|
|
22417
|
+
deps.renderer.writeError(`Unknown hq token subcommand: ${action ?? "(none)"}
|
|
22418
|
+
`);
|
|
22419
|
+
deps.renderer.write("Usage: wstack hq token <create|list|revoke>\n");
|
|
22420
|
+
return 1;
|
|
22421
|
+
}
|
|
22422
|
+
function resolveTokenScope(args) {
|
|
22423
|
+
return args.some((a) => a === "--client" || a === "-c") ? "client" : "browser";
|
|
22424
|
+
}
|
|
22425
|
+
function positionals(args) {
|
|
22426
|
+
return args.filter((a) => !a.startsWith("-"));
|
|
22427
|
+
}
|
|
22428
|
+
async function tokenCreate(args, deps) {
|
|
22429
|
+
const scope = resolveTokenScope(args);
|
|
22430
|
+
const pos = positionals(args);
|
|
22431
|
+
const label = pos[0];
|
|
22432
|
+
const dataDir = resolveDataDir(deps);
|
|
22433
|
+
const tokenField = scope === "client" ? "clientTokens" : "browserTokens";
|
|
22434
|
+
try {
|
|
22435
|
+
const next = await mutateHqAuthFile(
|
|
22436
|
+
dataDir,
|
|
22437
|
+
(current) => {
|
|
22438
|
+
const tokens = current[tokenField] ?? [];
|
|
22439
|
+
const newToken = mintHqToken(label);
|
|
22440
|
+
return {
|
|
22441
|
+
...current,
|
|
22442
|
+
[tokenField]: [...tokens, newToken]
|
|
22443
|
+
};
|
|
22444
|
+
},
|
|
22445
|
+
{ warn: (msg) => deps.renderer.writeWarning(`${msg}
|
|
22446
|
+
`) }
|
|
22447
|
+
);
|
|
22448
|
+
const list = next[tokenField] ?? [];
|
|
22449
|
+
const token = expectDefined(list[list.length - 1]);
|
|
22450
|
+
const endpoint = scope === "client" ? "/ws/client" : "/ws/browser";
|
|
22451
|
+
deps.renderer.write(`Created ${scope} token.
|
|
22452
|
+
`);
|
|
22453
|
+
deps.renderer.write(` id: ${token.id}
|
|
22454
|
+
`);
|
|
22455
|
+
if (token.label) deps.renderer.write(` label: ${token.label}
|
|
22456
|
+
`);
|
|
22457
|
+
deps.renderer.write(` token: ${token.token}
|
|
22458
|
+
`);
|
|
22459
|
+
deps.renderer.write(` createdAt: ${token.createdAt}
|
|
22460
|
+
`);
|
|
22461
|
+
deps.renderer.write(`
|
|
22462
|
+
`);
|
|
22463
|
+
deps.renderer.write(`Connect with: ws://localhost:3499${endpoint}?token=${token.token}
|
|
22464
|
+
`);
|
|
22465
|
+
deps.renderer.write(`(Copy the token now \u2014 it will not be shown again in full.)
|
|
22466
|
+
`);
|
|
22467
|
+
return 0;
|
|
22468
|
+
} catch (err) {
|
|
22469
|
+
deps.renderer.writeError(`Failed to write auth.json: ${err.message}
|
|
22470
|
+
`);
|
|
22471
|
+
return 1;
|
|
22472
|
+
}
|
|
22473
|
+
}
|
|
22474
|
+
async function tokenList(args, deps) {
|
|
22475
|
+
const scope = resolveTokenScope(args);
|
|
22476
|
+
const dataDir = resolveDataDir(deps);
|
|
22477
|
+
const tokenField = scope === "client" ? "clientTokens" : "browserTokens";
|
|
22478
|
+
const authFile = await readHqAuthFile(dataDir, {
|
|
22479
|
+
warn: (msg) => deps.renderer.writeWarning(`${msg}
|
|
22480
|
+
`)
|
|
22481
|
+
});
|
|
22482
|
+
const tokens = authFile[tokenField] ?? [];
|
|
22483
|
+
if (tokens.length === 0) {
|
|
22484
|
+
deps.renderer.write(`No ${scope} tokens issued. ${scope === "browser" ? "Browsers" : "Clients"} are in OPEN MODE.
|
|
22485
|
+
`);
|
|
22486
|
+
deps.renderer.write(`Run \`wstack hq token create ${scope === "client" ? "--client " : ""}[label]\` to enter TOKEN MODE.
|
|
22487
|
+
`);
|
|
22488
|
+
return 0;
|
|
22489
|
+
}
|
|
22490
|
+
deps.renderer.write(`${scope === "client" ? "Client" : "Browser"} tokens (${tokens.length}) \u2014 TOKEN MODE:
|
|
22491
|
+
`);
|
|
22492
|
+
deps.renderer.write("\n");
|
|
22493
|
+
for (const t of tokens) {
|
|
22494
|
+
const masked = `${t.token.slice(0, 6)}\u2026${t.token.slice(-4)} (${t.token.length} chars)`;
|
|
22495
|
+
deps.renderer.write(` ${t.id} ${masked} ${t.createdAt}${t.label ? ` "${t.label}"` : ""}${t.lastUsedAt ? ` lastUsed ${t.lastUsedAt}` : ""}
|
|
22496
|
+
`);
|
|
22497
|
+
}
|
|
22498
|
+
deps.renderer.write("\n");
|
|
22499
|
+
deps.renderer.write(`${scope === "client" ? "Clients" : "Browsers"} must append ?token=<full-token> to /ws/${scope}.
|
|
22500
|
+
`);
|
|
22501
|
+
return 0;
|
|
22502
|
+
}
|
|
22503
|
+
async function tokenRevoke(args, deps) {
|
|
22504
|
+
const scope = resolveTokenScope(args);
|
|
22505
|
+
const pos = positionals(args);
|
|
22506
|
+
const idPrefix = pos[0];
|
|
22507
|
+
if (!idPrefix) {
|
|
22508
|
+
deps.renderer.writeError(`Usage: wstack hq token revoke ${scope === "client" ? "--client " : ""}<id-prefix>
|
|
22509
|
+
`);
|
|
22510
|
+
return 1;
|
|
22511
|
+
}
|
|
22512
|
+
const dataDir = resolveDataDir(deps);
|
|
22513
|
+
const tokenField = scope === "client" ? "clientTokens" : "browserTokens";
|
|
22514
|
+
let revoked;
|
|
22515
|
+
try {
|
|
22516
|
+
await mutateHqAuthFile(
|
|
22517
|
+
dataDir,
|
|
22518
|
+
(current) => {
|
|
22519
|
+
const tokens = current[tokenField] ?? [];
|
|
22520
|
+
const matches = tokens.filter((t) => t.id.startsWith(idPrefix));
|
|
22521
|
+
if (matches.length === 0) {
|
|
22522
|
+
revoked = void 0;
|
|
22523
|
+
return current;
|
|
22524
|
+
}
|
|
22525
|
+
if (matches.length > 1) {
|
|
22526
|
+
revoked = matches[0];
|
|
22527
|
+
return current;
|
|
22528
|
+
}
|
|
22529
|
+
revoked = matches[0];
|
|
22530
|
+
return {
|
|
22531
|
+
...current,
|
|
22532
|
+
[tokenField]: tokens.filter((t) => t.id !== revoked.id)
|
|
22533
|
+
};
|
|
22534
|
+
},
|
|
22535
|
+
{ warn: (msg) => deps.renderer.writeWarning(`${msg}
|
|
22536
|
+
`) }
|
|
22537
|
+
);
|
|
22538
|
+
} catch (err) {
|
|
22539
|
+
deps.renderer.writeError(`Failed to write auth.json: ${err.message}
|
|
22540
|
+
`);
|
|
22541
|
+
return 1;
|
|
22542
|
+
}
|
|
22543
|
+
if (!revoked) {
|
|
22544
|
+
deps.renderer.writeError(`No ${scope} token found matching id-prefix "${idPrefix}".
|
|
22545
|
+
`);
|
|
22546
|
+
return 1;
|
|
22547
|
+
}
|
|
22548
|
+
deps.renderer.write(`Revoked ${scope} token ${revoked.id}${revoked.label ? ` ("${revoked.label}")` : ""}.
|
|
22549
|
+
`);
|
|
22550
|
+
return 0;
|
|
22551
|
+
}
|
|
22552
|
+
function printHelp(deps) {
|
|
22553
|
+
deps.renderer.write(`Usage: wstack hq <serve | token>
|
|
22554
|
+
`);
|
|
22555
|
+
deps.renderer.write("\n");
|
|
22556
|
+
deps.renderer.write(` wstack hq Start the HQ command center server.
|
|
22557
|
+
`);
|
|
22558
|
+
deps.renderer.write(` wstack hq serve Same as above (explicit form).
|
|
22559
|
+
`);
|
|
22560
|
+
deps.renderer.write(` wstack hq token create [label] Mint a browser token, enter token mode.
|
|
22561
|
+
`);
|
|
22562
|
+
deps.renderer.write(` wstack hq token create --client [label] Mint a client token (/ws/client).
|
|
22563
|
+
`);
|
|
22564
|
+
deps.renderer.write(` wstack hq token list List issued browser tokens.
|
|
22565
|
+
`);
|
|
22566
|
+
deps.renderer.write(` wstack hq token list --client List issued client tokens.
|
|
22567
|
+
`);
|
|
22568
|
+
deps.renderer.write(` wstack hq token revoke <id> Revoke a browser token (id prefix match).
|
|
22569
|
+
`);
|
|
22570
|
+
deps.renderer.write(` wstack hq token revoke --client <id> Revoke a client token.
|
|
22571
|
+
`);
|
|
22572
|
+
deps.renderer.write("\n");
|
|
22573
|
+
deps.renderer.write(`Flags (apply to all subcommands):
|
|
22574
|
+
`);
|
|
22575
|
+
deps.renderer.write(` --data-dir <path> Override HQ data directory (default ~/.wrongstack/hq).
|
|
22576
|
+
`);
|
|
22577
|
+
deps.renderer.write(` --host <ip> Bind host (default 127.0.0.1).
|
|
22578
|
+
`);
|
|
22579
|
+
deps.renderer.write(` --port <n> Bind port (default 3499).
|
|
22580
|
+
`);
|
|
22581
|
+
deps.renderer.write(` --strict-port Fail if port is in use.
|
|
22582
|
+
`);
|
|
22583
|
+
deps.renderer.write(` --open Open the dashboard in the default browser.
|
|
22584
|
+
`);
|
|
22585
|
+
deps.renderer.write(` --client, -c Operate on client tokens instead of browser tokens.
|
|
22586
|
+
`);
|
|
22587
|
+
deps.renderer.write("\n");
|
|
22588
|
+
deps.renderer.write(`auth.json schema version: ${HQ_AUTH_FILE_VERSION}.
|
|
22589
|
+
`);
|
|
22590
|
+
}
|
|
20846
22591
|
var initCmd = async (_args, deps) => {
|
|
20847
22592
|
deps.renderer.write(color.bold("WrongStack init (deprecated)\n"));
|
|
20848
22593
|
deps.renderer.write(
|
|
@@ -20999,8 +22744,8 @@ async function serveMcpStdio(deps) {
|
|
|
20999
22744
|
log(
|
|
21000
22745
|
`wrongstack MCP server ready at ${handle2.url} \u2014 exposing ${allowed.length} tool(s) (${mode})${token ? " [token auth]" : ""}.`
|
|
21001
22746
|
);
|
|
21002
|
-
await new Promise((
|
|
21003
|
-
const stop = () =>
|
|
22747
|
+
await new Promise((resolve12) => {
|
|
22748
|
+
const stop = () => resolve12();
|
|
21004
22749
|
process.once("SIGINT", stop);
|
|
21005
22750
|
process.once("SIGTERM", stop);
|
|
21006
22751
|
});
|
|
@@ -21085,18 +22830,14 @@ async function addMcpServer(args, deps) {
|
|
|
21085
22830
|
}
|
|
21086
22831
|
const serverCfg = { ...factory };
|
|
21087
22832
|
serverCfg.enabled = enable;
|
|
21088
|
-
|
|
21089
|
-
|
|
21090
|
-
existing = JSON.parse(await fsp5.readFile(deps.paths.globalConfig, "utf8"));
|
|
21091
|
-
} catch {
|
|
21092
|
-
}
|
|
21093
|
-
const mcpServers = existing.mcpServers ?? {};
|
|
22833
|
+
const existing = await readJsonObjectFile(deps.paths.globalConfig);
|
|
22834
|
+
const mcpServers = isRecord(existing.mcpServers) ? existing.mcpServers : {};
|
|
21094
22835
|
if (mcpServers[name])
|
|
21095
22836
|
deps.renderer.writeWarning(`Server "${name}" already in config. Updating.
|
|
21096
22837
|
`);
|
|
21097
|
-
|
|
21098
|
-
|
|
21099
|
-
|
|
22838
|
+
await updateJsonObjectFile(deps.paths.globalConfig, (config) => {
|
|
22839
|
+
setJsonPath(config, ["mcpServers", name], serverCfg);
|
|
22840
|
+
});
|
|
21100
22841
|
const verb = enable ? "Enabled" : "Added (disabled \u2014 set enabled:true to activate)";
|
|
21101
22842
|
deps.renderer.writeInfo(
|
|
21102
22843
|
`${verb} "${name}" (${serverCfg.transport}). Config written to ${deps.paths.globalConfig}.
|
|
@@ -21105,26 +22846,27 @@ async function addMcpServer(args, deps) {
|
|
|
21105
22846
|
return 0;
|
|
21106
22847
|
}
|
|
21107
22848
|
async function removeMcpServer(name, deps) {
|
|
21108
|
-
|
|
21109
|
-
try {
|
|
21110
|
-
existing = JSON.parse(await fsp5.readFile(deps.paths.globalConfig, "utf8"));
|
|
21111
|
-
} catch {
|
|
22849
|
+
if (!await jsonObjectFileExists(deps.paths.globalConfig)) {
|
|
21112
22850
|
deps.renderer.writeError("No config file found.\n");
|
|
21113
22851
|
return 1;
|
|
21114
22852
|
}
|
|
21115
|
-
const
|
|
22853
|
+
const existing = await readJsonObjectFile(deps.paths.globalConfig);
|
|
22854
|
+
const mcpServers = isRecord(existing.mcpServers) ? existing.mcpServers : {};
|
|
21116
22855
|
if (!mcpServers[name]) {
|
|
21117
22856
|
deps.renderer.writeError(`Server "${name}" not in config.
|
|
21118
22857
|
`);
|
|
21119
22858
|
return 1;
|
|
21120
22859
|
}
|
|
21121
|
-
|
|
21122
|
-
|
|
21123
|
-
|
|
22860
|
+
await updateJsonObjectFile(deps.paths.globalConfig, (config) => {
|
|
22861
|
+
removeJsonPath(config, ["mcpServers", name]);
|
|
22862
|
+
});
|
|
21124
22863
|
deps.renderer.writeInfo(`Removed "${name}" from config.
|
|
21125
22864
|
`);
|
|
21126
22865
|
return 0;
|
|
21127
22866
|
}
|
|
22867
|
+
function isRecord(value) {
|
|
22868
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22869
|
+
}
|
|
21128
22870
|
var MODEL_PROFILES = [
|
|
21129
22871
|
{
|
|
21130
22872
|
provider: "anthropic",
|
|
@@ -22130,7 +23872,7 @@ function renderConfiguredPlugins(config) {
|
|
|
22130
23872
|
return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
|
|
22131
23873
|
}).join("\n");
|
|
22132
23874
|
}
|
|
22133
|
-
async function
|
|
23875
|
+
async function readConfig(file) {
|
|
22134
23876
|
try {
|
|
22135
23877
|
return JSON.parse(await fsp5.readFile(file, "utf8"));
|
|
22136
23878
|
} catch {
|
|
@@ -22149,15 +23891,15 @@ function officialPluginState(config, spec) {
|
|
|
22149
23891
|
return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
|
|
22150
23892
|
}
|
|
22151
23893
|
async function upsertPlugin(spec, opts, deps, verb) {
|
|
22152
|
-
const existing = await
|
|
23894
|
+
const existing = await readConfig(deps.configPath);
|
|
22153
23895
|
const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
|
|
22154
23896
|
const idx = plugins.findIndex((p) => pluginName(p) === spec);
|
|
22155
23897
|
const nextEntry = pluginEntry(spec, opts.enabled);
|
|
22156
23898
|
if (idx >= 0) plugins[idx] = nextEntry;
|
|
22157
23899
|
else plugins.push(nextEntry);
|
|
22158
23900
|
const features = {
|
|
22159
|
-
...
|
|
22160
|
-
...
|
|
23901
|
+
...isRecord2(deps.config.features) ? deps.config.features : {},
|
|
23902
|
+
...isRecord2(existing.features) ? existing.features : {},
|
|
22161
23903
|
plugins: true
|
|
22162
23904
|
};
|
|
22163
23905
|
existing.plugins = plugins;
|
|
@@ -22172,7 +23914,7 @@ async function upsertPlugin(spec, opts, deps, verb) {
|
|
|
22172
23914
|
};
|
|
22173
23915
|
}
|
|
22174
23916
|
async function removePlugin(spec, deps) {
|
|
22175
|
-
const existing = await
|
|
23917
|
+
const existing = await readConfig(deps.configPath);
|
|
22176
23918
|
const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
|
|
22177
23919
|
const next = plugins.filter((p) => pluginName(p) !== spec);
|
|
22178
23920
|
if (next.length === plugins.length) {
|
|
@@ -22191,7 +23933,7 @@ async function removePlugin(spec, deps) {
|
|
|
22191
23933
|
function errorResult(message) {
|
|
22192
23934
|
return { code: 1, level: "error", message };
|
|
22193
23935
|
}
|
|
22194
|
-
function
|
|
23936
|
+
function isRecord2(value) {
|
|
22195
23937
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
22196
23938
|
}
|
|
22197
23939
|
|
|
@@ -22317,7 +24059,7 @@ function parseFlags2(args) {
|
|
|
22317
24059
|
}
|
|
22318
24060
|
return flags;
|
|
22319
24061
|
}
|
|
22320
|
-
function
|
|
24062
|
+
function positionals2(args) {
|
|
22321
24063
|
const out = [];
|
|
22322
24064
|
for (let i = 0; i < args.length; i++) {
|
|
22323
24065
|
const a = expectDefined(args[i]);
|
|
@@ -22335,11 +24077,17 @@ function positionals(args) {
|
|
|
22335
24077
|
return out;
|
|
22336
24078
|
}
|
|
22337
24079
|
var DEFAULT_PER_PAGE = 15;
|
|
24080
|
+
function fmtPrice3(usdPer1M) {
|
|
24081
|
+
if (usdPer1M === void 0) return color.dim("?");
|
|
24082
|
+
const value = usdPer1M >= 10 ? usdPer1M.toFixed(1) : usdPer1M.toFixed(2);
|
|
24083
|
+
return "$" + value;
|
|
24084
|
+
}
|
|
22338
24085
|
var modelsCmd = async (args, deps) => {
|
|
22339
24086
|
const sub = args[0];
|
|
22340
24087
|
if (sub === "add") return modelsAdd(args.slice(1), deps);
|
|
22341
24088
|
if (sub === "remove") return modelsRemove(args.slice(1), deps);
|
|
22342
24089
|
if (sub === "list") return modelsList(args.slice(1), deps);
|
|
24090
|
+
if (sub === "caps" || sub === "capabilities") return modelsCaps(args.slice(1), deps);
|
|
22343
24091
|
if (sub === "refresh") {
|
|
22344
24092
|
deps.renderer.writeInfo("Refreshing models.dev cache\u2026");
|
|
22345
24093
|
try {
|
|
@@ -22442,6 +24190,51 @@ ${navLines.join(" \xB7 ")}
|
|
|
22442
24190
|
);
|
|
22443
24191
|
return 0;
|
|
22444
24192
|
};
|
|
24193
|
+
async function modelsCaps(args, deps) {
|
|
24194
|
+
const providerId = args[0] ?? deps.config.provider;
|
|
24195
|
+
const modelId = args[1] ?? deps.config.model;
|
|
24196
|
+
if (!providerId || !modelId) {
|
|
24197
|
+
deps.renderer.writeError("Usage: wstack models caps [provider] [model]");
|
|
24198
|
+
deps.renderer.write(color.dim("Defaults to current configured provider/model when omitted.\n"));
|
|
24199
|
+
return 1;
|
|
24200
|
+
}
|
|
24201
|
+
const resolved = await deps.modelsRegistry.getModel(providerId, modelId);
|
|
24202
|
+
if (!resolved) {
|
|
24203
|
+
deps.renderer.writeError("Model not found in catalog: " + providerId + "/" + modelId);
|
|
24204
|
+
deps.renderer.write(color.dim("Run `wstack models refresh` or add a custom model if this is expected.\n"));
|
|
24205
|
+
return 1;
|
|
24206
|
+
}
|
|
24207
|
+
const caps = resolved.capabilities;
|
|
24208
|
+
const flags = [
|
|
24209
|
+
caps.tools ? "tools" : void 0,
|
|
24210
|
+
caps.vision ? "vision" : void 0,
|
|
24211
|
+
caps.reasoning ? "reasoning" : void 0
|
|
24212
|
+
].filter((v) => v !== void 0);
|
|
24213
|
+
const rc = caps.reasoningConfig;
|
|
24214
|
+
const cost = resolved.cost;
|
|
24215
|
+
deps.renderer.write(color.bold("Model capabilities") + " " + color.dim(providerId + "/" + modelId) + "\n");
|
|
24216
|
+
deps.renderer.write(" context: " + (caps.maxContext ? color.yellow(String(caps.maxContext)) : color.dim("?")) + "\n");
|
|
24217
|
+
if (caps.maxOutput !== void 0) {
|
|
24218
|
+
deps.renderer.write(" max output: " + color.yellow(String(caps.maxOutput)) + "\n");
|
|
24219
|
+
}
|
|
24220
|
+
if (caps.knowledge) deps.renderer.write(" knowledge: " + caps.knowledge + "\n");
|
|
24221
|
+
deps.renderer.write(" flags: " + (flags.length > 0 ? flags.join(", ") : color.dim("(none)")) + "\n");
|
|
24222
|
+
deps.renderer.write(" pricing/1M: input " + fmtPrice3(cost?.input) + " output " + fmtPrice3(cost?.output) + " cacheR " + fmtPrice3(cost?.cache_read) + "\n");
|
|
24223
|
+
if (cost?.cache_write !== void 0 || cost?.cache_write_5m !== void 0 || cost?.cache_write_1h !== void 0) {
|
|
24224
|
+
const cacheWrite1h = cost.cache_write_1h ?? (cost.input !== void 0 ? cost.input * 2 : void 0);
|
|
24225
|
+
deps.renderer.write(" cache write: default " + fmtPrice3(cost.cache_write) + " 5m " + fmtPrice3(cost.cache_write_5m ?? cost.cache_write) + " 1h " + fmtPrice3(cacheWrite1h) + "\n");
|
|
24226
|
+
}
|
|
24227
|
+
if (rc) {
|
|
24228
|
+
deps.renderer.write(" reasoning:\n");
|
|
24229
|
+
deps.renderer.write(" default: " + rc.default + "\n");
|
|
24230
|
+
deps.renderer.write(" disable: " + (rc.disableSupported ? "supported" : "unsupported") + "\n");
|
|
24231
|
+
deps.renderer.write(" effort: " + (rc.effortSupported ? rc.effortLevels.join(", ") : "unsupported") + "\n");
|
|
24232
|
+
deps.renderer.write(" preserve: " + rc.preserveThinking + "\n");
|
|
24233
|
+
} else if (caps.reasoning) {
|
|
24234
|
+
deps.renderer.write(color.dim(" reasoning: supported, but no detailed config in catalog\n"));
|
|
24235
|
+
}
|
|
24236
|
+
return 0;
|
|
24237
|
+
}
|
|
22445
24238
|
async function mutateModelsConfig(deps, mutator) {
|
|
22446
24239
|
const vault = deps.vault;
|
|
22447
24240
|
const configPath2 = deps.paths.globalConfig;
|
|
@@ -22491,7 +24284,7 @@ function parseBoolFlag(flags, key) {
|
|
|
22491
24284
|
}
|
|
22492
24285
|
async function modelsAdd(args, deps) {
|
|
22493
24286
|
const flags = parseFlags2(args);
|
|
22494
|
-
const pos =
|
|
24287
|
+
const pos = positionals2(args);
|
|
22495
24288
|
const modelId = pos[0];
|
|
22496
24289
|
if (!modelId) {
|
|
22497
24290
|
deps.renderer.writeError(
|
|
@@ -22831,13 +24624,13 @@ async function listFleetRuns(deps) {
|
|
|
22831
24624
|
const runs = [];
|
|
22832
24625
|
for (const id of entries) {
|
|
22833
24626
|
const runDir = path4.join(deps.paths.projectSessions, id);
|
|
22834
|
-
let
|
|
24627
|
+
let stat8;
|
|
22835
24628
|
try {
|
|
22836
|
-
|
|
24629
|
+
stat8 = await fsp5.stat(runDir);
|
|
22837
24630
|
} catch {
|
|
22838
24631
|
continue;
|
|
22839
24632
|
}
|
|
22840
|
-
if (!
|
|
24633
|
+
if (!stat8.isDirectory()) continue;
|
|
22841
24634
|
let manifest = false;
|
|
22842
24635
|
let checkpoint = false;
|
|
22843
24636
|
let subagentCount = 0;
|
|
@@ -22881,15 +24674,15 @@ async function listFleetRuns(deps) {
|
|
|
22881
24674
|
}
|
|
22882
24675
|
async function showFleetRun(runId, deps) {
|
|
22883
24676
|
const runDir = path4.join(deps.paths.projectSessions, runId);
|
|
22884
|
-
let
|
|
24677
|
+
let stat8;
|
|
22885
24678
|
try {
|
|
22886
|
-
|
|
24679
|
+
stat8 = await fsp5.stat(runDir);
|
|
22887
24680
|
} catch {
|
|
22888
24681
|
deps.renderer.writeError(`Fleet run not found: ${runId}
|
|
22889
24682
|
`);
|
|
22890
24683
|
return 1;
|
|
22891
24684
|
}
|
|
22892
|
-
if (!
|
|
24685
|
+
if (!stat8.isDirectory()) {
|
|
22893
24686
|
deps.renderer.writeError(`Not a directory: ${runId}
|
|
22894
24687
|
`);
|
|
22895
24688
|
return 1;
|
|
@@ -23175,7 +24968,7 @@ var updateCmd = async (args, deps) => {
|
|
|
23175
24968
|
deps.renderer.write(`Updating wrongstack from v${info.current} to v${info.latest}...
|
|
23176
24969
|
`);
|
|
23177
24970
|
try {
|
|
23178
|
-
const result = await new Promise((
|
|
24971
|
+
const result = await new Promise((resolve12, reject) => {
|
|
23179
24972
|
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
23180
24973
|
const child = spawn(npmCommand, ["install", "-g", "wrongstack@latest"], {
|
|
23181
24974
|
cwd,
|
|
@@ -23188,7 +24981,7 @@ var updateCmd = async (args, deps) => {
|
|
|
23188
24981
|
_stderr += d;
|
|
23189
24982
|
});
|
|
23190
24983
|
child.on("error", reject);
|
|
23191
|
-
child.on("close", (code) =>
|
|
24984
|
+
child.on("close", (code) => resolve12({ code: code ?? 0 }));
|
|
23192
24985
|
});
|
|
23193
24986
|
if (result.code === 0) {
|
|
23194
24987
|
deps.renderer.write(
|
|
@@ -23300,7 +25093,8 @@ var subcommands = {
|
|
|
23300
25093
|
projects: projectsCmd,
|
|
23301
25094
|
modeldiag: modeldiagCmd,
|
|
23302
25095
|
quick: quickCmd,
|
|
23303
|
-
bench: benchCmd
|
|
25096
|
+
bench: benchCmd,
|
|
25097
|
+
hq: hqCmd
|
|
23304
25098
|
};
|
|
23305
25099
|
|
|
23306
25100
|
// src/boot.ts
|
|
@@ -23674,7 +25468,7 @@ async function checkGitInCwd(opts) {
|
|
|
23674
25468
|
if (answer === "y" || answer === "yes") {
|
|
23675
25469
|
try {
|
|
23676
25470
|
const { spawn: spawn9 } = await import('child_process');
|
|
23677
|
-
await new Promise((
|
|
25471
|
+
await new Promise((resolve12, reject) => {
|
|
23678
25472
|
const child = spawn9("git", ["init"], {
|
|
23679
25473
|
cwd,
|
|
23680
25474
|
signal: AbortSignal.timeout(1e4),
|
|
@@ -23683,7 +25477,7 @@ async function checkGitInCwd(opts) {
|
|
|
23683
25477
|
child.on("error", reject);
|
|
23684
25478
|
child.on(
|
|
23685
25479
|
"close",
|
|
23686
|
-
(code) => code === 0 ?
|
|
25480
|
+
(code) => code === 0 ? resolve12() : reject(new Error(`git init failed with ${code}`))
|
|
23687
25481
|
);
|
|
23688
25482
|
});
|
|
23689
25483
|
renderer.write(` ${color.green("\u2713")} Git repository initialized
|
|
@@ -24434,7 +26228,9 @@ async function runRepl(opts) {
|
|
|
24434
26228
|
const replProjectRoot = opts.projectRoot ?? process.cwd();
|
|
24435
26229
|
const projectDir = resolveProjectDir(replProjectRoot, wstackGlobalRoot());
|
|
24436
26230
|
const clientId = `repl@${crypto3.randomUUID().slice(0, 8)}`;
|
|
24437
|
-
const
|
|
26231
|
+
const hqPublisher = createHqPublisherFromEnv({ clientKind: "repl", projectRoot: replProjectRoot, projectName: path4.basename(replProjectRoot), appConfig: opts.appConfig });
|
|
26232
|
+
hqPublisher?.connect();
|
|
26233
|
+
const clientMailbox = new GlobalMailbox(projectDir, void 0, hqPublisher);
|
|
24438
26234
|
let clientHeartbeat;
|
|
24439
26235
|
clientMailbox.registerClient({
|
|
24440
26236
|
clientId,
|
|
@@ -24507,7 +26303,7 @@ async function runRepl(opts) {
|
|
|
24507
26303
|
`[eternal] ${toErrorMessage(err)}`
|
|
24508
26304
|
);
|
|
24509
26305
|
}
|
|
24510
|
-
await new Promise((
|
|
26306
|
+
await new Promise((resolve12) => setTimeout(resolve12, 250));
|
|
24511
26307
|
continue;
|
|
24512
26308
|
}
|
|
24513
26309
|
} else if (opts.getAutonomy?.() === "eternal-parallel") {
|
|
@@ -24583,7 +26379,7 @@ async function runRepl(opts) {
|
|
|
24583
26379
|
`[parallel] ${toErrorMessage(err)}`
|
|
24584
26380
|
);
|
|
24585
26381
|
}
|
|
24586
|
-
await new Promise((
|
|
26382
|
+
await new Promise((resolve12) => setTimeout(resolve12, 250));
|
|
24587
26383
|
continue;
|
|
24588
26384
|
}
|
|
24589
26385
|
}
|
|
@@ -25150,12 +26946,12 @@ ${color.cyan("\u23F3 Auto")} ${color.dim("(Ctrl+C to cancel)")}
|
|
|
25150
26946
|
let interval;
|
|
25151
26947
|
let lastTickedSecond = sec + 1;
|
|
25152
26948
|
let onAbort;
|
|
25153
|
-
return new Promise((
|
|
25154
|
-
onAbort = () =>
|
|
26949
|
+
return new Promise((resolve12) => {
|
|
26950
|
+
onAbort = () => resolve12(false);
|
|
25155
26951
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
25156
26952
|
interval = setInterval(() => {
|
|
25157
26953
|
if (signal.aborted) {
|
|
25158
|
-
|
|
26954
|
+
resolve12(false);
|
|
25159
26955
|
return;
|
|
25160
26956
|
}
|
|
25161
26957
|
const elapsed = Date.now() - start;
|
|
@@ -25163,7 +26959,7 @@ ${color.cyan("\u23F3 Auto")} ${color.dim("(Ctrl+C to cancel)")}
|
|
|
25163
26959
|
if (remaining <= 0) {
|
|
25164
26960
|
opts.renderer.write(color.dim(` \u21B3 ${truncated}
|
|
25165
26961
|
`));
|
|
25166
|
-
|
|
26962
|
+
resolve12(true);
|
|
25167
26963
|
return;
|
|
25168
26964
|
}
|
|
25169
26965
|
if (opts.onCountdownTick && remaining !== lastTickedSecond) {
|
|
@@ -25174,7 +26970,7 @@ ${color.cyan("\u23F3 Auto")} ${color.dim("(Ctrl+C to cancel)")}
|
|
|
25174
26970
|
opts.renderer.write(
|
|
25175
26971
|
color.yellow(" \u21B3 Countdown cancelled \u2014 switching to manual mode\n")
|
|
25176
26972
|
);
|
|
25177
|
-
|
|
26973
|
+
resolve12(false);
|
|
25178
26974
|
return;
|
|
25179
26975
|
}
|
|
25180
26976
|
} catch {
|
|
@@ -25271,10 +27067,10 @@ async function execute(deps) {
|
|
|
25271
27067
|
reader,
|
|
25272
27068
|
session,
|
|
25273
27069
|
mcpRegistry,
|
|
25274
|
-
recoveryLock,
|
|
25275
|
-
wpaths,
|
|
27070
|
+
recoveryLock: initialRecoveryLock,
|
|
27071
|
+
wpaths: initialWpaths,
|
|
25276
27072
|
modelsRegistry,
|
|
25277
|
-
projectRoot,
|
|
27073
|
+
projectRoot: initialProjectRoot,
|
|
25278
27074
|
flags,
|
|
25279
27075
|
positional,
|
|
25280
27076
|
effectiveMaxContext,
|
|
@@ -25288,6 +27084,8 @@ async function execute(deps) {
|
|
|
25288
27084
|
getPickableProviders,
|
|
25289
27085
|
switchProviderAndModel,
|
|
25290
27086
|
director,
|
|
27087
|
+
getDirector,
|
|
27088
|
+
coordinatorController,
|
|
25291
27089
|
fleetRoster,
|
|
25292
27090
|
fleetStreamController,
|
|
25293
27091
|
interruptController,
|
|
@@ -25324,6 +27122,11 @@ async function execute(deps) {
|
|
|
25324
27122
|
restoredToolCalls,
|
|
25325
27123
|
needsSetup
|
|
25326
27124
|
} = deps;
|
|
27125
|
+
let wpaths = initialWpaths;
|
|
27126
|
+
let projectRoot = initialProjectRoot;
|
|
27127
|
+
let activeSessionStore = sessionStore;
|
|
27128
|
+
let activeRecoveryLock = initialRecoveryLock;
|
|
27129
|
+
let detachActiveTodosCheckpoint = detachTodosCheckpoint;
|
|
25327
27130
|
const rootTraceId = context.traceId;
|
|
25328
27131
|
const storageLog = (event, payload) => {
|
|
25329
27132
|
const traceId = payload.traceId ?? rootTraceId;
|
|
@@ -25372,8 +27175,8 @@ async function execute(deps) {
|
|
|
25372
27175
|
timeoutMs: 3e5
|
|
25373
27176
|
};
|
|
25374
27177
|
const subagentId = await dir.spawn(cfg);
|
|
25375
|
-
const { randomUUID:
|
|
25376
|
-
const taskId =
|
|
27178
|
+
const { randomUUID: randomUUID8 } = await import('crypto');
|
|
27179
|
+
const taskId = randomUUID8();
|
|
25377
27180
|
await dir.assign({
|
|
25378
27181
|
id: taskId,
|
|
25379
27182
|
description: taskDesc,
|
|
@@ -25588,8 +27391,11 @@ async function execute(deps) {
|
|
|
25588
27391
|
let pendingProjectSwitch = null;
|
|
25589
27392
|
const coordinatorEvents = /* @__PURE__ */ new Set();
|
|
25590
27393
|
let autonomousCoordinator = null;
|
|
25591
|
-
|
|
25592
|
-
|
|
27394
|
+
let coordinatorRun = null;
|
|
27395
|
+
const ensureAutonomousCoordinator = () => {
|
|
27396
|
+
if (autonomousCoordinator) return autonomousCoordinator;
|
|
27397
|
+
const currentDirector = getDirector?.() ?? director;
|
|
27398
|
+
if (!currentDirector) return null;
|
|
25593
27399
|
const transcript = context.session.transcriptPath;
|
|
25594
27400
|
const sessionDir = transcript ? path4.dirname(transcript) : wpaths.projectDir;
|
|
25595
27401
|
const llmProvider = {
|
|
@@ -25604,7 +27410,7 @@ async function execute(deps) {
|
|
|
25604
27410
|
type: "text",
|
|
25605
27411
|
text: `Decision: ${prompt.question}
|
|
25606
27412
|
|
|
25607
|
-
Context: ${prompt.context}
|
|
27413
|
+
Context: ${JSON.stringify(prompt.context)}
|
|
25608
27414
|
|
|
25609
27415
|
Options:
|
|
25610
27416
|
${prompt.options.map((o, i) => ` ${i + 1}. [${o.id}] ${o.label}${o.consequence ? ` \u2014 ${o.consequence}` : ""}`).join("\n")}
|
|
@@ -25641,7 +27447,9 @@ Reply with ONLY the JSON object.`
|
|
|
25641
27447
|
};
|
|
25642
27448
|
autonomousCoordinator = new AutonomousCoordinator({
|
|
25643
27449
|
sessionDir,
|
|
25644
|
-
fleet:
|
|
27450
|
+
fleet: currentDirector.fleet,
|
|
27451
|
+
fleetManager: currentDirector.fleetManager,
|
|
27452
|
+
director: currentDirector,
|
|
25645
27453
|
mailbox,
|
|
25646
27454
|
selfAgentId: `leader@${context.session.id ?? "unknown"}`,
|
|
25647
27455
|
selfAgentName: "Leader",
|
|
@@ -25650,16 +27458,168 @@ Reply with ONLY the JSON object.`
|
|
|
25650
27458
|
for (const fn of coordinatorEvents) fn(event);
|
|
25651
27459
|
}
|
|
25652
27460
|
});
|
|
25653
|
-
deps.onCoordinatorStop = () => autonomousCoordinator?.
|
|
27461
|
+
deps.onCoordinatorStop = () => autonomousCoordinator?.dispose();
|
|
27462
|
+
if (coordinatorController) {
|
|
27463
|
+
coordinatorController["onCoordinatorStart"] = (goal) => {
|
|
27464
|
+
const coordinator = autonomousCoordinator;
|
|
27465
|
+
if (!coordinator) return;
|
|
27466
|
+
coordinator.run({ goal: goal ?? "Improve the codebase", runUntilComplete: true }).then(() => void 0).catch((err) => console.error("[coordinator] run() failed:", err));
|
|
27467
|
+
};
|
|
27468
|
+
coordinatorController["onCoordinatorStop"] = () => autonomousCoordinator?.stop();
|
|
27469
|
+
coordinatorController["onCoordinatorTasks"] = async () => {
|
|
27470
|
+
if (!autonomousCoordinator) return null;
|
|
27471
|
+
await autonomousCoordinator.graph.load();
|
|
27472
|
+
return autonomousCoordinator.auction.getPendingTasks().map((task) => ({ id: task.id, title: task.title, priority: task.priority, tags: task.tags }));
|
|
27473
|
+
};
|
|
27474
|
+
coordinatorController["onCoordinatorClaim"] = async (taskId) => {
|
|
27475
|
+
if (!autonomousCoordinator) return "No coordinator is active.";
|
|
27476
|
+
await autonomousCoordinator.graph.load();
|
|
27477
|
+
const goal = autonomousCoordinator.graph.get(taskId);
|
|
27478
|
+
if (!goal || goal.type !== "goal") return `Task ${taskId.slice(0, 8)} not found.`;
|
|
27479
|
+
if (goal.status !== "pending") return `Task ${taskId.slice(0, 8)} is ${goal.status}, not claimable.`;
|
|
27480
|
+
const ok = await autonomousCoordinator.auction.claim(
|
|
27481
|
+
taskId,
|
|
27482
|
+
`terminal@${context.session.id ?? "unknown"}`,
|
|
27483
|
+
"Terminal worker"
|
|
27484
|
+
);
|
|
27485
|
+
if (!ok) return `Task ${taskId.slice(0, 8)} could not be claimed.`;
|
|
27486
|
+
return { description: goal.description };
|
|
27487
|
+
};
|
|
27488
|
+
coordinatorController["onCoordinatorComplete"] = async (taskId, result) => {
|
|
27489
|
+
if (!autonomousCoordinator) return "No coordinator is active.";
|
|
27490
|
+
await autonomousCoordinator.graph.load();
|
|
27491
|
+
const goal = autonomousCoordinator.graph.get(taskId);
|
|
27492
|
+
if (!goal || goal.type !== "goal") return `Task ${taskId.slice(0, 8)} not found.`;
|
|
27493
|
+
if (goal.status !== "in_progress") return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot complete.`;
|
|
27494
|
+
await autonomousCoordinator.reportTaskCompletion(taskId, result ?? "Terminal worker completed the task");
|
|
27495
|
+
return null;
|
|
27496
|
+
};
|
|
27497
|
+
coordinatorController["onCoordinatorFail"] = async (taskId, error) => {
|
|
27498
|
+
if (!autonomousCoordinator) return "No coordinator is active.";
|
|
27499
|
+
await autonomousCoordinator.graph.load();
|
|
27500
|
+
const goal = autonomousCoordinator.graph.get(taskId);
|
|
27501
|
+
if (!goal || goal.type !== "goal") return `Task ${taskId.slice(0, 8)} not found.`;
|
|
27502
|
+
if (goal.status !== "in_progress") return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot fail.`;
|
|
27503
|
+
await autonomousCoordinator.reportTaskFailure(taskId, error);
|
|
27504
|
+
return null;
|
|
27505
|
+
};
|
|
27506
|
+
coordinatorController["onCoordinatorStatus"] = async () => {
|
|
27507
|
+
if (!autonomousCoordinator) return null;
|
|
27508
|
+
await autonomousCoordinator.syncFromGraph();
|
|
27509
|
+
const stats2 = autonomousCoordinator.getStats();
|
|
27510
|
+
return {
|
|
27511
|
+
goals: { total: stats2.goals.total, done: stats2.goals.done, pending: stats2.goals.pending, failed: stats2.goals.failed },
|
|
27512
|
+
dag: { running: stats2.dag.running, ready: stats2.dag.ready, done: stats2.dag.done, failed: stats2.dag.failed },
|
|
27513
|
+
auction: { pending: stats2.auction.pending, inProgress: stats2.auction.in_progress }
|
|
27514
|
+
};
|
|
27515
|
+
};
|
|
27516
|
+
}
|
|
27517
|
+
return autonomousCoordinator;
|
|
25654
27518
|
};
|
|
25655
|
-
if (director)
|
|
27519
|
+
if (director) ensureAutonomousCoordinator();
|
|
25656
27520
|
const offDirectorSpawned = events.onPattern("subagent.spawned", () => {
|
|
25657
|
-
|
|
25658
|
-
if (dir) {
|
|
25659
|
-
offDirectorSpawned();
|
|
25660
|
-
onDirectorReady(dir);
|
|
25661
|
-
}
|
|
27521
|
+
if (ensureAutonomousCoordinator()) offDirectorSpawned();
|
|
25662
27522
|
});
|
|
27523
|
+
const switchProjectInPlace = async (targetRoot, displayName) => {
|
|
27524
|
+
const resolved = path4.resolve(targetRoot);
|
|
27525
|
+
const stat8 = await fsp5.stat(resolved).catch(() => null);
|
|
27526
|
+
if (!stat8?.isDirectory()) return `Cannot switch: not a directory: ${resolved}`;
|
|
27527
|
+
const oldWriter = context.session;
|
|
27528
|
+
const oldUsage = tokenCounter.total();
|
|
27529
|
+
const oldRecoveryLock = activeRecoveryLock;
|
|
27530
|
+
const oldProjectRoot = projectRoot;
|
|
27531
|
+
const nextWpaths = resolveWstackPaths({ projectRoot: resolved, globalRoot: wpaths.globalRoot });
|
|
27532
|
+
await fsp5.mkdir(nextWpaths.projectSessions, { recursive: true });
|
|
27533
|
+
const nextSessionStore = new DefaultSessionStore({ dir: nextWpaths.projectSessions });
|
|
27534
|
+
const nextWriter = await nextSessionStore.create({
|
|
27535
|
+
id: "",
|
|
27536
|
+
title: "",
|
|
27537
|
+
model: context.model,
|
|
27538
|
+
provider: context.provider.id ?? config.provider
|
|
27539
|
+
});
|
|
27540
|
+
detachActiveTodosCheckpoint?.();
|
|
27541
|
+
process.chdir(resolved);
|
|
27542
|
+
projectRoot = resolved;
|
|
27543
|
+
wpaths = nextWpaths;
|
|
27544
|
+
activeSessionStore = nextSessionStore;
|
|
27545
|
+
activeRecoveryLock = new RecoveryLock({ dir: nextWpaths.projectSessions, sessionStore: nextSessionStore });
|
|
27546
|
+
context.cwd = resolved;
|
|
27547
|
+
context.projectRoot = resolved;
|
|
27548
|
+
context.workingDir = resolved;
|
|
27549
|
+
context.session = nextWriter;
|
|
27550
|
+
context.state.replaceMessages([]);
|
|
27551
|
+
context.state.replaceTodos([]);
|
|
27552
|
+
context.clearFileTracking();
|
|
27553
|
+
context.tokenCounter.reset();
|
|
27554
|
+
context.meta["packageTrackerOpts"] = { storageDir: nextWpaths.projectDir, projectRoot: resolved };
|
|
27555
|
+
context.state.setMeta("plan.path", path4.join(nextWpaths.projectSessions, `${nextWriter.id}.plan.json`));
|
|
27556
|
+
context.state.setMeta("task.path", path4.join(nextWpaths.projectSessions, `${nextWriter.id}.tasks.json`));
|
|
27557
|
+
detachActiveTodosCheckpoint = attachTodosCheckpoint(
|
|
27558
|
+
context.state,
|
|
27559
|
+
path4.join(nextWpaths.projectSessions, `${nextWriter.id}.todos.json`),
|
|
27560
|
+
nextWriter.id,
|
|
27561
|
+
events,
|
|
27562
|
+
context.traceId
|
|
27563
|
+
);
|
|
27564
|
+
setQueuedMessagesSnapshot(context, []);
|
|
27565
|
+
try {
|
|
27566
|
+
const switchMode = modeId && modeId !== "default" && modeStore ? await modeStore.getMode(modeId) : void 0;
|
|
27567
|
+
const switchBuilder = new DefaultSystemPromptBuilder({
|
|
27568
|
+
memoryStore: memoryStore ?? void 0,
|
|
27569
|
+
skillLoader,
|
|
27570
|
+
modeStore,
|
|
27571
|
+
modeId: modeId ?? "default",
|
|
27572
|
+
modePrompt: switchMode?.prompt ?? ""
|
|
27573
|
+
});
|
|
27574
|
+
context.systemPrompt = await switchBuilder.build({
|
|
27575
|
+
cwd: resolved,
|
|
27576
|
+
projectRoot: resolved,
|
|
27577
|
+
tools: agent.tools.list(),
|
|
27578
|
+
provider: context.provider.id,
|
|
27579
|
+
model: context.model
|
|
27580
|
+
});
|
|
27581
|
+
} catch (err) {
|
|
27582
|
+
console.error(
|
|
27583
|
+
JSON.stringify({
|
|
27584
|
+
level: "warn",
|
|
27585
|
+
event: "execution.project_switch_prompt_rebuild_failed",
|
|
27586
|
+
message: err instanceof Error ? err.message : String(err),
|
|
27587
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
27588
|
+
})
|
|
27589
|
+
);
|
|
27590
|
+
}
|
|
27591
|
+
void (async () => {
|
|
27592
|
+
try {
|
|
27593
|
+
await oldWriter.append({ type: "session_end", ts: (/* @__PURE__ */ new Date()).toISOString(), usage: oldUsage });
|
|
27594
|
+
await oldWriter.close();
|
|
27595
|
+
} catch (err) {
|
|
27596
|
+
console.error(
|
|
27597
|
+
JSON.stringify({
|
|
27598
|
+
level: "warn",
|
|
27599
|
+
event: "execution.project_switch_old_session_close_failed",
|
|
27600
|
+
message: err instanceof Error ? err.message : String(err),
|
|
27601
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
27602
|
+
})
|
|
27603
|
+
);
|
|
27604
|
+
}
|
|
27605
|
+
await oldRecoveryLock.clear().catch(() => void 0);
|
|
27606
|
+
})();
|
|
27607
|
+
try {
|
|
27608
|
+
await activeRecoveryLock.write(nextWriter.id);
|
|
27609
|
+
} catch (err) {
|
|
27610
|
+
console.error(
|
|
27611
|
+
JSON.stringify({
|
|
27612
|
+
level: "error",
|
|
27613
|
+
event: "execution.project_switch_recovery_lock_failed",
|
|
27614
|
+
message: err instanceof Error ? err.message : String(err),
|
|
27615
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
27616
|
+
})
|
|
27617
|
+
);
|
|
27618
|
+
}
|
|
27619
|
+
const emitUntyped = events.emit;
|
|
27620
|
+
emitUntyped("project.switched", { from: oldProjectRoot, to: resolved, name: displayName });
|
|
27621
|
+
return null;
|
|
27622
|
+
};
|
|
25663
27623
|
try {
|
|
25664
27624
|
code = await runTui({
|
|
25665
27625
|
agent,
|
|
@@ -25729,6 +27689,9 @@ Reply with ONLY the JSON object.`
|
|
|
25729
27689
|
const autonomy = cfg.autonomy;
|
|
25730
27690
|
const rawMode = autonomy?.defaultMode;
|
|
25731
27691
|
const mode = rawMode === "suggest" || rawMode === "auto" ? rawMode : "off";
|
|
27692
|
+
const modelRuntime = cfg.modelRuntime;
|
|
27693
|
+
const reasoningEffortRaw = modelRuntime?.reasoning?.effort;
|
|
27694
|
+
const reasoningEffort = reasoningEffortRaw === "none" || reasoningEffortRaw === "minimal" || reasoningEffortRaw === "low" || reasoningEffortRaw === "medium" || reasoningEffortRaw === "high" || reasoningEffortRaw === "xhigh" || reasoningEffortRaw === "max" ? reasoningEffortRaw : "high";
|
|
25732
27695
|
return {
|
|
25733
27696
|
mode,
|
|
25734
27697
|
delayMs: autonomy?.autoProceedDelayMs ?? 45e3,
|
|
@@ -25759,12 +27722,17 @@ Reply with ONLY the JSON object.`
|
|
|
25759
27722
|
restrictFsToRoot: cfg.tools?.restrictToProjectRoot ?? false,
|
|
25760
27723
|
autoProceedMaxIterations: cfg.autonomy?.autoProceedMaxIterations ?? 50,
|
|
25761
27724
|
debugStream: cfg.debugStream ?? false,
|
|
27725
|
+
statuslineMode: autonomy?.statuslineMode === "minimum" ? "minimum" : "detailed",
|
|
25762
27726
|
configScope: cfg.configScope ?? "global",
|
|
25763
27727
|
enhanceDelayMs: cfg.autonomy?.enhanceDelayMs ?? 6e4,
|
|
25764
27728
|
enhanceEnabled: cfg.autonomy?.enhance ?? true,
|
|
25765
27729
|
enhanceLanguage: cfg.autonomy?.enhanceLanguage === "english" ? "english" : "original",
|
|
25766
27730
|
mouseMode: autonomy?.mouseMode ?? false,
|
|
25767
27731
|
autonomyNextPrompt: cfg.autonomy?.autonomyNextPrompt ?? "auto {{suggestion}}",
|
|
27732
|
+
reasoningMode: modelRuntime?.reasoning?.mode === "on" || modelRuntime?.reasoning?.mode === "off" ? modelRuntime.reasoning.mode : "auto",
|
|
27733
|
+
reasoningEffort,
|
|
27734
|
+
reasoningPreserve: modelRuntime?.reasoning?.preserve === true,
|
|
27735
|
+
cacheTtl: modelRuntime?.cache?.ttl === "5m" || modelRuntime?.cache?.ttl === "1h" ? modelRuntime.cache.ttl : "default",
|
|
25768
27736
|
breakerEnabled: cfg.circuitBreaker?.enabled === true,
|
|
25769
27737
|
breakerAutoKillResetMs: cfg.circuitBreaker?.autoKillResetMs ?? 6e4
|
|
25770
27738
|
};
|
|
@@ -25790,6 +27758,7 @@ Reply with ONLY the JSON object.`
|
|
|
25790
27758
|
if (s.mouseMode !== void 0) a["mouseMode"] = s.mouseMode;
|
|
25791
27759
|
if (s.enhanceEnabled !== void 0) a["enhance"] = s.enhanceEnabled;
|
|
25792
27760
|
if (s.enhanceLanguage !== void 0) a["enhanceLanguage"] = s.enhanceLanguage;
|
|
27761
|
+
if (s.statuslineMode !== void 0) a["statuslineMode"] = s.statuslineMode;
|
|
25793
27762
|
if (s.autonomyNextPrompt !== void 0) a["autonomyNextPrompt"] = s.autonomyNextPrompt;
|
|
25794
27763
|
if (s.autoProceedMaxIterations !== void 0)
|
|
25795
27764
|
a["autoProceedMaxIterations"] = s.autoProceedMaxIterations;
|
|
@@ -25945,7 +27914,7 @@ Reply with ONLY the JSON object.`
|
|
|
25945
27914
|
// The coordinator tracks goals, tasks, knowledge, and consensus across all
|
|
25946
27915
|
// active sessions in the same project. It runs independently of the leader
|
|
25947
27916
|
// agent and is accessible to any session in the project via the GlobalMailbox.
|
|
25948
|
-
getAutonomousCoordinator: () =>
|
|
27917
|
+
getAutonomousCoordinator: () => ensureAutonomousCoordinator(),
|
|
25949
27918
|
subscribeCoordinatorEvents: (fn) => {
|
|
25950
27919
|
coordinatorEvents.add(fn);
|
|
25951
27920
|
return () => {
|
|
@@ -25953,17 +27922,105 @@ Reply with ONLY the JSON object.`
|
|
|
25953
27922
|
};
|
|
25954
27923
|
},
|
|
25955
27924
|
onCoordinatorStart: (goal) => {
|
|
25956
|
-
|
|
25957
|
-
|
|
27925
|
+
const coordinator = ensureAutonomousCoordinator();
|
|
27926
|
+
if (!coordinator) {
|
|
27927
|
+
console.error("[coordinator] not ready \u2014 no director available");
|
|
25958
27928
|
return;
|
|
25959
27929
|
}
|
|
25960
|
-
|
|
27930
|
+
if (coordinatorRun) return;
|
|
27931
|
+
coordinatorRun = coordinator.run({ goal: goal ?? "Improve the codebase", runUntilComplete: true }).then(() => void 0).catch((err) => {
|
|
25961
27932
|
console.error("[coordinator] run() failed:", err);
|
|
27933
|
+
}).finally(() => {
|
|
27934
|
+
coordinatorRun = null;
|
|
25962
27935
|
});
|
|
25963
27936
|
},
|
|
25964
27937
|
onCoordinatorStop: () => {
|
|
25965
27938
|
autonomousCoordinator?.stop();
|
|
25966
27939
|
},
|
|
27940
|
+
onCoordinatorTasks: async () => {
|
|
27941
|
+
const coordinator = ensureAutonomousCoordinator();
|
|
27942
|
+
if (!coordinator) return null;
|
|
27943
|
+
await coordinator.graph.load();
|
|
27944
|
+
return coordinator.auction.getPendingTasks().map((task) => ({
|
|
27945
|
+
id: task.id,
|
|
27946
|
+
title: task.title,
|
|
27947
|
+
priority: task.priority,
|
|
27948
|
+
tags: task.tags
|
|
27949
|
+
}));
|
|
27950
|
+
},
|
|
27951
|
+
onCoordinatorClaim: async (taskId) => {
|
|
27952
|
+
const coordinator = ensureAutonomousCoordinator();
|
|
27953
|
+
if (!coordinator) return "No coordinator is active.";
|
|
27954
|
+
await coordinator.graph.load();
|
|
27955
|
+
const goal = coordinator.graph.get(taskId);
|
|
27956
|
+
if (!goal || goal.type !== "goal") {
|
|
27957
|
+
return `Task ${taskId.slice(0, 8)} not found in the coordinator graph.`;
|
|
27958
|
+
}
|
|
27959
|
+
if (goal.status !== "pending") {
|
|
27960
|
+
return `Task ${taskId.slice(0, 8)} is ${goal.status}, not claimable.`;
|
|
27961
|
+
}
|
|
27962
|
+
const ok = await coordinator.auction.claim(
|
|
27963
|
+
taskId,
|
|
27964
|
+
`terminal@${context.session.id ?? "unknown"}`,
|
|
27965
|
+
"Terminal worker"
|
|
27966
|
+
);
|
|
27967
|
+
if (!ok) {
|
|
27968
|
+
return `Task ${taskId.slice(0, 8)} could not be claimed (status changed?).`;
|
|
27969
|
+
}
|
|
27970
|
+
return { description: goal.description };
|
|
27971
|
+
},
|
|
27972
|
+
onCoordinatorComplete: async (taskId, result) => {
|
|
27973
|
+
const coordinator = ensureAutonomousCoordinator();
|
|
27974
|
+
if (!coordinator) return "No coordinator is active.";
|
|
27975
|
+
await coordinator.graph.load();
|
|
27976
|
+
const goal = coordinator.graph.get(taskId);
|
|
27977
|
+
if (!goal || goal.type !== "goal") {
|
|
27978
|
+
return `Task ${taskId.slice(0, 8)} not found in the coordinator graph.`;
|
|
27979
|
+
}
|
|
27980
|
+
if (goal.status !== "in_progress") {
|
|
27981
|
+
return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot complete.`;
|
|
27982
|
+
}
|
|
27983
|
+
await coordinator.reportTaskCompletion(taskId, result ?? "Terminal worker completed the task");
|
|
27984
|
+
return null;
|
|
27985
|
+
},
|
|
27986
|
+
onCoordinatorFail: async (taskId, error) => {
|
|
27987
|
+
const coordinator = ensureAutonomousCoordinator();
|
|
27988
|
+
if (!coordinator) return "No coordinator is active.";
|
|
27989
|
+
await coordinator.graph.load();
|
|
27990
|
+
const goal = coordinator.graph.get(taskId);
|
|
27991
|
+
if (!goal || goal.type !== "goal") {
|
|
27992
|
+
return `Task ${taskId.slice(0, 8)} not found in the coordinator graph.`;
|
|
27993
|
+
}
|
|
27994
|
+
if (goal.status !== "in_progress") {
|
|
27995
|
+
return `Task ${taskId.slice(0, 8)} is ${goal.status}, cannot fail.`;
|
|
27996
|
+
}
|
|
27997
|
+
await coordinator.reportTaskFailure(taskId, error);
|
|
27998
|
+
return null;
|
|
27999
|
+
},
|
|
28000
|
+
onCoordinatorStatus: async () => {
|
|
28001
|
+
const coordinator = ensureAutonomousCoordinator();
|
|
28002
|
+
if (!coordinator) return null;
|
|
28003
|
+
await coordinator.syncFromGraph();
|
|
28004
|
+
const stats2 = coordinator.getStats();
|
|
28005
|
+
return {
|
|
28006
|
+
goals: {
|
|
28007
|
+
total: stats2.goals.total,
|
|
28008
|
+
done: stats2.goals.done,
|
|
28009
|
+
pending: stats2.goals.pending,
|
|
28010
|
+
failed: stats2.goals.failed
|
|
28011
|
+
},
|
|
28012
|
+
dag: {
|
|
28013
|
+
running: stats2.dag.running,
|
|
28014
|
+
ready: stats2.dag.ready,
|
|
28015
|
+
done: stats2.dag.done,
|
|
28016
|
+
failed: stats2.dag.failed
|
|
28017
|
+
},
|
|
28018
|
+
auction: {
|
|
28019
|
+
pending: stats2.auction.pending,
|
|
28020
|
+
inProgress: stats2.auction.in_progress
|
|
28021
|
+
}
|
|
28022
|
+
};
|
|
28023
|
+
},
|
|
25967
28024
|
// /clear: signal the TUI to wipe entries and reset fleet/leader stats
|
|
25968
28025
|
// AND bump the context chip version — so the display reflects a
|
|
25969
28026
|
// completely fresh session after the backend has been cleared.
|
|
@@ -26013,6 +28070,7 @@ Reply with ONLY the JSON object.`
|
|
|
26013
28070
|
initialGoal: goalFlag,
|
|
26014
28071
|
initialAsk: askFlag,
|
|
26015
28072
|
projectRoot,
|
|
28073
|
+
appConfig: config,
|
|
26016
28074
|
getSDDContext: async () => {
|
|
26017
28075
|
const { getActiveSDDContext: getActiveSDDContext2 } = await Promise.resolve().then(() => (init_sdd(), sdd_exports));
|
|
26018
28076
|
return getActiveSDDContext2();
|
|
@@ -26091,9 +28149,9 @@ Reply with ONLY the JSON object.`
|
|
|
26091
28149
|
restoredToolCalls,
|
|
26092
28150
|
// ── Session resume support ──────────────────────────────────
|
|
26093
28151
|
listSessions: async (limit = 20) => {
|
|
26094
|
-
if (!
|
|
26095
|
-
const summaries = await
|
|
26096
|
-
const currentId = session.id;
|
|
28152
|
+
if (!activeSessionStore) return [];
|
|
28153
|
+
const summaries = await activeSessionStore.list(limit);
|
|
28154
|
+
const currentId = agent.ctx.session?.id ?? session.id;
|
|
26097
28155
|
return summaries.map((s) => ({
|
|
26098
28156
|
id: s.id,
|
|
26099
28157
|
title: s.title ?? "",
|
|
@@ -26108,7 +28166,7 @@ Reply with ONLY the JSON object.`
|
|
|
26108
28166
|
}));
|
|
26109
28167
|
},
|
|
26110
28168
|
onResumeSession: async (sessionId) => {
|
|
26111
|
-
if (!
|
|
28169
|
+
if (!activeSessionStore) return null;
|
|
26112
28170
|
try {
|
|
26113
28171
|
const { SessionRegistry } = await import('@wrongstack/core');
|
|
26114
28172
|
const registry = new SessionRegistry(path4.dirname(wpaths.globalConfig));
|
|
@@ -26124,7 +28182,7 @@ Reply with ONLY the JSON object.`
|
|
|
26124
28182
|
if (err instanceof Error && err.message.startsWith("Session is open")) throw err;
|
|
26125
28183
|
}
|
|
26126
28184
|
try {
|
|
26127
|
-
const resumed = await
|
|
28185
|
+
const resumed = await activeSessionStore.resume(sessionId);
|
|
26128
28186
|
const meta = resumed.data.metadata;
|
|
26129
28187
|
agent.ctx.state.replaceMessages(resumed.data.messages);
|
|
26130
28188
|
if (meta.model && meta.model !== agent.ctx.model) {
|
|
@@ -26178,7 +28236,7 @@ Reply with ONLY the JSON object.`
|
|
|
26178
28236
|
tokenCounter.reset();
|
|
26179
28237
|
void (async () => {
|
|
26180
28238
|
try {
|
|
26181
|
-
await
|
|
28239
|
+
await activeRecoveryLock.clear();
|
|
26182
28240
|
} catch (err) {
|
|
26183
28241
|
console.error(
|
|
26184
28242
|
JSON.stringify({
|
|
@@ -26190,7 +28248,7 @@ Reply with ONLY the JSON object.`
|
|
|
26190
28248
|
);
|
|
26191
28249
|
}
|
|
26192
28250
|
try {
|
|
26193
|
-
await
|
|
28251
|
+
await activeRecoveryLock.write(resumed.writer.id);
|
|
26194
28252
|
} catch (err) {
|
|
26195
28253
|
console.error(
|
|
26196
28254
|
JSON.stringify({
|
|
@@ -26238,60 +28296,50 @@ Reply with ONLY the JSON object.`
|
|
|
26238
28296
|
},
|
|
26239
28297
|
/**
|
|
26240
28298
|
* Called when the user selects a project in the picker.
|
|
26241
|
-
*
|
|
26242
|
-
*
|
|
26243
|
-
*
|
|
26244
|
-
* the target directory.
|
|
26245
|
-
* For actions: handled by the slash command path (no-op here).
|
|
28299
|
+
* Re-roots the live TUI process in place: new Context root, fresh
|
|
28300
|
+
* per-project session writer, rebuilt system prompt, and no spawned
|
|
28301
|
+
* replacement process.
|
|
26246
28302
|
*/
|
|
26247
28303
|
onProjectSelect: async (slug, kind) => {
|
|
26248
|
-
if (kind === "action") {
|
|
26249
|
-
if (slug === "new-session") {
|
|
26250
|
-
pendingProjectSwitch = {
|
|
26251
|
-
root: projectRoot,
|
|
26252
|
-
name: path4.basename(projectRoot) || projectRoot
|
|
26253
|
-
};
|
|
26254
|
-
}
|
|
26255
|
-
return;
|
|
26256
|
-
}
|
|
26257
|
-
const { loadManifest: loadManifest2 } = await Promise.resolve().then(() => (init_project_utils(), project_utils_exports));
|
|
26258
28304
|
try {
|
|
28305
|
+
if (kind === "action") {
|
|
28306
|
+
if (slug === "new-session") {
|
|
28307
|
+
const name = path4.basename(projectRoot) || projectRoot;
|
|
28308
|
+
const err2 = await switchProjectInPlace(projectRoot, name);
|
|
28309
|
+
if (err2) renderer.write(color.red(`Project switch failed: ${err2}
|
|
28310
|
+
`));
|
|
28311
|
+
}
|
|
28312
|
+
return;
|
|
28313
|
+
}
|
|
28314
|
+
const { loadManifest: loadManifest2, saveManifest: saveManifest2 } = await Promise.resolve().then(() => (init_project_utils(), project_utils_exports));
|
|
26259
28315
|
const manifest = await loadManifest2(wpaths.globalConfig);
|
|
26260
28316
|
const project = manifest.projects.find((p) => p.slug === slug);
|
|
26261
28317
|
if (!project) return;
|
|
26262
|
-
|
|
28318
|
+
const targetRoot = path4.resolve(project.root);
|
|
28319
|
+
if (path4.resolve(projectRoot) === targetRoot) return;
|
|
26263
28320
|
const fleetStatus = director?.status();
|
|
26264
28321
|
const fleetRunning = fleetStatus?.subagents.filter((a) => a.status === "running").length ?? 0;
|
|
26265
|
-
const
|
|
26266
|
-
const
|
|
26267
|
-
const eternalActive = eternalEngine?.currentState === "running";
|
|
26268
|
-
const parallelActive = parallelEngine?.currentState === "running";
|
|
28322
|
+
const eternalActive = getEternalEngine?.()?.currentState === "running";
|
|
28323
|
+
const parallelActive = getParallelEngine?.()?.currentState === "running";
|
|
26269
28324
|
const hasActiveAgents = fleetRunning > 0 || eternalActive || parallelActive;
|
|
26270
28325
|
if (hasActiveAgents) {
|
|
26271
28326
|
const parts = [
|
|
26272
|
-
color.yellow(
|
|
26273
|
-
"\u26A0 Switching projects exits this wstack \u2014 running agents will stop:"
|
|
26274
|
-
)
|
|
28327
|
+
color.yellow("\u26A0 Switching project in place; active background work is still tied to the previous project:")
|
|
26275
28328
|
];
|
|
26276
|
-
if (fleetRunning > 0) {
|
|
26277
|
-
|
|
26278
|
-
|
|
26279
|
-
if (eternalActive) {
|
|
26280
|
-
parts.push(color.dim(" \u2022 Eternal engine is active"));
|
|
26281
|
-
}
|
|
26282
|
-
if (parallelActive) {
|
|
26283
|
-
parts.push(color.dim(" \u2022 Parallel engine is active"));
|
|
26284
|
-
}
|
|
28329
|
+
if (fleetRunning > 0) parts.push(color.dim(` \u2022 ${fleetRunning} subagent(s) currently running`));
|
|
28330
|
+
if (eternalActive) parts.push(color.dim(" \u2022 Eternal engine is active"));
|
|
28331
|
+
if (parallelActive) parts.push(color.dim(" \u2022 Parallel engine is active"));
|
|
26285
28332
|
parts.push("");
|
|
26286
|
-
parts.push(color.dim(`
|
|
28333
|
+
parts.push(color.dim(` New project: ${project.name}`));
|
|
26287
28334
|
renderer.write(`
|
|
26288
28335
|
${parts.join("\n")}
|
|
26289
28336
|
`);
|
|
26290
28337
|
}
|
|
26291
28338
|
project.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
26292
|
-
const { saveManifest: saveManifest2 } = await Promise.resolve().then(() => (init_project_utils(), project_utils_exports));
|
|
26293
28339
|
await saveManifest2(manifest, wpaths.globalConfig);
|
|
26294
|
-
|
|
28340
|
+
const err = await switchProjectInPlace(targetRoot, project.name);
|
|
28341
|
+
if (err) renderer.write(color.red(`Project switch failed: ${err}
|
|
28342
|
+
`));
|
|
26295
28343
|
} catch (err) {
|
|
26296
28344
|
renderer.write(
|
|
26297
28345
|
color.red(
|
|
@@ -26361,18 +28409,19 @@ ${parts.join("\n")}
|
|
|
26361
28409
|
session,
|
|
26362
28410
|
port: Number.parseInt(String(flags.port ?? "3457"), 10),
|
|
26363
28411
|
projectRoot,
|
|
28412
|
+
appConfig: config,
|
|
26364
28413
|
open: !!flags.open,
|
|
26365
28414
|
modelsRegistry,
|
|
26366
28415
|
globalConfigPath: wpaths.globalConfig,
|
|
26367
28416
|
mcpRegistry,
|
|
26368
28417
|
subscribeEternalIteration,
|
|
26369
|
-
sessionStore,
|
|
28418
|
+
sessionStore: activeSessionStore,
|
|
26370
28419
|
sessionsDir: wpaths.projectSessions,
|
|
26371
28420
|
brain,
|
|
26372
28421
|
brainSettings,
|
|
26373
28422
|
getBrainLog,
|
|
26374
28423
|
onSessionSwapped: (newSessionId) => {
|
|
26375
|
-
void
|
|
28424
|
+
void activeRecoveryLock.clear().then(() => activeRecoveryLock.write(newSessionId)).catch(() => void 0);
|
|
26376
28425
|
},
|
|
26377
28426
|
memoryStore,
|
|
26378
28427
|
skillLoader,
|
|
@@ -26404,12 +28453,12 @@ ${parts.join("\n")}
|
|
|
26404
28453
|
}
|
|
26405
28454
|
}
|
|
26406
28455
|
});
|
|
26407
|
-
const webuiExit = new Promise((
|
|
28456
|
+
const webuiExit = new Promise((resolve12) => {
|
|
26408
28457
|
const onSigint = () => {
|
|
26409
28458
|
renderer.setSilent(false);
|
|
26410
28459
|
renderer.write("\n");
|
|
26411
28460
|
renderer.writeInfo(color.yellow(" Shutting down WebUI server\u2026"));
|
|
26412
|
-
|
|
28461
|
+
resolve12(0);
|
|
26413
28462
|
};
|
|
26414
28463
|
process.on("SIGINT", onSigint);
|
|
26415
28464
|
process.on("SIGTERM", onSigint);
|
|
@@ -26417,13 +28466,13 @@ ${parts.join("\n")}
|
|
|
26417
28466
|
renderer.setSilent(false);
|
|
26418
28467
|
process.off("SIGINT", onSigint);
|
|
26419
28468
|
process.off("SIGTERM", onSigint);
|
|
26420
|
-
|
|
28469
|
+
resolve12(0);
|
|
26421
28470
|
}).catch((err) => {
|
|
26422
28471
|
renderer.setSilent(false);
|
|
26423
28472
|
process.off("SIGINT", onSigint);
|
|
26424
28473
|
process.off("SIGTERM", onSigint);
|
|
26425
28474
|
console.debug(`[execution] webui error: ${err}`);
|
|
26426
|
-
|
|
28475
|
+
resolve12(1);
|
|
26427
28476
|
});
|
|
26428
28477
|
});
|
|
26429
28478
|
code = await webuiExit;
|
|
@@ -26439,6 +28488,8 @@ ${parts.join("\n")}
|
|
|
26439
28488
|
attachments,
|
|
26440
28489
|
effectiveMaxContext,
|
|
26441
28490
|
projectName: path4.basename(projectRoot) || void 0,
|
|
28491
|
+
projectRoot,
|
|
28492
|
+
appConfig: config,
|
|
26442
28493
|
getAutonomy,
|
|
26443
28494
|
onAutonomy,
|
|
26444
28495
|
getNextPredict,
|
|
@@ -26494,7 +28545,7 @@ ${parts.join("\n")}
|
|
|
26494
28545
|
events.emit("session.ended", { id: activeSession.id, usage: tokenCounter.total() });
|
|
26495
28546
|
await pendingChimeraWork;
|
|
26496
28547
|
await activeSession.close();
|
|
26497
|
-
await
|
|
28548
|
+
await activeRecoveryLock.clear().catch(() => void 0);
|
|
26498
28549
|
await reader.close();
|
|
26499
28550
|
}
|
|
26500
28551
|
return code;
|
|
@@ -27812,6 +29863,19 @@ function setupMetrics(params) {
|
|
|
27812
29863
|
function setupPipelines(params) {
|
|
27813
29864
|
const { events, logger } = params;
|
|
27814
29865
|
const pipelines = createDefaultPipelines();
|
|
29866
|
+
if (params.modelRuntime) {
|
|
29867
|
+
const mr = params.modelRuntime;
|
|
29868
|
+
pipelines.request.use({
|
|
29869
|
+
name: "ModelRuntimeSettings",
|
|
29870
|
+
async handler(req2) {
|
|
29871
|
+
return applyModelRuntime(req2, {
|
|
29872
|
+
getSettings: mr.getSettings,
|
|
29873
|
+
getReasoningConfig: mr.getReasoningConfig,
|
|
29874
|
+
onWarning: mr.onWarning
|
|
29875
|
+
});
|
|
29876
|
+
}
|
|
29877
|
+
});
|
|
29878
|
+
}
|
|
27815
29879
|
const installBoundary = (p) => {
|
|
27816
29880
|
p.setErrorHandler((ev) => {
|
|
27817
29881
|
const fromPlugin = !!ev.owner && ev.owner !== "core";
|
|
@@ -28256,6 +30320,28 @@ async function main(argv) {
|
|
|
28256
30320
|
const handler = earlyFlags["help"] === true ? helpCmd : versionCmd;
|
|
28257
30321
|
return await handler([], { renderer: stubRenderer });
|
|
28258
30322
|
}
|
|
30323
|
+
if (earlyFlags["hq"] === true) {
|
|
30324
|
+
const { startHqServer: startHqServer2 } = await Promise.resolve().then(() => (init_hq_server(), hq_server_exports));
|
|
30325
|
+
const host = typeof earlyFlags["host"] === "string" ? earlyFlags["host"] : "127.0.0.1";
|
|
30326
|
+
const port = typeof earlyFlags["port"] === "string" ? Number.parseInt(earlyFlags["port"], 10) : 3499;
|
|
30327
|
+
const dataDir = typeof earlyFlags["data-dir"] === "string" ? earlyFlags["data-dir"] : void 0;
|
|
30328
|
+
const handle = await startHqServer2({ host, port, strictPort: earlyFlags["strict-port"] === true, ...dataDir !== void 0 ? { dataDir } : {} });
|
|
30329
|
+
if (earlyFlags["open"] === true) {
|
|
30330
|
+
try {
|
|
30331
|
+
const { openBrowser: openBrowser5 } = await import('@wrongstack/webui/server');
|
|
30332
|
+
openBrowser5(handle.firstRunSetup?.browserUrl ?? `http://${handle.host}:${handle.port}`);
|
|
30333
|
+
} catch {
|
|
30334
|
+
}
|
|
30335
|
+
}
|
|
30336
|
+
await new Promise((resolve12) => {
|
|
30337
|
+
const shutdown = () => {
|
|
30338
|
+
void handle.close().then(() => resolve12());
|
|
30339
|
+
};
|
|
30340
|
+
process.on("SIGINT", shutdown);
|
|
30341
|
+
process.on("SIGTERM", shutdown);
|
|
30342
|
+
});
|
|
30343
|
+
return 0;
|
|
30344
|
+
}
|
|
28259
30345
|
const ctx = await boot(argv);
|
|
28260
30346
|
if (typeof ctx === "number") return ctx;
|
|
28261
30347
|
let {
|
|
@@ -28499,7 +30585,10 @@ async function main(argv) {
|
|
|
28499
30585
|
const promptBuilder = container.resolve(TOKENS.SystemPromptBuilder);
|
|
28500
30586
|
let onlineAgents = [];
|
|
28501
30587
|
try {
|
|
28502
|
-
const
|
|
30588
|
+
const hqPublisher2 = createHqPublisherFromEnv({ clientKind: "cli", projectRoot, projectName: path4.basename(projectRoot), appConfig: config });
|
|
30589
|
+
hqPublisher2?.connect();
|
|
30590
|
+
if (hqPublisher2) teardownHandlers.push(() => hqPublisher2.close());
|
|
30591
|
+
const systemMailbox = new GlobalMailbox(wpaths.projectDir, void 0, hqPublisher2);
|
|
28503
30592
|
onlineAgents = await systemMailbox.getAgentStatuses();
|
|
28504
30593
|
} catch {
|
|
28505
30594
|
}
|
|
@@ -28710,7 +30799,27 @@ async function main(argv) {
|
|
|
28710
30799
|
}).catch(() => {
|
|
28711
30800
|
});
|
|
28712
30801
|
});
|
|
28713
|
-
|
|
30802
|
+
let activeReasoningConfig;
|
|
30803
|
+
const refreshActiveReasoningConfig = async (providerId, modelId) => {
|
|
30804
|
+
try {
|
|
30805
|
+
const resolved = await modelsRegistry.getModel(providerId, modelId);
|
|
30806
|
+
activeReasoningConfig = resolved?.capabilities.reasoningConfig;
|
|
30807
|
+
} catch {
|
|
30808
|
+
activeReasoningConfig = void 0;
|
|
30809
|
+
}
|
|
30810
|
+
};
|
|
30811
|
+
void refreshActiveReasoningConfig(config.provider, config.model);
|
|
30812
|
+
const pipelines = setupPipelines({
|
|
30813
|
+
events,
|
|
30814
|
+
logger,
|
|
30815
|
+
modelRuntime: {
|
|
30816
|
+
getSettings: () => configStore.get().modelRuntime,
|
|
30817
|
+
getReasoningConfig: () => activeReasoningConfig,
|
|
30818
|
+
onWarning: (message) => {
|
|
30819
|
+
logger.warn(`model-runtime: ${message}`);
|
|
30820
|
+
}
|
|
30821
|
+
}
|
|
30822
|
+
});
|
|
28714
30823
|
const hooksEnabled = flags["no-hooks"] !== true;
|
|
28715
30824
|
const hookRegistry = new HookRegistry();
|
|
28716
30825
|
if (hooksEnabled) hookRegistry.loadShellHooks(config.hooks);
|
|
@@ -28985,7 +31094,10 @@ async function main(argv) {
|
|
|
28985
31094
|
outcome: e.intervened ? "steered the agent" : "observed (no action)"
|
|
28986
31095
|
});
|
|
28987
31096
|
});
|
|
28988
|
-
const
|
|
31097
|
+
const hqPublisher = createHqPublisherFromEnv({ clientKind: "cli", projectRoot, projectName: path4.basename(projectRoot), appConfig: config });
|
|
31098
|
+
hqPublisher?.connect();
|
|
31099
|
+
if (hqPublisher) teardownHandlers.push(() => hqPublisher.close());
|
|
31100
|
+
const brainMailbox = new GlobalMailbox(wpaths.projectDir, events, hqPublisher);
|
|
28989
31101
|
const brainMonitor = new BrainMonitor({
|
|
28990
31102
|
events,
|
|
28991
31103
|
brain,
|
|
@@ -29213,6 +31325,7 @@ async function main(argv) {
|
|
|
29213
31325
|
log: (line) => renderer.write(`${line}
|
|
29214
31326
|
`)
|
|
29215
31327
|
});
|
|
31328
|
+
const coordinatorController = {};
|
|
29216
31329
|
const slashCmds = buildBuiltinSlashCommands({
|
|
29217
31330
|
registry: slashRegistry,
|
|
29218
31331
|
toolRegistry,
|
|
@@ -29254,6 +31367,7 @@ async function main(argv) {
|
|
|
29254
31367
|
brain,
|
|
29255
31368
|
brainSettings,
|
|
29256
31369
|
getBrainLog: () => brainLog,
|
|
31370
|
+
coordinatorController,
|
|
29257
31371
|
confirm: async (question, defaultYes = true) => {
|
|
29258
31372
|
if (!isStdinTTY()) return false;
|
|
29259
31373
|
const hint = defaultYes ? "[Y/n/q]" : "[y/N/q]";
|
|
@@ -29507,12 +31621,12 @@ ${color.dim("\u2500".repeat(40))}` : "";
|
|
|
29507
31621
|
if (!f.endsWith(".jsonl")) continue;
|
|
29508
31622
|
const full = path4.join(runDir, f);
|
|
29509
31623
|
try {
|
|
29510
|
-
const
|
|
31624
|
+
const stat8 = await fsp5.stat(full);
|
|
29511
31625
|
found.push({
|
|
29512
31626
|
runId,
|
|
29513
31627
|
subagentId: f.replace(/\.jsonl$/, ""),
|
|
29514
31628
|
file: full,
|
|
29515
|
-
size:
|
|
31629
|
+
size: stat8.size
|
|
29516
31630
|
});
|
|
29517
31631
|
} catch {
|
|
29518
31632
|
}
|
|
@@ -29820,7 +31934,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
29820
31934
|
onBeforeExit: async () => {
|
|
29821
31935
|
const cwd2 = projectRoot;
|
|
29822
31936
|
const statusResult = await new Promise(
|
|
29823
|
-
(
|
|
31937
|
+
(resolve12, reject) => {
|
|
29824
31938
|
const child = spawn("git", ["status", "--porcelain"], {
|
|
29825
31939
|
cwd: cwd2,
|
|
29826
31940
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -29832,7 +31946,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
29832
31946
|
stdout += d;
|
|
29833
31947
|
});
|
|
29834
31948
|
child.on("error", reject);
|
|
29835
|
-
child.on("close", (code) =>
|
|
31949
|
+
child.on("close", (code) => resolve12({ stdout, code: code ?? 0 }));
|
|
29836
31950
|
}
|
|
29837
31951
|
);
|
|
29838
31952
|
if (statusResult.stdout.trim().length > 0) {
|
|
@@ -30002,6 +32116,8 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
30002
32116
|
getPickableProviders: () => buildPickableProviders(modelsRegistry, config),
|
|
30003
32117
|
switchProviderAndModel,
|
|
30004
32118
|
director: director ?? null,
|
|
32119
|
+
getDirector: () => director,
|
|
32120
|
+
coordinatorController,
|
|
30005
32121
|
fleetRoster: FLEET_ROSTER,
|
|
30006
32122
|
fleetStreamController,
|
|
30007
32123
|
interruptController,
|