@wrongstack/cli 0.5.0 → 0.5.3
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 +1091 -165
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import * as
|
|
3
|
-
import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
|
|
2
|
+
import * as path20 from 'path';
|
|
3
|
+
import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import * as fs3 from 'fs/promises';
|
|
6
|
+
import * as os6 from 'os';
|
|
7
|
+
import os6__default from 'os';
|
|
4
8
|
import * as crypto from 'crypto';
|
|
5
9
|
import { randomUUID } from 'crypto';
|
|
6
|
-
import * as fs14 from 'fs/promises';
|
|
7
10
|
import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
|
|
8
11
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
12
|
import { writeFileSync } from 'fs';
|
|
10
|
-
import { createRequire } from 'module';
|
|
11
13
|
import { MCPRegistry } from '@wrongstack/mcp';
|
|
12
14
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
|
|
13
15
|
import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
|
|
14
16
|
import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
|
|
15
|
-
import * as os4 from 'os';
|
|
16
17
|
import * as readline from 'readline';
|
|
18
|
+
import { spawn } from 'child_process';
|
|
17
19
|
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
18
20
|
import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
|
|
19
21
|
|
|
@@ -21,6 +23,12 @@ var __defProp = Object.defineProperty;
|
|
|
21
23
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
22
24
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
23
25
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
26
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
27
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
28
|
+
}) : x)(function(x) {
|
|
29
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
30
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
31
|
+
});
|
|
24
32
|
var __esm = (fn, res) => function __init() {
|
|
25
33
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
26
34
|
};
|
|
@@ -236,8 +244,8 @@ function buildSddCommand(opts) {
|
|
|
236
244
|
async run(args) {
|
|
237
245
|
const ctx = opts.context;
|
|
238
246
|
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
239
|
-
const specsDir =
|
|
240
|
-
const graphsDir =
|
|
247
|
+
const specsDir = path20.join(projectRoot, ".wrongstack", "specs");
|
|
248
|
+
const graphsDir = path20.join(projectRoot, ".wrongstack", "task-graphs");
|
|
241
249
|
const specStore = new SpecStore({ baseDir: specsDir });
|
|
242
250
|
new TaskGraphStore({ baseDir: graphsDir });
|
|
243
251
|
const versioning = new SpecVersioning();
|
|
@@ -253,7 +261,7 @@ function buildSddCommand(opts) {
|
|
|
253
261
|
const forceFlag = rest.includes("--force") || rest.includes("-f");
|
|
254
262
|
const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
|
|
255
263
|
if (!activeBuilder && !forceFlag) {
|
|
256
|
-
const sessionPath =
|
|
264
|
+
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
257
265
|
try {
|
|
258
266
|
const fsp = await import('fs/promises');
|
|
259
267
|
await fsp.access(sessionPath);
|
|
@@ -291,7 +299,7 @@ function buildSddCommand(opts) {
|
|
|
291
299
|
projectContext,
|
|
292
300
|
minQuestions: 2,
|
|
293
301
|
maxQuestions: 10,
|
|
294
|
-
sessionPath:
|
|
302
|
+
sessionPath: path20.join(projectRoot, ".wrongstack", "sdd-session.json")
|
|
295
303
|
});
|
|
296
304
|
activeBuilder.startSession(title);
|
|
297
305
|
const aiPrompt = activeBuilder.getAIPrompt();
|
|
@@ -551,7 +559,7 @@ Start executing the tasks one by one.`
|
|
|
551
559
|
};
|
|
552
560
|
}
|
|
553
561
|
case "cancel": {
|
|
554
|
-
const sessionPath =
|
|
562
|
+
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
555
563
|
let deletedFromDisk = false;
|
|
556
564
|
try {
|
|
557
565
|
const fsp = await import('fs/promises');
|
|
@@ -577,7 +585,7 @@ Start executing the tasks one by one.`
|
|
|
577
585
|
if (activeBuilder) {
|
|
578
586
|
return { message: "An SDD session is already active. Use /sdd cancel first." };
|
|
579
587
|
}
|
|
580
|
-
const sessionPath =
|
|
588
|
+
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
581
589
|
const projectContext = await gatherProjectContext(projectRoot);
|
|
582
590
|
activeBuilder = new AISpecBuilder({
|
|
583
591
|
store: specStore,
|
|
@@ -803,7 +811,7 @@ async function gatherProjectContext(projectRoot) {
|
|
|
803
811
|
const parts = [];
|
|
804
812
|
try {
|
|
805
813
|
const fsp = await import('fs/promises');
|
|
806
|
-
const pkgPath =
|
|
814
|
+
const pkgPath = path20.join(projectRoot, "package.json");
|
|
807
815
|
const pkgRaw = await fsp.readFile(pkgPath, "utf8");
|
|
808
816
|
const pkg = JSON.parse(pkgRaw);
|
|
809
817
|
parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
|
|
@@ -820,14 +828,14 @@ async function gatherProjectContext(projectRoot) {
|
|
|
820
828
|
}
|
|
821
829
|
try {
|
|
822
830
|
const fsp = await import('fs/promises');
|
|
823
|
-
const tsconfigPath =
|
|
831
|
+
const tsconfigPath = path20.join(projectRoot, "tsconfig.json");
|
|
824
832
|
await fsp.access(tsconfigPath);
|
|
825
833
|
parts.push("Language: TypeScript");
|
|
826
834
|
} catch {
|
|
827
835
|
}
|
|
828
836
|
try {
|
|
829
837
|
const fsp = await import('fs/promises');
|
|
830
|
-
const srcDir =
|
|
838
|
+
const srcDir = path20.join(projectRoot, "src");
|
|
831
839
|
const entries = await fsp.readdir(srcDir, { withFileTypes: true });
|
|
832
840
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
833
841
|
if (dirs.length > 0) {
|
|
@@ -899,6 +907,130 @@ var init_provider_config_utils = __esm({
|
|
|
899
907
|
}
|
|
900
908
|
});
|
|
901
909
|
|
|
910
|
+
// src/update-check.ts
|
|
911
|
+
var update_check_exports = {};
|
|
912
|
+
__export(update_check_exports, {
|
|
913
|
+
cachePath: () => cachePath,
|
|
914
|
+
checkForUpdate: () => checkForUpdate,
|
|
915
|
+
currentVersion: () => currentVersion,
|
|
916
|
+
getUpdateNotification: () => getUpdateNotification
|
|
917
|
+
});
|
|
918
|
+
function cachePath(homeFn = defaultHomeDir2) {
|
|
919
|
+
return path20.join(homeFn(), ".wrongstack", "update-cache.json");
|
|
920
|
+
}
|
|
921
|
+
function currentVersion() {
|
|
922
|
+
const req2 = createRequire(import.meta.url);
|
|
923
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
924
|
+
for (const rel of candidates) {
|
|
925
|
+
try {
|
|
926
|
+
const pkg = req2(rel);
|
|
927
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return "dev";
|
|
932
|
+
}
|
|
933
|
+
function isNewer(a, b) {
|
|
934
|
+
const parse = (v) => v.replace(/^v/i, "").split(".").map((p) => parseInt(p, 10) || 0);
|
|
935
|
+
const [ap, bp] = [parse(a), parse(b)];
|
|
936
|
+
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
937
|
+
const ai = ap[i] ?? 0;
|
|
938
|
+
const bi = bp[i] ?? 0;
|
|
939
|
+
if (ai > bi) return true;
|
|
940
|
+
if (ai < bi) return false;
|
|
941
|
+
}
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
async function readCache(homeFn = defaultHomeDir2) {
|
|
945
|
+
try {
|
|
946
|
+
const raw = await fs3.readFile(cachePath(homeFn), "utf8");
|
|
947
|
+
const entry = JSON.parse(raw);
|
|
948
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
|
|
949
|
+
return entry;
|
|
950
|
+
} catch {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function writeCache(entry, homeFn = defaultHomeDir2) {
|
|
955
|
+
try {
|
|
956
|
+
const dir = path20.dirname(cachePath(homeFn));
|
|
957
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
958
|
+
await fs3.writeFile(cachePath(homeFn), JSON.stringify(entry, null, 2), "utf8");
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function fetchLatestFromNpm(timeoutMs = 3e3) {
|
|
963
|
+
const controller = new AbortController();
|
|
964
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
965
|
+
try {
|
|
966
|
+
const res = await fetch("https://registry.npmjs.org/wrongstack/latest", {
|
|
967
|
+
signal: controller.signal,
|
|
968
|
+
headers: { "Accept": "application/json" }
|
|
969
|
+
});
|
|
970
|
+
clearTimeout(timer);
|
|
971
|
+
if (!res.ok) throw new Error(`npm registry responded ${res.status}`);
|
|
972
|
+
const data = await res.json();
|
|
973
|
+
if (typeof data.version === "string") return data.version;
|
|
974
|
+
throw new Error("No version field in npm response");
|
|
975
|
+
} finally {
|
|
976
|
+
clearTimeout(timer);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
async function checkForUpdate(signal, homeFn) {
|
|
980
|
+
const current = currentVersion();
|
|
981
|
+
const aborted = () => signal?.aborted ?? false;
|
|
982
|
+
const hf = homeFn ?? defaultHomeDir2;
|
|
983
|
+
if (aborted()) {
|
|
984
|
+
return { current, latest: current, outdated: false, checkFailed: true };
|
|
985
|
+
}
|
|
986
|
+
const cached = await readCache(hf);
|
|
987
|
+
if (cached && !cached.error) {
|
|
988
|
+
return {
|
|
989
|
+
current,
|
|
990
|
+
latest: cached.latestVersion,
|
|
991
|
+
outdated: isNewer(cached.latestVersion, current),
|
|
992
|
+
checkFailed: false
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
const latest = await fetchLatestFromNpm();
|
|
997
|
+
await writeCache({ timestamp: Date.now(), latestVersion: latest }, hf);
|
|
998
|
+
return {
|
|
999
|
+
current,
|
|
1000
|
+
latest,
|
|
1001
|
+
outdated: isNewer(latest, current),
|
|
1002
|
+
checkFailed: false
|
|
1003
|
+
};
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
if (aborted()) {
|
|
1006
|
+
return { current, latest: current, outdated: false, checkFailed: true };
|
|
1007
|
+
}
|
|
1008
|
+
if (cached?.latestVersion) {
|
|
1009
|
+
return {
|
|
1010
|
+
current,
|
|
1011
|
+
latest: cached.latestVersion,
|
|
1012
|
+
outdated: isNewer(cached.latestVersion, current),
|
|
1013
|
+
checkFailed: true
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
return { current, latest: current, outdated: false, checkFailed: true };
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function getUpdateNotification(signal, homeFn) {
|
|
1020
|
+
const info = await checkForUpdate(signal, homeFn);
|
|
1021
|
+
if (info.outdated) {
|
|
1022
|
+
return `Update available: v${info.current} \u2192 v${info.latest}`;
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
var defaultHomeDir2, CACHE_TTL_MS;
|
|
1027
|
+
var init_update_check = __esm({
|
|
1028
|
+
"src/update-check.ts"() {
|
|
1029
|
+
defaultHomeDir2 = () => os6.homedir();
|
|
1030
|
+
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
902
1034
|
// src/webui-server.ts
|
|
903
1035
|
var webui_server_exports = {};
|
|
904
1036
|
__export(webui_server_exports, {
|
|
@@ -1005,7 +1137,7 @@ async function runWebUI(opts) {
|
|
|
1005
1137
|
})
|
|
1006
1138
|
);
|
|
1007
1139
|
}
|
|
1008
|
-
return new Promise((
|
|
1140
|
+
return new Promise((resolve4) => {
|
|
1009
1141
|
wss.on("listening", () => {
|
|
1010
1142
|
console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
|
|
1011
1143
|
setupEvents();
|
|
@@ -1075,7 +1207,7 @@ async function runWebUI(opts) {
|
|
|
1075
1207
|
clients.clear();
|
|
1076
1208
|
wss.close(() => {
|
|
1077
1209
|
console.log("[WebUI] Server stopped");
|
|
1078
|
-
|
|
1210
|
+
resolve4();
|
|
1079
1211
|
});
|
|
1080
1212
|
}
|
|
1081
1213
|
process.on("SIGINT", shutdown);
|
|
@@ -1381,7 +1513,7 @@ async function runWebUI(opts) {
|
|
|
1381
1513
|
if (!opts.globalConfigPath) return {};
|
|
1382
1514
|
let raw;
|
|
1383
1515
|
try {
|
|
1384
|
-
raw = await
|
|
1516
|
+
raw = await fs3.readFile(opts.globalConfigPath, "utf8");
|
|
1385
1517
|
} catch {
|
|
1386
1518
|
return {};
|
|
1387
1519
|
}
|
|
@@ -1392,7 +1524,7 @@ async function runWebUI(opts) {
|
|
|
1392
1524
|
return {};
|
|
1393
1525
|
}
|
|
1394
1526
|
if (!parsed.providers) return {};
|
|
1395
|
-
const keyFile =
|
|
1527
|
+
const keyFile = path20.join(path20.dirname(opts.globalConfigPath), ".key");
|
|
1396
1528
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1397
1529
|
return decryptConfigSecrets$1(parsed.providers, vault);
|
|
1398
1530
|
}
|
|
@@ -1400,7 +1532,7 @@ async function runWebUI(opts) {
|
|
|
1400
1532
|
if (!opts.globalConfigPath) return;
|
|
1401
1533
|
let raw;
|
|
1402
1534
|
try {
|
|
1403
|
-
raw = await
|
|
1535
|
+
raw = await fs3.readFile(opts.globalConfigPath, "utf8");
|
|
1404
1536
|
} catch {
|
|
1405
1537
|
raw = "{}";
|
|
1406
1538
|
}
|
|
@@ -1411,7 +1543,7 @@ async function runWebUI(opts) {
|
|
|
1411
1543
|
parsed = {};
|
|
1412
1544
|
}
|
|
1413
1545
|
parsed.providers = providers;
|
|
1414
|
-
const keyFile =
|
|
1546
|
+
const keyFile = path20.join(path20.dirname(opts.globalConfigPath), ".key");
|
|
1415
1547
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1416
1548
|
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1417
1549
|
await atomicWrite(opts.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
@@ -1439,6 +1571,37 @@ var init_plugin_api_factory = __esm({
|
|
|
1439
1571
|
}
|
|
1440
1572
|
});
|
|
1441
1573
|
|
|
1574
|
+
// src/slash-commands/commit-llm.ts
|
|
1575
|
+
async function generateCommitMessageWithLLM(diff, opts) {
|
|
1576
|
+
const systemPrompt = "You are a helpful assistant that generates concise, conventional-commit-formatted git commit messages. Analyze the provided diff and output ONLY the commit message (no explanation, no quotes). Format: <type>(<scope>): <short description> \u2014 <type> is one of: feat, fix, docs, style, refactor, test, chore, perf, ci, build, temp. If the diff contains multiple unrelated changes, pick the most important one. Keep the description under 72 characters. Example: feat(cli): add /commit LLM integration";
|
|
1577
|
+
const userPrompt = `Here is the git diff:
|
|
1578
|
+
|
|
1579
|
+
${diff}`;
|
|
1580
|
+
try {
|
|
1581
|
+
const signal = new AbortController();
|
|
1582
|
+
const timeout = setTimeout(() => signal.abort(), 15e3);
|
|
1583
|
+
const resp = await opts.provider.complete(
|
|
1584
|
+
{
|
|
1585
|
+
model: opts.model,
|
|
1586
|
+
system: [{ type: "text", text: systemPrompt }],
|
|
1587
|
+
messages: [{ role: "user", content: [{ type: "text", text: userPrompt }] }],
|
|
1588
|
+
maxTokens: 80,
|
|
1589
|
+
temperature: 0.3
|
|
1590
|
+
},
|
|
1591
|
+
{ signal: signal.signal }
|
|
1592
|
+
);
|
|
1593
|
+
clearTimeout(timeout);
|
|
1594
|
+
const rawContent = resp.content;
|
|
1595
|
+
const text = Array.isArray(rawContent) ? rawContent[0]?.text ?? "" : typeof rawContent === "object" && rawContent !== null ? rawContent.text ?? "" : String(rawContent);
|
|
1596
|
+
const message = text.trim().split("\n")[0];
|
|
1597
|
+
if (message.length > 0 && message.length < 200) {
|
|
1598
|
+
return message;
|
|
1599
|
+
}
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
return "chore: update";
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1442
1605
|
// src/arg-parser.ts
|
|
1443
1606
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
1444
1607
|
"yolo",
|
|
@@ -1457,7 +1620,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
1457
1620
|
"output-json",
|
|
1458
1621
|
"prompt",
|
|
1459
1622
|
"metrics",
|
|
1460
|
-
"webui"
|
|
1623
|
+
"webui",
|
|
1624
|
+
"no-check"
|
|
1461
1625
|
]);
|
|
1462
1626
|
function parseArgs(argv) {
|
|
1463
1627
|
const flags = {};
|
|
@@ -1563,10 +1727,10 @@ function parseSpawnFlags(input) {
|
|
|
1563
1727
|
return { description: rest.trim(), opts };
|
|
1564
1728
|
}
|
|
1565
1729
|
async function bootConfig(flags) {
|
|
1566
|
-
const cwd = typeof flags["cwd"] === "string" ?
|
|
1730
|
+
const cwd = typeof flags["cwd"] === "string" ? path20.resolve(flags["cwd"]) : process.cwd();
|
|
1567
1731
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
1568
1732
|
const projectRoot = pathResolver.projectRoot;
|
|
1569
|
-
const userHome =
|
|
1733
|
+
const userHome = os6.homedir();
|
|
1570
1734
|
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
1571
1735
|
await ensureProjectMeta(wpaths, projectRoot);
|
|
1572
1736
|
const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
|
|
@@ -1614,13 +1778,13 @@ function flagsToConfigPatch(flags) {
|
|
|
1614
1778
|
}
|
|
1615
1779
|
async function ensureProjectMeta(paths, projectRoot) {
|
|
1616
1780
|
try {
|
|
1617
|
-
await
|
|
1781
|
+
await fs3.mkdir(paths.projectDir, { recursive: true });
|
|
1618
1782
|
const meta = {
|
|
1619
1783
|
hash: paths.projectHash,
|
|
1620
1784
|
root: projectRoot,
|
|
1621
1785
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1622
1786
|
};
|
|
1623
|
-
await
|
|
1787
|
+
await fs3.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
1624
1788
|
} catch {
|
|
1625
1789
|
}
|
|
1626
1790
|
}
|
|
@@ -1630,11 +1794,11 @@ var ReadlineInputReader = class {
|
|
|
1630
1794
|
history = [];
|
|
1631
1795
|
pending = false;
|
|
1632
1796
|
constructor(opts = {}) {
|
|
1633
|
-
this.historyFile = opts.historyFile ??
|
|
1797
|
+
this.historyFile = opts.historyFile ?? path20.join(os6.homedir(), ".wrongstack", "history");
|
|
1634
1798
|
}
|
|
1635
1799
|
async loadHistory() {
|
|
1636
1800
|
try {
|
|
1637
|
-
const raw = await
|
|
1801
|
+
const raw = await fs3.readFile(this.historyFile, "utf8");
|
|
1638
1802
|
this.history = raw.split("\n").filter(Boolean).slice(-1e3);
|
|
1639
1803
|
} catch {
|
|
1640
1804
|
this.history = [];
|
|
@@ -1642,8 +1806,8 @@ var ReadlineInputReader = class {
|
|
|
1642
1806
|
}
|
|
1643
1807
|
async saveHistory() {
|
|
1644
1808
|
try {
|
|
1645
|
-
await
|
|
1646
|
-
await
|
|
1809
|
+
await fs3.mkdir(path20.dirname(this.historyFile), { recursive: true });
|
|
1810
|
+
await fs3.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
|
|
1647
1811
|
} catch {
|
|
1648
1812
|
}
|
|
1649
1813
|
}
|
|
@@ -1661,7 +1825,7 @@ var ReadlineInputReader = class {
|
|
|
1661
1825
|
async readLine(prompt) {
|
|
1662
1826
|
if (this.history.length === 0) await this.loadHistory();
|
|
1663
1827
|
while (this.pending) {
|
|
1664
|
-
await new Promise((
|
|
1828
|
+
await new Promise((resolve4) => setTimeout(resolve4, 50));
|
|
1665
1829
|
}
|
|
1666
1830
|
this.pending = true;
|
|
1667
1831
|
try {
|
|
@@ -1671,15 +1835,15 @@ var ReadlineInputReader = class {
|
|
|
1671
1835
|
this.rl = void 0;
|
|
1672
1836
|
}
|
|
1673
1837
|
const fresh = this.ensure();
|
|
1674
|
-
return new Promise((
|
|
1838
|
+
return new Promise((resolve4) => {
|
|
1675
1839
|
fresh.question(prompt ?? "> ", (line) => {
|
|
1676
1840
|
if (line.trim()) {
|
|
1677
1841
|
this.history.push(line);
|
|
1678
1842
|
void this.saveHistory();
|
|
1679
1843
|
}
|
|
1680
|
-
|
|
1844
|
+
resolve4(line);
|
|
1681
1845
|
});
|
|
1682
|
-
fresh.once("close", () =>
|
|
1846
|
+
fresh.once("close", () => resolve4(""));
|
|
1683
1847
|
}).then((result) => {
|
|
1684
1848
|
this.rl?.close();
|
|
1685
1849
|
this.rl = void 0;
|
|
@@ -1691,7 +1855,7 @@ var ReadlineInputReader = class {
|
|
|
1691
1855
|
}
|
|
1692
1856
|
async readKey(prompt, options) {
|
|
1693
1857
|
process.stdout.write(prompt);
|
|
1694
|
-
return new Promise((
|
|
1858
|
+
return new Promise((resolve4) => {
|
|
1695
1859
|
const stdin = process.stdin;
|
|
1696
1860
|
const wasRaw = stdin.isRaw;
|
|
1697
1861
|
const wasPaused = stdin.isPaused();
|
|
@@ -1702,7 +1866,7 @@ var ReadlineInputReader = class {
|
|
|
1702
1866
|
if (key === "") {
|
|
1703
1867
|
cleanup();
|
|
1704
1868
|
process.stdout.write("\n");
|
|
1705
|
-
|
|
1869
|
+
resolve4("");
|
|
1706
1870
|
return;
|
|
1707
1871
|
}
|
|
1708
1872
|
const opt = options.find(
|
|
@@ -1712,12 +1876,12 @@ var ReadlineInputReader = class {
|
|
|
1712
1876
|
cleanup();
|
|
1713
1877
|
process.stdout.write(`${opt.key}
|
|
1714
1878
|
`);
|
|
1715
|
-
|
|
1879
|
+
resolve4(opt.value);
|
|
1716
1880
|
}
|
|
1717
1881
|
};
|
|
1718
1882
|
const onClose = () => {
|
|
1719
1883
|
cleanup();
|
|
1720
|
-
|
|
1884
|
+
resolve4("");
|
|
1721
1885
|
};
|
|
1722
1886
|
const cleanup = () => {
|
|
1723
1887
|
stdin.off("data", onData);
|
|
@@ -1745,7 +1909,7 @@ var ReadlineInputReader = class {
|
|
|
1745
1909
|
this.rl?.close();
|
|
1746
1910
|
this.rl = void 0;
|
|
1747
1911
|
process.stdout.write(prompt);
|
|
1748
|
-
return new Promise((
|
|
1912
|
+
return new Promise((resolve4) => {
|
|
1749
1913
|
let buf = "";
|
|
1750
1914
|
const wasRaw = stdin.isRaw;
|
|
1751
1915
|
stdin.setRawMode(true);
|
|
@@ -1763,7 +1927,7 @@ var ReadlineInputReader = class {
|
|
|
1763
1927
|
cleanup();
|
|
1764
1928
|
process.stdout.write(` ${dim(`[${buf.length} chars]`)}
|
|
1765
1929
|
`);
|
|
1766
|
-
|
|
1930
|
+
resolve4(buf);
|
|
1767
1931
|
return;
|
|
1768
1932
|
}
|
|
1769
1933
|
if (ch === "") {
|
|
@@ -1860,8 +2024,247 @@ async function buildPickableProviders(modelsRegistry, config) {
|
|
|
1860
2024
|
}
|
|
1861
2025
|
return out;
|
|
1862
2026
|
}
|
|
2027
|
+
var PROTECTED_BASENAMES = /* @__PURE__ */ new Set([
|
|
2028
|
+
"config.json",
|
|
2029
|
+
".key",
|
|
2030
|
+
"index.json"
|
|
2031
|
+
]);
|
|
2032
|
+
function assertSafeToDelete(filename, parentDir) {
|
|
2033
|
+
if (PROTECTED_BASENAMES.has(filename)) {
|
|
2034
|
+
throw new Error(`Refusing to delete protected file: ${filename}`);
|
|
2035
|
+
}
|
|
2036
|
+
if (filename !== path20.basename(filename)) {
|
|
2037
|
+
throw new Error(`Refusing to delete path with traversal: ${filename}`);
|
|
2038
|
+
}
|
|
2039
|
+
if (!filename.startsWith("config.json.") || !filename.endsWith(".bak")) {
|
|
2040
|
+
throw new Error(`Refusing to delete unknown file: ${filename}`);
|
|
2041
|
+
}
|
|
2042
|
+
const resolvedParent = path20.resolve(parentDir);
|
|
2043
|
+
if (!resolvedParent.endsWith(".wrongstack")) {
|
|
2044
|
+
throw new Error(`Unexpected parent directory for bak prune: ${resolvedParent}`);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
async function safeDelete(filePath) {
|
|
2048
|
+
const dir = path20.dirname(filePath);
|
|
2049
|
+
const filename = path20.basename(filePath);
|
|
2050
|
+
try {
|
|
2051
|
+
assertSafeToDelete(filename, dir);
|
|
2052
|
+
await fs3.unlink(filePath);
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
if (err instanceof Error && err.message.startsWith("Refusing")) {
|
|
2055
|
+
process.stderr.write(`[config-history] SAFETY: ${err.message}
|
|
2056
|
+
`);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function maskConfigSecrets(cfg) {
|
|
2061
|
+
if (typeof cfg !== "object" || cfg === null) return {};
|
|
2062
|
+
const out = {};
|
|
2063
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
2064
|
+
if (k === "apiKey" || k === "apiKeys" || k === "secret" || k === "secrets") {
|
|
2065
|
+
out[k] = "[REDACTED]";
|
|
2066
|
+
} else if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
2067
|
+
out[k] = maskConfigSecrets(v);
|
|
2068
|
+
} else {
|
|
2069
|
+
out[k] = v;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return out;
|
|
2073
|
+
}
|
|
2074
|
+
function diffSummary(oldCfg, newCfg) {
|
|
2075
|
+
const changes = [];
|
|
2076
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldCfg), ...Object.keys(newCfg)]);
|
|
2077
|
+
for (const k of allKeys) {
|
|
2078
|
+
const o = JSON.stringify(oldCfg[k]);
|
|
2079
|
+
const n = JSON.stringify(newCfg[k]);
|
|
2080
|
+
if (o !== n) {
|
|
2081
|
+
if (k === "apiKey" || k === "apiKeys" || k === "secret") {
|
|
2082
|
+
changes.push(`${k}: [CHANGED]`);
|
|
2083
|
+
} else if (typeof newCfg[k] !== "object") {
|
|
2084
|
+
changes.push(`${k}: ${oldCfg[k] ?? "(unset)"} \u2192 ${newCfg[k]}`);
|
|
2085
|
+
} else {
|
|
2086
|
+
changes.push(`${k}: [CHANGED]`);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return changes.length > 0 ? changes.slice(0, 5).join(", ") : "no changes";
|
|
2091
|
+
}
|
|
2092
|
+
var defaultHomeDir = () => os6__default.homedir();
|
|
2093
|
+
function historyDir(homeFn = defaultHomeDir) {
|
|
2094
|
+
return path20.join(homeFn(), ".wrongstack", "config.history", "entries");
|
|
2095
|
+
}
|
|
2096
|
+
function historyIndexPath(homeFn = defaultHomeDir) {
|
|
2097
|
+
return path20.join(homeFn(), ".wrongstack", "config.history", "index.json");
|
|
2098
|
+
}
|
|
2099
|
+
function configPath(homeFn = defaultHomeDir) {
|
|
2100
|
+
return path20.join(homeFn(), ".wrongstack", "config.json");
|
|
2101
|
+
}
|
|
2102
|
+
function backupLastPath(homeFn = defaultHomeDir) {
|
|
2103
|
+
return path20.join(homeFn(), ".wrongstack", "config.json.last");
|
|
2104
|
+
}
|
|
2105
|
+
function entryId(ts) {
|
|
2106
|
+
return ts.replace(/[:.]/g, "-").slice(0, 19);
|
|
2107
|
+
}
|
|
2108
|
+
async function ensureHistoryDir(homeFn = defaultHomeDir) {
|
|
2109
|
+
await fs3.mkdir(historyDir(homeFn), { recursive: true });
|
|
2110
|
+
}
|
|
2111
|
+
async function readIndex(homeFn = defaultHomeDir) {
|
|
2112
|
+
try {
|
|
2113
|
+
const raw = await fs3.readFile(historyIndexPath(homeFn), "utf8");
|
|
2114
|
+
return JSON.parse(raw);
|
|
2115
|
+
} catch {
|
|
2116
|
+
return { version: 1, entries: [] };
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
async function writeIndex(idx, homeFn = defaultHomeDir) {
|
|
2120
|
+
await ensureHistoryDir(homeFn);
|
|
2121
|
+
await fs3.writeFile(historyIndexPath(homeFn), JSON.stringify(idx, null, 2), "utf8");
|
|
2122
|
+
}
|
|
2123
|
+
async function backupCurrent(homeFn = defaultHomeDir) {
|
|
2124
|
+
const cfg = configPath(homeFn);
|
|
2125
|
+
const last = backupLastPath(homeFn);
|
|
2126
|
+
const ts = Date.now();
|
|
2127
|
+
let content;
|
|
2128
|
+
try {
|
|
2129
|
+
content = await fs3.readFile(cfg, "utf8");
|
|
2130
|
+
} catch {
|
|
2131
|
+
}
|
|
2132
|
+
if (content !== void 0) {
|
|
2133
|
+
try {
|
|
2134
|
+
await atomicWrite(last, content);
|
|
2135
|
+
} catch {
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (content !== void 0) {
|
|
2139
|
+
try {
|
|
2140
|
+
const bakPath = path20.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
|
|
2141
|
+
await atomicWrite(bakPath, content);
|
|
2142
|
+
} catch {
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
try {
|
|
2146
|
+
const dir = path20.join(homeFn(), ".wrongstack");
|
|
2147
|
+
const files = await fs3.readdir(dir);
|
|
2148
|
+
const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
|
|
2149
|
+
for (const f of baks.slice(10)) {
|
|
2150
|
+
await safeDelete(path20.join(dir, f));
|
|
2151
|
+
}
|
|
2152
|
+
} catch {
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDir) {
|
|
2156
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2157
|
+
const id = entryId(timestamp);
|
|
2158
|
+
await ensureHistoryDir(homeFn);
|
|
2159
|
+
const entry = {
|
|
2160
|
+
id,
|
|
2161
|
+
timestamp,
|
|
2162
|
+
description,
|
|
2163
|
+
snapshotMasked: maskConfigSecrets(newCfg),
|
|
2164
|
+
diffSummary: diffSummary(oldCfg, newCfg)
|
|
2165
|
+
};
|
|
2166
|
+
await fs3.writeFile(
|
|
2167
|
+
path20.join(historyDir(homeFn), `${id}.json`),
|
|
2168
|
+
JSON.stringify(entry, null, 2),
|
|
2169
|
+
"utf8"
|
|
2170
|
+
);
|
|
2171
|
+
const idx = await readIndex(homeFn);
|
|
2172
|
+
idx.entries.unshift({ id, timestamp, description });
|
|
2173
|
+
await writeIndex(idx, homeFn);
|
|
2174
|
+
return id;
|
|
2175
|
+
}
|
|
2176
|
+
async function listHistory(homeFn = defaultHomeDir) {
|
|
2177
|
+
const idx = await readIndex(homeFn);
|
|
2178
|
+
return idx.entries;
|
|
2179
|
+
}
|
|
2180
|
+
async function getHistoryEntry(id, homeFn = defaultHomeDir) {
|
|
2181
|
+
try {
|
|
2182
|
+
const raw = await fs3.readFile(path20.join(historyDir(homeFn), `${id}.json`), "utf8");
|
|
2183
|
+
return JSON.parse(raw);
|
|
2184
|
+
} catch {
|
|
2185
|
+
return null;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
async function restoreFromHistory(id, homeFn = defaultHomeDir) {
|
|
2189
|
+
const entry = await getHistoryEntry(id, homeFn);
|
|
2190
|
+
if (!entry) return { ok: false, backupId: null, error: "History entry not found" };
|
|
2191
|
+
await backupCurrent(homeFn);
|
|
2192
|
+
let oldCfg = {};
|
|
2193
|
+
try {
|
|
2194
|
+
const raw = await fs3.readFile(configPath(homeFn), "utf8");
|
|
2195
|
+
oldCfg = JSON.parse(raw);
|
|
2196
|
+
} catch {
|
|
2197
|
+
}
|
|
2198
|
+
try {
|
|
2199
|
+
await atomicWrite(configPath(homeFn), JSON.stringify(entry.snapshotMasked, null, 2));
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
return { ok: false, backupId: null, error: String(err) };
|
|
2202
|
+
}
|
|
2203
|
+
const backupId = await appendHistory(
|
|
2204
|
+
oldCfg,
|
|
2205
|
+
entry.snapshotMasked,
|
|
2206
|
+
`Restored from history ${id}`,
|
|
2207
|
+
homeFn
|
|
2208
|
+
);
|
|
2209
|
+
return { ok: true, backupId };
|
|
2210
|
+
}
|
|
2211
|
+
async function restoreLast(homeFn = defaultHomeDir) {
|
|
2212
|
+
const last = backupLastPath(homeFn);
|
|
2213
|
+
const cfg = configPath(homeFn);
|
|
2214
|
+
let oldCfg = {};
|
|
2215
|
+
try {
|
|
2216
|
+
const raw = await fs3.readFile(cfg, "utf8");
|
|
2217
|
+
oldCfg = JSON.parse(raw);
|
|
2218
|
+
} catch {
|
|
2219
|
+
}
|
|
2220
|
+
let lastCfg = {};
|
|
2221
|
+
try {
|
|
2222
|
+
const raw = await fs3.readFile(last, "utf8");
|
|
2223
|
+
lastCfg = JSON.parse(raw);
|
|
2224
|
+
} catch {
|
|
2225
|
+
return { ok: false, error: "No prior backup found" };
|
|
2226
|
+
}
|
|
2227
|
+
await backupCurrent(homeFn);
|
|
2228
|
+
try {
|
|
2229
|
+
await atomicWrite(cfg, JSON.stringify(lastCfg, null, 2));
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
return { ok: false, error: String(err) };
|
|
2232
|
+
}
|
|
2233
|
+
await appendHistory(oldCfg, lastCfg, "Restored from config.json.last", homeFn);
|
|
2234
|
+
return { ok: true };
|
|
2235
|
+
}
|
|
1863
2236
|
|
|
1864
2237
|
// src/picker.ts
|
|
2238
|
+
var theme = { primary: color.amber };
|
|
2239
|
+
async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
|
|
2240
|
+
try {
|
|
2241
|
+
const { atomicWrite: atomicWrite7 } = await import('@wrongstack/core');
|
|
2242
|
+
const fs17 = await import('fs/promises');
|
|
2243
|
+
let existing = {};
|
|
2244
|
+
try {
|
|
2245
|
+
const raw = await fs17.readFile(configPath2, "utf8");
|
|
2246
|
+
existing = JSON.parse(raw);
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
const oldCfg = { ...existing };
|
|
2250
|
+
existing.provider = provider;
|
|
2251
|
+
existing.model = model;
|
|
2252
|
+
await backupCurrent(homeFn);
|
|
2253
|
+
await atomicWrite7(configPath2, JSON.stringify(existing, null, 2));
|
|
2254
|
+
try {
|
|
2255
|
+
await appendHistory(
|
|
2256
|
+
oldCfg,
|
|
2257
|
+
existing,
|
|
2258
|
+
`Provider/model changed: ${oldCfg.provider ?? "(none)"} \u2192 ${provider}, ${oldCfg.model ?? "(none)"} \u2192 ${model}`,
|
|
2259
|
+
homeFn
|
|
2260
|
+
);
|
|
2261
|
+
} catch {
|
|
2262
|
+
}
|
|
2263
|
+
return true;
|
|
2264
|
+
} catch {
|
|
2265
|
+
return false;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
1865
2268
|
async function runPicker(deps) {
|
|
1866
2269
|
const { modelsRegistry, renderer, reader, config, defaultProvider, defaultModel } = deps;
|
|
1867
2270
|
renderer.write(
|
|
@@ -2092,28 +2495,9 @@ async function resolveModelSelection(answer, models, provider, _registry, render
|
|
|
2092
2495
|
`);
|
|
2093
2496
|
return { provider: provider.id, model: modelId };
|
|
2094
2497
|
}
|
|
2095
|
-
var theme = { primary: color.amber };
|
|
2096
|
-
async function saveToGlobalConfig(configPath, provider, model) {
|
|
2097
|
-
try {
|
|
2098
|
-
const { atomicWrite: atomicWrite6 } = await import('@wrongstack/core');
|
|
2099
|
-
const fs15 = await import('fs/promises');
|
|
2100
|
-
let existing = {};
|
|
2101
|
-
try {
|
|
2102
|
-
const raw = await fs15.readFile(configPath, "utf8");
|
|
2103
|
-
existing = JSON.parse(raw);
|
|
2104
|
-
} catch {
|
|
2105
|
-
}
|
|
2106
|
-
existing.provider = provider;
|
|
2107
|
-
existing.model = model;
|
|
2108
|
-
await atomicWrite6(configPath, JSON.stringify(existing, null, 2));
|
|
2109
|
-
return true;
|
|
2110
|
-
} catch {
|
|
2111
|
-
return false;
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
2498
|
async function pathExists(file) {
|
|
2115
2499
|
try {
|
|
2116
|
-
await
|
|
2500
|
+
await fs3.access(file);
|
|
2117
2501
|
return true;
|
|
2118
2502
|
} catch {
|
|
2119
2503
|
return false;
|
|
@@ -2124,10 +2508,10 @@ async function detectPackageManager(root, declared) {
|
|
|
2124
2508
|
const name = declared.split("@")[0];
|
|
2125
2509
|
if (name) return name;
|
|
2126
2510
|
}
|
|
2127
|
-
if (await pathExists(
|
|
2128
|
-
if (await pathExists(
|
|
2129
|
-
if (await pathExists(
|
|
2130
|
-
if (await pathExists(
|
|
2511
|
+
if (await pathExists(path20.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2512
|
+
if (await pathExists(path20.join(root, "bun.lockb"))) return "bun";
|
|
2513
|
+
if (await pathExists(path20.join(root, "bun.lock"))) return "bun";
|
|
2514
|
+
if (await pathExists(path20.join(root, "yarn.lock"))) return "yarn";
|
|
2131
2515
|
return "npm";
|
|
2132
2516
|
}
|
|
2133
2517
|
function hasUsableScript(scripts, name) {
|
|
@@ -2148,7 +2532,7 @@ function parseMakeTargets(makefile) {
|
|
|
2148
2532
|
async function detectProjectFacts(root) {
|
|
2149
2533
|
const facts = { hints: [] };
|
|
2150
2534
|
try {
|
|
2151
|
-
const pkg = JSON.parse(await
|
|
2535
|
+
const pkg = JSON.parse(await fs3.readFile(path20.join(root, "package.json"), "utf8"));
|
|
2152
2536
|
const scripts = pkg.scripts ?? {};
|
|
2153
2537
|
const pm = await detectPackageManager(root, pkg.packageManager);
|
|
2154
2538
|
if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
|
|
@@ -2162,14 +2546,14 @@ async function detectProjectFacts(root) {
|
|
|
2162
2546
|
} catch {
|
|
2163
2547
|
}
|
|
2164
2548
|
try {
|
|
2165
|
-
if (!await pathExists(
|
|
2549
|
+
if (!await pathExists(path20.join(root, "pyproject.toml"))) throw new Error("not python");
|
|
2166
2550
|
facts.test ??= "pytest";
|
|
2167
2551
|
facts.lint ??= "ruff check .";
|
|
2168
2552
|
facts.hints.push("pyproject.toml");
|
|
2169
2553
|
} catch {
|
|
2170
2554
|
}
|
|
2171
2555
|
try {
|
|
2172
|
-
if (!await pathExists(
|
|
2556
|
+
if (!await pathExists(path20.join(root, "go.mod"))) throw new Error("not go");
|
|
2173
2557
|
facts.build ??= "go build ./...";
|
|
2174
2558
|
facts.test ??= "go test ./...";
|
|
2175
2559
|
facts.run ??= "go run .";
|
|
@@ -2177,7 +2561,7 @@ async function detectProjectFacts(root) {
|
|
|
2177
2561
|
} catch {
|
|
2178
2562
|
}
|
|
2179
2563
|
try {
|
|
2180
|
-
if (!await pathExists(
|
|
2564
|
+
if (!await pathExists(path20.join(root, "Cargo.toml"))) throw new Error("not rust");
|
|
2181
2565
|
facts.build ??= "cargo build";
|
|
2182
2566
|
facts.test ??= "cargo test";
|
|
2183
2567
|
facts.lint ??= "cargo clippy";
|
|
@@ -2186,7 +2570,7 @@ async function detectProjectFacts(root) {
|
|
|
2186
2570
|
} catch {
|
|
2187
2571
|
}
|
|
2188
2572
|
try {
|
|
2189
|
-
const makefile = await
|
|
2573
|
+
const makefile = await fs3.readFile(path20.join(root, "Makefile"), "utf8");
|
|
2190
2574
|
const targets = parseMakeTargets(makefile);
|
|
2191
2575
|
facts.build ??= targets.has("build") ? "make build" : "make";
|
|
2192
2576
|
if (targets.has("test")) facts.test ??= "make test";
|
|
@@ -2320,6 +2704,179 @@ function buildClearCommand(opts) {
|
|
|
2320
2704
|
}
|
|
2321
2705
|
};
|
|
2322
2706
|
}
|
|
2707
|
+
async function runGit(args, cwd) {
|
|
2708
|
+
return new Promise((resolve4) => {
|
|
2709
|
+
const child = spawn("git", args, {
|
|
2710
|
+
cwd,
|
|
2711
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2712
|
+
});
|
|
2713
|
+
let stdout = "";
|
|
2714
|
+
let stderr = "";
|
|
2715
|
+
child.stdout?.on("data", (d) => stdout += d);
|
|
2716
|
+
child.stderr?.on("data", (d) => stderr += d);
|
|
2717
|
+
child.on("close", (code) => resolve4({ stdout, stderr, code: code ?? 0 }));
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
function detectCommitType(stats) {
|
|
2721
|
+
const lines = stats.split("\n");
|
|
2722
|
+
const hasTestFiles = lines.some(
|
|
2723
|
+
(l) => l.includes("_test.") || l.includes(".test.") || l.includes(".spec.")
|
|
2724
|
+
);
|
|
2725
|
+
const hasDocs = lines.some(
|
|
2726
|
+
(l) => l.includes("README") || l.includes("CHANGELOG") || l.includes("docs/") || l.includes(".md")
|
|
2727
|
+
);
|
|
2728
|
+
const hasConfig = lines.some(
|
|
2729
|
+
(l) => l.includes("config") || l.includes("tsconfig") || l.includes(".json")
|
|
2730
|
+
);
|
|
2731
|
+
if (hasTestFiles) return "test";
|
|
2732
|
+
if (hasDocs) return "docs";
|
|
2733
|
+
if (hasConfig) return "chore";
|
|
2734
|
+
return "feat";
|
|
2735
|
+
}
|
|
2736
|
+
async function generateCommitMessageHeuristics(cwd) {
|
|
2737
|
+
const statsResult = await runGit(["diff", "--stat"], cwd);
|
|
2738
|
+
if (statsResult.code !== 0) return "chore: update";
|
|
2739
|
+
const nameResult = await runGit(["diff", "--name-only"], cwd);
|
|
2740
|
+
const files = nameResult.stdout.split("\n").filter(Boolean);
|
|
2741
|
+
const commitType = detectCommitType(statsResult.stdout);
|
|
2742
|
+
let scope = "";
|
|
2743
|
+
if (files.length > 0) {
|
|
2744
|
+
const primary = files[0].split("/")[0];
|
|
2745
|
+
if (primary && primary !== "packages" && primary !== "apps" && primary !== "node_modules") {
|
|
2746
|
+
scope = `(${primary})`;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
if (files.length === 0) {
|
|
2750
|
+
return `${commitType}${scope}: update`;
|
|
2751
|
+
}
|
|
2752
|
+
if (files.length <= 3) {
|
|
2753
|
+
const summary2 = files.map((f) => f.split("/").pop()).join(", ");
|
|
2754
|
+
return `${commitType}${scope}: ${summary2}`;
|
|
2755
|
+
}
|
|
2756
|
+
const summary = files.slice(0, 3).map((f) => f.split("/").pop()).join(", ") + ` and ${files.length - 3} more`;
|
|
2757
|
+
return `${commitType}${scope}: ${summary}`;
|
|
2758
|
+
}
|
|
2759
|
+
async function hasUncommittedChanges(cwd) {
|
|
2760
|
+
const result = await runGit(["status", "--porcelain"], cwd);
|
|
2761
|
+
return result.stdout.trim().length > 0;
|
|
2762
|
+
}
|
|
2763
|
+
async function isGitRepo(cwd) {
|
|
2764
|
+
const result = await runGit(["rev-parse", "--git-dir"], cwd);
|
|
2765
|
+
return result.code === 0;
|
|
2766
|
+
}
|
|
2767
|
+
function buildCommitCommand(_opts, generateCommitMessage) {
|
|
2768
|
+
return {
|
|
2769
|
+
name: "commit",
|
|
2770
|
+
description: "Stage all changes and commit with auto-generated message.",
|
|
2771
|
+
aliases: ["gc"],
|
|
2772
|
+
async run(args, ctx) {
|
|
2773
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2774
|
+
if (!await isGitRepo(cwd)) {
|
|
2775
|
+
return { message: "Not a git repository." };
|
|
2776
|
+
}
|
|
2777
|
+
if (!await hasUncommittedChanges(cwd)) {
|
|
2778
|
+
return { message: "Nothing to commit (working tree clean)." };
|
|
2779
|
+
}
|
|
2780
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2781
|
+
args.includes("--no-llm");
|
|
2782
|
+
let message;
|
|
2783
|
+
{
|
|
2784
|
+
message = await generateCommitMessageHeuristics(cwd);
|
|
2785
|
+
}
|
|
2786
|
+
if (dryRun) {
|
|
2787
|
+
return {
|
|
2788
|
+
message: `Would commit:
|
|
2789
|
+
|
|
2790
|
+
${color.green(message)}
|
|
2791
|
+
|
|
2792
|
+
${color.dim("(dry-run \u2014 no actual commit)")}`
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
const stageResult = await runGit(["add", "."], cwd);
|
|
2796
|
+
if (stageResult.code !== 0) {
|
|
2797
|
+
return { message: `Stage failed: ${stageResult.stderr}` };
|
|
2798
|
+
}
|
|
2799
|
+
const commitResult = await runGit(["commit", "-m", message], cwd);
|
|
2800
|
+
if (commitResult.code !== 0) {
|
|
2801
|
+
return { message: `Commit failed: ${commitResult.stderr}` };
|
|
2802
|
+
}
|
|
2803
|
+
const hashResult = await runGit(["rev-parse", "--short", "HEAD"], cwd);
|
|
2804
|
+
const hash = hashResult.stdout.trim();
|
|
2805
|
+
const pushResult = await runGit(["remote"], cwd);
|
|
2806
|
+
const hasRemote = pushResult.stdout.trim().length > 0;
|
|
2807
|
+
let pushMsg = "";
|
|
2808
|
+
if (hasRemote) {
|
|
2809
|
+
pushMsg = `
|
|
2810
|
+
|
|
2811
|
+
${color.dim("Tip: Run /push to push to remote")}`;
|
|
2812
|
+
}
|
|
2813
|
+
return {
|
|
2814
|
+
message: `${color.green("\u2713")} Committed: ${color.bold(message)}
|
|
2815
|
+
${color.dim(hash)}${pushMsg}`
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function buildGitcheckCommand(_opts) {
|
|
2821
|
+
return {
|
|
2822
|
+
name: "gitcheck",
|
|
2823
|
+
description: "Check for uncommitted changes (for system prompt integration).",
|
|
2824
|
+
aliases: ["gcstatus"],
|
|
2825
|
+
async run(_args, ctx) {
|
|
2826
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2827
|
+
if (!await isGitRepo(cwd)) {
|
|
2828
|
+
return { message: "" };
|
|
2829
|
+
}
|
|
2830
|
+
if (!await hasUncommittedChanges(cwd)) {
|
|
2831
|
+
return { message: "" };
|
|
2832
|
+
}
|
|
2833
|
+
const statusResult = await runGit(["status", "--porcelain"], cwd);
|
|
2834
|
+
const lines = statusResult.stdout.split("\n").filter(Boolean);
|
|
2835
|
+
const count = lines.length;
|
|
2836
|
+
if (count === 0) return { message: "" };
|
|
2837
|
+
return {
|
|
2838
|
+
message: `\u26A0 ${color.yellow(`${count} uncommitted change${count > 1 ? "s" : ""}`)} \u2014 consider /commit`
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
function buildPushCommand(_opts) {
|
|
2844
|
+
return {
|
|
2845
|
+
name: "push",
|
|
2846
|
+
description: "Push to remote after commit.",
|
|
2847
|
+
async run(args, ctx) {
|
|
2848
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2849
|
+
if (!await isGitRepo(cwd)) {
|
|
2850
|
+
return { message: "Not a git repository." };
|
|
2851
|
+
}
|
|
2852
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2853
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
2854
|
+
const remoteResult = await runGit(["remote"], cwd);
|
|
2855
|
+
const remotes = remoteResult.stdout.split("\n").filter(Boolean);
|
|
2856
|
+
if (remotes.length === 0) {
|
|
2857
|
+
return { message: "No remote configured. Add one with: git remote add origin <url>" };
|
|
2858
|
+
}
|
|
2859
|
+
if (dryRun) {
|
|
2860
|
+
return {
|
|
2861
|
+
message: `Would push to ${remotes.join(", ")}${force ? " (force)" : ""}
|
|
2862
|
+
${color.dim("(dry-run)")}`
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
const branchResult = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
2866
|
+
const branch = branchResult.stdout.trim() || "main";
|
|
2867
|
+
const pushArgs = ["push"];
|
|
2868
|
+
if (force) pushArgs.push("--force");
|
|
2869
|
+
pushArgs.push(...remotes, branch);
|
|
2870
|
+
const pushResult = await runGit(pushArgs, cwd);
|
|
2871
|
+
if (pushResult.code !== 0) {
|
|
2872
|
+
return { message: `Push failed: ${pushResult.stderr}` };
|
|
2873
|
+
}
|
|
2874
|
+
return {
|
|
2875
|
+
message: `${color.green("\u2713")} Pushed to ${remotes.join(", ")} (${branch})`
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2323
2880
|
|
|
2324
2881
|
// src/slash-commands/compact.ts
|
|
2325
2882
|
function buildCompactCommand(opts) {
|
|
@@ -2649,10 +3206,10 @@ function buildInitCommand(opts) {
|
|
|
2649
3206
|
description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
|
|
2650
3207
|
async run(args, ctx) {
|
|
2651
3208
|
const force = args.trim() === "--force";
|
|
2652
|
-
const dir =
|
|
2653
|
-
const file =
|
|
3209
|
+
const dir = path20.join(ctx.projectRoot, ".wrongstack");
|
|
3210
|
+
const file = path20.join(dir, "AGENTS.md");
|
|
2654
3211
|
try {
|
|
2655
|
-
await
|
|
3212
|
+
await fs3.access(file);
|
|
2656
3213
|
if (!force) {
|
|
2657
3214
|
const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
2658
3215
|
opts.renderer.writeWarning(msg2);
|
|
@@ -2662,8 +3219,8 @@ function buildInitCommand(opts) {
|
|
|
2662
3219
|
}
|
|
2663
3220
|
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
2664
3221
|
const body = renderAgentsTemplate(detected);
|
|
2665
|
-
await
|
|
2666
|
-
await
|
|
3222
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
3223
|
+
await fs3.writeFile(file, body, "utf8");
|
|
2667
3224
|
if (detected.hints.length > 0) {
|
|
2668
3225
|
const msg2 = `Wrote ${file}
|
|
2669
3226
|
Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
|
|
@@ -2892,6 +3449,13 @@ function buildExitCommand(opts) {
|
|
|
2892
3449
|
aliases: ["quit", "q"],
|
|
2893
3450
|
description: "Exit the REPL.",
|
|
2894
3451
|
async run() {
|
|
3452
|
+
if (opts.onBeforeExit) {
|
|
3453
|
+
const result = await opts.onBeforeExit();
|
|
3454
|
+
if (result?.abort) {
|
|
3455
|
+
opts.onExit?.();
|
|
3456
|
+
return { message: result.message ?? "", exit: true };
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
2895
3459
|
opts.onExit?.();
|
|
2896
3460
|
return { exit: true };
|
|
2897
3461
|
}
|
|
@@ -3264,11 +3828,11 @@ ${lines.join("\n\n")}
|
|
|
3264
3828
|
};
|
|
3265
3829
|
}
|
|
3266
3830
|
function makeInstaller(opts, projectRoot, global) {
|
|
3267
|
-
const globalRoot =
|
|
3831
|
+
const globalRoot = path20.join(os6.homedir(), ".wrongstack");
|
|
3268
3832
|
return new SkillInstaller({
|
|
3269
|
-
manifestPath:
|
|
3270
|
-
projectSkillsDir:
|
|
3271
|
-
globalSkillsDir:
|
|
3833
|
+
manifestPath: path20.join(globalRoot, "installed-skills.json"),
|
|
3834
|
+
projectSkillsDir: path20.join(projectRoot, ".wrongstack", "skills"),
|
|
3835
|
+
globalSkillsDir: path20.join(globalRoot, "skills"),
|
|
3272
3836
|
projectHash: projectHash(projectRoot),
|
|
3273
3837
|
skillLoader: opts.skillLoader
|
|
3274
3838
|
});
|
|
@@ -3457,7 +4021,10 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
3457
4021
|
buildYoloCommand(opts),
|
|
3458
4022
|
buildAutonomyCommand(opts),
|
|
3459
4023
|
buildModeCommand(opts),
|
|
3460
|
-
buildExitCommand(opts)
|
|
4024
|
+
buildExitCommand(opts),
|
|
4025
|
+
buildCommitCommand(),
|
|
4026
|
+
buildGitcheckCommand(),
|
|
4027
|
+
buildPushCommand()
|
|
3461
4028
|
];
|
|
3462
4029
|
}
|
|
3463
4030
|
|
|
@@ -3476,13 +4043,13 @@ var MANIFESTS = [
|
|
|
3476
4043
|
];
|
|
3477
4044
|
async function detectProjectKind(projectRoot) {
|
|
3478
4045
|
try {
|
|
3479
|
-
await
|
|
4046
|
+
await fs3.access(path20.join(projectRoot, ".wrongstack", "AGENTS.md"));
|
|
3480
4047
|
return "initialized";
|
|
3481
4048
|
} catch {
|
|
3482
4049
|
}
|
|
3483
4050
|
for (const m of MANIFESTS) {
|
|
3484
4051
|
try {
|
|
3485
|
-
await
|
|
4052
|
+
await fs3.access(path20.join(projectRoot, m));
|
|
3486
4053
|
return "project";
|
|
3487
4054
|
} catch {
|
|
3488
4055
|
}
|
|
@@ -3490,12 +4057,12 @@ async function detectProjectKind(projectRoot) {
|
|
|
3490
4057
|
return "empty";
|
|
3491
4058
|
}
|
|
3492
4059
|
async function scaffoldAgentsMd(projectRoot) {
|
|
3493
|
-
const dir =
|
|
3494
|
-
const file =
|
|
4060
|
+
const dir = path20.join(projectRoot, ".wrongstack");
|
|
4061
|
+
const file = path20.join(dir, "AGENTS.md");
|
|
3495
4062
|
const facts = await detectProjectFacts(projectRoot);
|
|
3496
4063
|
const body = renderAgentsTemplate(facts);
|
|
3497
|
-
await
|
|
3498
|
-
await
|
|
4064
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
4065
|
+
await fs3.writeFile(file, body, "utf8");
|
|
3499
4066
|
return file;
|
|
3500
4067
|
}
|
|
3501
4068
|
async function runProjectCheck(opts) {
|
|
@@ -3504,7 +4071,7 @@ async function runProjectCheck(opts) {
|
|
|
3504
4071
|
if (kind === "initialized") {
|
|
3505
4072
|
renderer.write(
|
|
3506
4073
|
`
|
|
3507
|
-
${color.green("\u2713")} Project initialized ${color.dim(`(${
|
|
4074
|
+
${color.green("\u2713")} Project initialized ${color.dim(`(${path20.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
|
|
3508
4075
|
`
|
|
3509
4076
|
);
|
|
3510
4077
|
return true;
|
|
@@ -3531,11 +4098,43 @@ async function runProjectCheck(opts) {
|
|
|
3531
4098
|
}
|
|
3532
4099
|
return true;
|
|
3533
4100
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
4101
|
+
const gitDir = path20.join(projectRoot, ".git");
|
|
4102
|
+
let hasGit = false;
|
|
4103
|
+
try {
|
|
4104
|
+
await fs3.access(gitDir);
|
|
4105
|
+
hasGit = true;
|
|
4106
|
+
} catch {
|
|
4107
|
+
}
|
|
4108
|
+
if (!hasGit) {
|
|
4109
|
+
renderer.write(
|
|
4110
|
+
`
|
|
3536
4111
|
${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
|
|
3537
4112
|
`
|
|
3538
|
-
|
|
4113
|
+
);
|
|
4114
|
+
const answer2 = (await reader.readLine(
|
|
4115
|
+
` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N]")} `
|
|
4116
|
+
)).trim().toLowerCase();
|
|
4117
|
+
if (answer2 === "y" || answer2 === "yes") {
|
|
4118
|
+
try {
|
|
4119
|
+
const { spawn: spawn3 } = await import('child_process');
|
|
4120
|
+
await new Promise((resolve4, reject) => {
|
|
4121
|
+
const child = spawn3("git", ["init"], { cwd: projectRoot });
|
|
4122
|
+
child.on("close", (code) => code === 0 ? resolve4() : reject(new Error(`git init failed with ${code}`)));
|
|
4123
|
+
});
|
|
4124
|
+
renderer.write(` ${color.green("\u2713")} Git repository initialized
|
|
4125
|
+
`);
|
|
4126
|
+
} catch (err) {
|
|
4127
|
+
renderer.writeError(`git init failed: ${err instanceof Error ? err.message : String(err)}
|
|
4128
|
+
`);
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
} else {
|
|
4132
|
+
renderer.write(
|
|
4133
|
+
`
|
|
4134
|
+
${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
|
|
4135
|
+
`
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
3539
4138
|
const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n]")} `)).trim().toLowerCase();
|
|
3540
4139
|
if (answer === "n" || answer === "no") {
|
|
3541
4140
|
renderer.write(color.dim(" Cancelled.\n"));
|
|
@@ -3800,14 +4399,14 @@ function summarize(value, name) {
|
|
|
3800
4399
|
if (typeof v === "object" && v !== null) {
|
|
3801
4400
|
const o = v;
|
|
3802
4401
|
if (name === "edit") {
|
|
3803
|
-
const
|
|
4402
|
+
const path21 = typeof o["path"] === "string" ? o["path"] : "";
|
|
3804
4403
|
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
3805
|
-
return `${
|
|
4404
|
+
return `${path21} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
3806
4405
|
}
|
|
3807
4406
|
if (name === "write") {
|
|
3808
|
-
const
|
|
4407
|
+
const path21 = typeof o["path"] === "string" ? o["path"] : "";
|
|
3809
4408
|
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
3810
|
-
return bytes !== void 0 ? `${
|
|
4409
|
+
return bytes !== void 0 ? `${path21} ${bytes}B` : path21;
|
|
3811
4410
|
}
|
|
3812
4411
|
if (typeof o["count"] === "number") {
|
|
3813
4412
|
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
@@ -4403,7 +5002,7 @@ async function readKeyInput(deps, intent) {
|
|
|
4403
5002
|
async function loadProviders(deps) {
|
|
4404
5003
|
let raw;
|
|
4405
5004
|
try {
|
|
4406
|
-
raw = await
|
|
5005
|
+
raw = await fs3.readFile(deps.globalConfigPath, "utf8");
|
|
4407
5006
|
} catch {
|
|
4408
5007
|
return {};
|
|
4409
5008
|
}
|
|
@@ -4419,7 +5018,7 @@ async function loadProviders(deps) {
|
|
|
4419
5018
|
async function mutateProviders(deps, mutator) {
|
|
4420
5019
|
let raw;
|
|
4421
5020
|
try {
|
|
4422
|
-
raw = await
|
|
5021
|
+
raw = await fs3.readFile(deps.globalConfigPath, "utf8");
|
|
4423
5022
|
} catch {
|
|
4424
5023
|
raw = "{}";
|
|
4425
5024
|
}
|
|
@@ -4456,6 +5055,66 @@ var authCmd = async (args, deps) => {
|
|
|
4456
5055
|
envVars: flags.envVars
|
|
4457
5056
|
});
|
|
4458
5057
|
};
|
|
5058
|
+
|
|
5059
|
+
// src/subcommands/handlers/update.ts
|
|
5060
|
+
init_update_check();
|
|
5061
|
+
var updateCmd = async (args, deps) => {
|
|
5062
|
+
const cwd = deps.cwd;
|
|
5063
|
+
const checkOnly = args.includes("--check-only") || args.includes("-c");
|
|
5064
|
+
const info = await checkForUpdate();
|
|
5065
|
+
if (checkOnly) {
|
|
5066
|
+
if (info.outdated) {
|
|
5067
|
+
deps.renderer.write(`Update available: v${info.current} \u2192 v${info.latest}
|
|
5068
|
+
`);
|
|
5069
|
+
} else {
|
|
5070
|
+
deps.renderer.write(`You are on the latest version: v${info.current}
|
|
5071
|
+
`);
|
|
5072
|
+
}
|
|
5073
|
+
return 0;
|
|
5074
|
+
}
|
|
5075
|
+
if (!info.outdated) {
|
|
5076
|
+
deps.renderer.write(`You are already on the latest version: v${info.current}
|
|
5077
|
+
`);
|
|
5078
|
+
return 0;
|
|
5079
|
+
}
|
|
5080
|
+
deps.renderer.write(`Updating wrongstack from v${info.current} to v${info.latest}...
|
|
5081
|
+
`);
|
|
5082
|
+
try {
|
|
5083
|
+
const result = await new Promise((resolve4) => {
|
|
5084
|
+
const child = spawn("npm", ["install", "-g", "wrongstack@latest"], {
|
|
5085
|
+
cwd,
|
|
5086
|
+
stdio: "pipe"
|
|
5087
|
+
});
|
|
5088
|
+
let stderr = "";
|
|
5089
|
+
child.stderr?.on("data", (d) => {
|
|
5090
|
+
stderr += d;
|
|
5091
|
+
});
|
|
5092
|
+
child.on("close", (code) => resolve4({ code: code ?? 0 }));
|
|
5093
|
+
});
|
|
5094
|
+
if (result.code === 0) {
|
|
5095
|
+
deps.renderer.write(`
|
|
5096
|
+
Updated to v${info.latest}. Restart wrongstack to use the new version.
|
|
5097
|
+
`);
|
|
5098
|
+
} else {
|
|
5099
|
+
deps.renderer.write(`
|
|
5100
|
+
Update failed with exit code ${result.code}.
|
|
5101
|
+
`);
|
|
5102
|
+
}
|
|
5103
|
+
return result.code;
|
|
5104
|
+
} catch (err) {
|
|
5105
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5106
|
+
if (msg.includes("ENOENT")) {
|
|
5107
|
+
deps.renderer.write(`
|
|
5108
|
+
Update failed: npm not found in PATH.
|
|
5109
|
+
`);
|
|
5110
|
+
return 1;
|
|
5111
|
+
}
|
|
5112
|
+
deps.renderer.write(`
|
|
5113
|
+
Update failed: ${msg}
|
|
5114
|
+
`);
|
|
5115
|
+
return 1;
|
|
5116
|
+
}
|
|
5117
|
+
};
|
|
4459
5118
|
var req = createRequire(import.meta.url);
|
|
4460
5119
|
function readOwnVersion() {
|
|
4461
5120
|
const candidates = ["../package.json", "../../package.json"];
|
|
@@ -4491,7 +5150,7 @@ var diagCmd = async (_args, deps) => {
|
|
|
4491
5150
|
` modelsCache: ${deps.paths.modelsCache}`,
|
|
4492
5151
|
` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
|
|
4493
5152
|
` node: ${process.version}`,
|
|
4494
|
-
` os: ${
|
|
5153
|
+
` os: ${os6.platform()} ${os6.release()}`,
|
|
4495
5154
|
` provider: ${cfg.provider ?? "<unset>"}`,
|
|
4496
5155
|
` model: ${cfg.model ?? "<unset>"}`,
|
|
4497
5156
|
` tools: ${deps.toolRegistry?.list().length ?? 0}`,
|
|
@@ -4559,7 +5218,7 @@ var doctorCmd = async (_args, deps) => {
|
|
|
4559
5218
|
});
|
|
4560
5219
|
}
|
|
4561
5220
|
try {
|
|
4562
|
-
await
|
|
5221
|
+
await fs3.access(deps.paths.secretsKey);
|
|
4563
5222
|
checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
|
|
4564
5223
|
} catch {
|
|
4565
5224
|
checks.push({
|
|
@@ -4569,10 +5228,10 @@ var doctorCmd = async (_args, deps) => {
|
|
|
4569
5228
|
});
|
|
4570
5229
|
}
|
|
4571
5230
|
try {
|
|
4572
|
-
await
|
|
4573
|
-
const probe =
|
|
4574
|
-
await
|
|
4575
|
-
await
|
|
5231
|
+
await fs3.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
5232
|
+
const probe = path20.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
5233
|
+
await fs3.writeFile(probe, "");
|
|
5234
|
+
await fs3.unlink(probe);
|
|
4576
5235
|
checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
|
|
4577
5236
|
} catch (err) {
|
|
4578
5237
|
checks.push({
|
|
@@ -4673,8 +5332,8 @@ var exportCmd = async (args, deps) => {
|
|
|
4673
5332
|
return 1;
|
|
4674
5333
|
}
|
|
4675
5334
|
if (output) {
|
|
4676
|
-
await
|
|
4677
|
-
await
|
|
5335
|
+
await fs3.mkdir(path20.dirname(path20.resolve(deps.cwd, output)), { recursive: true });
|
|
5336
|
+
await fs3.writeFile(path20.resolve(deps.cwd, output), rendered, "utf8");
|
|
4678
5337
|
deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
|
|
4679
5338
|
`);
|
|
4680
5339
|
} else {
|
|
@@ -4731,17 +5390,17 @@ var initCmd = async (_args, deps) => {
|
|
|
4731
5390
|
} else {
|
|
4732
5391
|
deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
|
|
4733
5392
|
}
|
|
4734
|
-
await
|
|
5393
|
+
await fs3.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
4735
5394
|
const config = { version: 1, provider: providerId, model: modelId };
|
|
4736
5395
|
if (apiKey) config.apiKey = apiKey;
|
|
4737
|
-
const keyFile =
|
|
5396
|
+
const keyFile = path20.join(path20.dirname(deps.paths.globalConfig), ".key");
|
|
4738
5397
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
4739
5398
|
const encrypted = encryptConfigSecrets(config, vault);
|
|
4740
5399
|
await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
|
|
4741
|
-
await
|
|
4742
|
-
const agentsFile =
|
|
5400
|
+
await fs3.mkdir(path20.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
5401
|
+
const agentsFile = path20.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
4743
5402
|
try {
|
|
4744
|
-
await
|
|
5403
|
+
await fs3.access(agentsFile);
|
|
4745
5404
|
} catch {
|
|
4746
5405
|
const detected2 = await detectProjectFacts(deps.projectRoot);
|
|
4747
5406
|
await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
|
|
@@ -4817,7 +5476,7 @@ async function addMcpServer(args, deps) {
|
|
|
4817
5476
|
serverCfg.enabled = enable;
|
|
4818
5477
|
let existing = {};
|
|
4819
5478
|
try {
|
|
4820
|
-
existing = JSON.parse(await
|
|
5479
|
+
existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
|
|
4821
5480
|
} catch {
|
|
4822
5481
|
}
|
|
4823
5482
|
const mcpServers = existing.mcpServers ?? {};
|
|
@@ -4837,7 +5496,7 @@ async function addMcpServer(args, deps) {
|
|
|
4837
5496
|
async function removeMcpServer(name, deps) {
|
|
4838
5497
|
let existing = {};
|
|
4839
5498
|
try {
|
|
4840
|
-
existing = JSON.parse(await
|
|
5499
|
+
existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
|
|
4841
5500
|
} catch {
|
|
4842
5501
|
deps.renderer.writeError("No config file found.\n");
|
|
4843
5502
|
return 1;
|
|
@@ -4958,7 +5617,7 @@ function renderConfiguredPlugins(config) {
|
|
|
4958
5617
|
}
|
|
4959
5618
|
async function readConfig(file) {
|
|
4960
5619
|
try {
|
|
4961
|
-
return JSON.parse(await
|
|
5620
|
+
return JSON.parse(await fs3.readFile(file, "utf8"));
|
|
4962
5621
|
} catch {
|
|
4963
5622
|
return {};
|
|
4964
5623
|
}
|
|
@@ -5049,9 +5708,9 @@ var usageCmd = async (_args, deps) => {
|
|
|
5049
5708
|
return 0;
|
|
5050
5709
|
};
|
|
5051
5710
|
var projectsCmd = async (_args, deps) => {
|
|
5052
|
-
const projectsRoot =
|
|
5711
|
+
const projectsRoot = path20.join(deps.paths.globalRoot, "projects");
|
|
5053
5712
|
try {
|
|
5054
|
-
const entries = await
|
|
5713
|
+
const entries = await fs3.readdir(projectsRoot);
|
|
5055
5714
|
if (entries.length === 0) {
|
|
5056
5715
|
deps.renderer.write("No projects tracked.\n");
|
|
5057
5716
|
return 0;
|
|
@@ -5059,7 +5718,7 @@ var projectsCmd = async (_args, deps) => {
|
|
|
5059
5718
|
for (const hash of entries) {
|
|
5060
5719
|
try {
|
|
5061
5720
|
const meta = JSON.parse(
|
|
5062
|
-
await
|
|
5721
|
+
await fs3.readFile(path20.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
5063
5722
|
);
|
|
5064
5723
|
deps.renderer.write(
|
|
5065
5724
|
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
@@ -5233,9 +5892,219 @@ var configCmd = async (args, deps) => {
|
|
|
5233
5892
|
`);
|
|
5234
5893
|
return 0;
|
|
5235
5894
|
}
|
|
5236
|
-
|
|
5895
|
+
if (sub === "history") {
|
|
5896
|
+
return runHistory(args.slice(1), deps);
|
|
5897
|
+
}
|
|
5898
|
+
if (sub === "restore") {
|
|
5899
|
+
return runRestore(args.slice(1), deps);
|
|
5900
|
+
}
|
|
5901
|
+
deps.renderer.writeError(`Unknown config subcommand: ${sub}
|
|
5902
|
+
`);
|
|
5237
5903
|
return 1;
|
|
5238
5904
|
};
|
|
5905
|
+
function extractArg(args, key) {
|
|
5906
|
+
const idx = args.indexOf(key);
|
|
5907
|
+
if (idx !== -1 && args[idx + 1] !== void 0) return args[idx + 1];
|
|
5908
|
+
const eq = key.startsWith("--") ? args.find((a) => a.startsWith(`${key}=`)) : null;
|
|
5909
|
+
if (eq) return eq.slice(eq.indexOf("=") + 1);
|
|
5910
|
+
return null;
|
|
5911
|
+
}
|
|
5912
|
+
async function runHistory(args, deps) {
|
|
5913
|
+
const idFlag = extractArg(args, "--id");
|
|
5914
|
+
if (idFlag) {
|
|
5915
|
+
const entry = await getHistoryEntry(idFlag);
|
|
5916
|
+
if (!entry) {
|
|
5917
|
+
deps.renderer.writeError(`History entry '${idFlag}' not found.
|
|
5918
|
+
`);
|
|
5919
|
+
return 1;
|
|
5920
|
+
}
|
|
5921
|
+
deps.renderer.write(
|
|
5922
|
+
[
|
|
5923
|
+
`ID: ${entry.id}`,
|
|
5924
|
+
`Time: ${new Date(entry.timestamp).toLocaleString()}`,
|
|
5925
|
+
`Change: ${entry.description}`,
|
|
5926
|
+
`Diff: ${entry.diffSummary}`,
|
|
5927
|
+
"",
|
|
5928
|
+
"Snapshot (secrets masked):",
|
|
5929
|
+
JSON.stringify(entry.snapshotMasked, null, 2)
|
|
5930
|
+
].join("\n") + "\n"
|
|
5931
|
+
);
|
|
5932
|
+
return 0;
|
|
5933
|
+
}
|
|
5934
|
+
const entries = await listHistory();
|
|
5935
|
+
if (entries.length === 0) {
|
|
5936
|
+
deps.renderer.write("No config history yet.\n");
|
|
5937
|
+
return 0;
|
|
5938
|
+
}
|
|
5939
|
+
deps.renderer.write(
|
|
5940
|
+
[
|
|
5941
|
+
color.bold("Config History"),
|
|
5942
|
+
"",
|
|
5943
|
+
...entries.map((e, i) => {
|
|
5944
|
+
const ts = new Date(e.timestamp).toLocaleString();
|
|
5945
|
+
const desc = e.description.length > 60 ? e.description.slice(0, 60) + "\u2026" : e.description;
|
|
5946
|
+
return ` [${i + 1}] ${e.id} ${color.dim(ts)}
|
|
5947
|
+
${desc}`;
|
|
5948
|
+
}),
|
|
5949
|
+
"",
|
|
5950
|
+
" Run `wrongstack config history --id <id>` for details.",
|
|
5951
|
+
" Run `wrongstack config restore <id>` to restore."
|
|
5952
|
+
].join("\n") + "\n"
|
|
5953
|
+
);
|
|
5954
|
+
return 0;
|
|
5955
|
+
}
|
|
5956
|
+
async function runRestore(args, deps) {
|
|
5957
|
+
const latest = args.includes("--latest") || args.includes("-l");
|
|
5958
|
+
const id = extractArg(args, "--id") ?? (args[0] && !args[0].startsWith("-") ? args[0] : null);
|
|
5959
|
+
if (latest) {
|
|
5960
|
+
const result2 = await restoreLast();
|
|
5961
|
+
if (!result2.ok) {
|
|
5962
|
+
deps.renderer.writeError(`Restore failed: ${result2.error}
|
|
5963
|
+
`);
|
|
5964
|
+
return 1;
|
|
5965
|
+
}
|
|
5966
|
+
deps.renderer.write("Restored from config.json.last.\n");
|
|
5967
|
+
return 0;
|
|
5968
|
+
}
|
|
5969
|
+
if (!id) {
|
|
5970
|
+
deps.renderer.write("Usage: wrongstack config restore <id> | --latest\n");
|
|
5971
|
+
return 1;
|
|
5972
|
+
}
|
|
5973
|
+
const result = await restoreFromHistory(id);
|
|
5974
|
+
if (!result.ok) {
|
|
5975
|
+
deps.renderer.writeError(`Restore failed: ${result.error}
|
|
5976
|
+
`);
|
|
5977
|
+
return 1;
|
|
5978
|
+
}
|
|
5979
|
+
deps.renderer.write(`Restored to history entry '${id}'. Backup created.
|
|
5980
|
+
`);
|
|
5981
|
+
return 0;
|
|
5982
|
+
}
|
|
5983
|
+
function parseRewindFlags(args) {
|
|
5984
|
+
const flags = {};
|
|
5985
|
+
for (let i = 0; i < args.length; i++) {
|
|
5986
|
+
const a = args[i];
|
|
5987
|
+
if (a === "--all") flags.all = true;
|
|
5988
|
+
else if (a === "--last") flags.last = args[++i] ?? "1";
|
|
5989
|
+
else if (a === "--to") flags.to = args[++i] ?? "";
|
|
5990
|
+
else if (a === "--list") flags.list = true;
|
|
5991
|
+
else if (a === "--resume") flags.resume = true;
|
|
5992
|
+
}
|
|
5993
|
+
return flags;
|
|
5994
|
+
}
|
|
5995
|
+
var rewindCmd = async (args, deps) => {
|
|
5996
|
+
const flags = parseRewindFlags(args);
|
|
5997
|
+
const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
|
|
5998
|
+
const sessionsDir = path20.join(wpaths.globalRoot, "sessions");
|
|
5999
|
+
const rewind = new DefaultSessionRewinder(sessionsDir);
|
|
6000
|
+
let sessionId = args.find((a) => !a.startsWith("--"));
|
|
6001
|
+
if (!sessionId) {
|
|
6002
|
+
if (!deps.sessionStore) {
|
|
6003
|
+
deps.renderer.writeError("No session store available.");
|
|
6004
|
+
return 1;
|
|
6005
|
+
}
|
|
6006
|
+
const sessions = await deps.sessionStore.list(1);
|
|
6007
|
+
if (sessions.length === 0) {
|
|
6008
|
+
deps.renderer.writeError("No sessions found.");
|
|
6009
|
+
return 1;
|
|
6010
|
+
}
|
|
6011
|
+
sessionId = sessions[0].id;
|
|
6012
|
+
}
|
|
6013
|
+
if (flags.list) {
|
|
6014
|
+
deps.renderer.write(`Session: ${color.bold(sessionId)}
|
|
6015
|
+
|
|
6016
|
+
`);
|
|
6017
|
+
const checkpoints = await rewind.listCheckpoints(sessionId);
|
|
6018
|
+
if (checkpoints.length === 0) {
|
|
6019
|
+
deps.renderer.write("No checkpoints in this session.\n");
|
|
6020
|
+
return 0;
|
|
6021
|
+
}
|
|
6022
|
+
for (const cp of checkpoints) {
|
|
6023
|
+
deps.renderer.write(
|
|
6024
|
+
` [${cp.promptIndex}] ${color.dim(cp.ts)} ${cp.promptPreview}${cp.fileCount > 0 ? color.dim(` (${cp.fileCount} file${cp.fileCount === 1 ? "" : "s"})`) : ""}
|
|
6025
|
+
`
|
|
6026
|
+
);
|
|
6027
|
+
}
|
|
6028
|
+
return 0;
|
|
6029
|
+
}
|
|
6030
|
+
try {
|
|
6031
|
+
let result;
|
|
6032
|
+
if (flags.all) {
|
|
6033
|
+
deps.renderer.write("Rewinding to session start...\n");
|
|
6034
|
+
result = await rewind.rewindToStart(sessionId);
|
|
6035
|
+
} else if (flags.last) {
|
|
6036
|
+
const n = parseInt(flags.last, 10);
|
|
6037
|
+
if (isNaN(n) || n < 1) {
|
|
6038
|
+
deps.renderer.writeError("--last requires a positive number");
|
|
6039
|
+
return 1;
|
|
6040
|
+
}
|
|
6041
|
+
deps.renderer.write(`Rewinding last ${n} prompt(s)...
|
|
6042
|
+
`);
|
|
6043
|
+
result = await rewind.rewindLastN(sessionId, n);
|
|
6044
|
+
} else if (flags.to) {
|
|
6045
|
+
const idx = parseInt(flags.to, 10);
|
|
6046
|
+
if (isNaN(idx) || idx < 0) {
|
|
6047
|
+
deps.renderer.writeError("--to requires a non-negative number");
|
|
6048
|
+
return 1;
|
|
6049
|
+
}
|
|
6050
|
+
deps.renderer.write(`Rewinding to checkpoint ${idx}...
|
|
6051
|
+
`);
|
|
6052
|
+
result = await rewind.rewindToCheckpoint(sessionId, idx);
|
|
6053
|
+
} else {
|
|
6054
|
+
deps.renderer.write("Usage: ws rewind --all | --last N | --to <index> [--list] [--resume]\n");
|
|
6055
|
+
deps.renderer.write(" --all Rewind to session start\n");
|
|
6056
|
+
deps.renderer.write(" --last N Rewind last N prompts\n");
|
|
6057
|
+
deps.renderer.write(" --to N Rewind to checkpoint N\n");
|
|
6058
|
+
deps.renderer.write(" --list List checkpoints\n");
|
|
6059
|
+
deps.renderer.write(" --resume After rewind, truncate session history at checkpoint\n");
|
|
6060
|
+
return 1;
|
|
6061
|
+
}
|
|
6062
|
+
if (result.revertedFiles.length === 0) {
|
|
6063
|
+
deps.renderer.write("No files to revert.\n");
|
|
6064
|
+
if (flags.resume) {
|
|
6065
|
+
const store = new DefaultSessionStore({ dir: sessionsDir });
|
|
6066
|
+
const resumed = await store.resume(sessionId);
|
|
6067
|
+
const toIdx = result.toPromptIndex;
|
|
6068
|
+
await resumed.writer.truncateToCheckpoint(toIdx);
|
|
6069
|
+
await resumed.writer.close();
|
|
6070
|
+
deps.renderer.write(` ${color.green("\u2713")} Session truncated at checkpoint ${toIdx}
|
|
6071
|
+
`);
|
|
6072
|
+
}
|
|
6073
|
+
return 0;
|
|
6074
|
+
}
|
|
6075
|
+
deps.renderer.write(`
|
|
6076
|
+
Reverted ${result.revertedFiles.length} file(s):
|
|
6077
|
+
`);
|
|
6078
|
+
for (const f of result.revertedFiles) {
|
|
6079
|
+
deps.renderer.write(` ${color.green("\u2713")} ${f}
|
|
6080
|
+
`);
|
|
6081
|
+
}
|
|
6082
|
+
if (flags.resume) {
|
|
6083
|
+
const store = new DefaultSessionStore({ dir: sessionsDir });
|
|
6084
|
+
const resumed = await store.resume(sessionId);
|
|
6085
|
+
const toIdx = result.toPromptIndex;
|
|
6086
|
+
const removed = await resumed.writer.truncateToCheckpoint(toIdx);
|
|
6087
|
+
await resumed.writer.close();
|
|
6088
|
+
deps.renderer.write(`
|
|
6089
|
+
${color.green("\u2713")} Session truncated \u2014 ${removed} event(s) removed
|
|
6090
|
+
`);
|
|
6091
|
+
}
|
|
6092
|
+
if (result.errors.length > 0) {
|
|
6093
|
+
deps.renderer.write(`
|
|
6094
|
+
${result.errors.length} error(s):
|
|
6095
|
+
`);
|
|
6096
|
+
for (const e of result.errors) {
|
|
6097
|
+
deps.renderer.write(` ${color.red("\u2717")} ${e}
|
|
6098
|
+
`);
|
|
6099
|
+
}
|
|
6100
|
+
return 1;
|
|
6101
|
+
}
|
|
6102
|
+
return 0;
|
|
6103
|
+
} catch (err) {
|
|
6104
|
+
deps.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
6105
|
+
return 1;
|
|
6106
|
+
}
|
|
6107
|
+
};
|
|
5239
6108
|
var toolsCmd = async (_args, deps) => {
|
|
5240
6109
|
const reg = deps.toolRegistry;
|
|
5241
6110
|
if (!reg) return 0;
|
|
@@ -5258,7 +6127,7 @@ var skillsCmd = async (_args, deps) => {
|
|
|
5258
6127
|
};
|
|
5259
6128
|
var versionCmd = async (_args, deps) => {
|
|
5260
6129
|
deps.renderer.write(
|
|
5261
|
-
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${
|
|
6130
|
+
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os6.platform()})
|
|
5262
6131
|
`
|
|
5263
6132
|
);
|
|
5264
6133
|
return 0;
|
|
@@ -5296,8 +6165,10 @@ var helpCmd = async (_args, deps) => {
|
|
|
5296
6165
|
var subcommands = {
|
|
5297
6166
|
init: initCmd,
|
|
5298
6167
|
auth: authCmd,
|
|
6168
|
+
update: updateCmd,
|
|
5299
6169
|
sessions: sessionsCmd,
|
|
5300
6170
|
config: configCmd,
|
|
6171
|
+
rewind: rewindCmd,
|
|
5301
6172
|
tools: toolsCmd,
|
|
5302
6173
|
skills: skillsCmd,
|
|
5303
6174
|
providers: providersCmd,
|
|
@@ -5334,29 +6205,32 @@ function fmtDuration(ms) {
|
|
|
5334
6205
|
const remMin = m - h * 60;
|
|
5335
6206
|
return `${h}h${remMin}m`;
|
|
5336
6207
|
}
|
|
5337
|
-
function fmtTaskResultLine(r,
|
|
6208
|
+
function fmtTaskResultLine(r, color32) {
|
|
5338
6209
|
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
5339
6210
|
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
5340
6211
|
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
5341
6212
|
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
5342
|
-
const errKindChip = errKind ?
|
|
5343
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
6213
|
+
const errKindChip = errKind ? color32.dim(` [${errKind}]`) : "";
|
|
6214
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color32.dim(errTail)}` : "";
|
|
5344
6215
|
switch (r.status) {
|
|
5345
6216
|
case "success":
|
|
5346
|
-
return { mark:
|
|
6217
|
+
return { mark: color32.green("\u2713"), stats, tail: "" };
|
|
5347
6218
|
case "timeout":
|
|
5348
|
-
return { mark:
|
|
6219
|
+
return { mark: color32.yellow("\u23F1"), stats: `${color32.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
5349
6220
|
case "stopped":
|
|
5350
|
-
return { mark:
|
|
6221
|
+
return { mark: color32.dim("\u2298"), stats: `${color32.dim("stopped")} ${stats}`, tail: errSnip };
|
|
5351
6222
|
case "failed":
|
|
5352
|
-
return { mark:
|
|
6223
|
+
return { mark: color32.red("\u2717"), stats: `${color32.red("failed")} ${stats}`, tail: errSnip };
|
|
5353
6224
|
}
|
|
5354
6225
|
}
|
|
6226
|
+
|
|
6227
|
+
// src/boot.ts
|
|
6228
|
+
init_update_check();
|
|
5355
6229
|
function resolveBundledSkillsDir() {
|
|
5356
6230
|
try {
|
|
5357
6231
|
const req2 = createRequire(import.meta.url);
|
|
5358
6232
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
5359
|
-
return
|
|
6233
|
+
return path20.join(path20.dirname(corePkg), "skills");
|
|
5360
6234
|
} catch {
|
|
5361
6235
|
return void 0;
|
|
5362
6236
|
}
|
|
@@ -5385,6 +6259,13 @@ async function boot(argv) {
|
|
|
5385
6259
|
cacheFile: wpaths.modelsCache,
|
|
5386
6260
|
ttlSeconds: 24 * 3600
|
|
5387
6261
|
});
|
|
6262
|
+
let updateInfo;
|
|
6263
|
+
if (!flags["no-check"] && !process.env["WRONGSTACK_NO_CHECK"]) {
|
|
6264
|
+
checkForUpdate().then((info) => {
|
|
6265
|
+
updateInfo = info;
|
|
6266
|
+
}).catch(() => {
|
|
6267
|
+
});
|
|
6268
|
+
}
|
|
5388
6269
|
const first = positional[0];
|
|
5389
6270
|
if (first && subcommands[first]) {
|
|
5390
6271
|
const container = createDefaultContainer({
|
|
@@ -5493,7 +6374,8 @@ async function boot(argv) {
|
|
|
5493
6374
|
modelsRegistry,
|
|
5494
6375
|
renderer,
|
|
5495
6376
|
reader,
|
|
5496
|
-
logger
|
|
6377
|
+
logger,
|
|
6378
|
+
updateInfo
|
|
5497
6379
|
};
|
|
5498
6380
|
}
|
|
5499
6381
|
|
|
@@ -6074,7 +6956,7 @@ async function execute(deps) {
|
|
|
6074
6956
|
supportsVision,
|
|
6075
6957
|
attachments,
|
|
6076
6958
|
effectiveMaxContext,
|
|
6077
|
-
projectName:
|
|
6959
|
+
projectName: path20.basename(projectRoot) || void 0,
|
|
6078
6960
|
getAutonomy,
|
|
6079
6961
|
skillLoader
|
|
6080
6962
|
});
|
|
@@ -6092,7 +6974,7 @@ async function execute(deps) {
|
|
|
6092
6974
|
supportsVision,
|
|
6093
6975
|
attachments,
|
|
6094
6976
|
effectiveMaxContext,
|
|
6095
|
-
projectName:
|
|
6977
|
+
projectName: path20.basename(projectRoot) || void 0,
|
|
6096
6978
|
getAutonomy,
|
|
6097
6979
|
skillLoader
|
|
6098
6980
|
});
|
|
@@ -6364,7 +7246,7 @@ var MultiAgentHost = class {
|
|
|
6364
7246
|
model: opts?.model,
|
|
6365
7247
|
tools: opts?.tools
|
|
6366
7248
|
};
|
|
6367
|
-
const transcriptPath = this.sessionFactory ?
|
|
7249
|
+
const transcriptPath = this.sessionFactory ? path20.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
|
|
6368
7250
|
if (this.director) {
|
|
6369
7251
|
const subagentId = await this.director.spawn(subagentConfig);
|
|
6370
7252
|
const taskId2 = randomUUID();
|
|
@@ -6524,16 +7406,16 @@ var MultiAgentHost = class {
|
|
|
6524
7406
|
}
|
|
6525
7407
|
this.opts.directorMode = true;
|
|
6526
7408
|
if (this.opts.fleetRoot && !this.opts.manifestPath) {
|
|
6527
|
-
this.opts.manifestPath =
|
|
7409
|
+
this.opts.manifestPath = path20.join(this.opts.fleetRoot, "fleet.json");
|
|
6528
7410
|
}
|
|
6529
7411
|
if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
|
|
6530
|
-
this.opts.sharedScratchpadPath =
|
|
7412
|
+
this.opts.sharedScratchpadPath = path20.join(this.opts.fleetRoot, "shared");
|
|
6531
7413
|
}
|
|
6532
7414
|
if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
|
|
6533
|
-
this.opts.sessionsRoot =
|
|
7415
|
+
this.opts.sessionsRoot = path20.join(this.opts.fleetRoot, "subagents");
|
|
6534
7416
|
}
|
|
6535
7417
|
if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
|
|
6536
|
-
this.opts.stateCheckpointPath =
|
|
7418
|
+
this.opts.stateCheckpointPath = path20.join(this.opts.fleetRoot, "director-state.json");
|
|
6537
7419
|
}
|
|
6538
7420
|
await this.ensureDirector();
|
|
6539
7421
|
return this.director ?? null;
|
|
@@ -6654,11 +7536,11 @@ var SessionStats = class {
|
|
|
6654
7536
|
if (e.name === "bash") this.bashCommands++;
|
|
6655
7537
|
else if (e.name === "fetch") this.fetches++;
|
|
6656
7538
|
if (!e.ok) return;
|
|
6657
|
-
const
|
|
6658
|
-
if (e.name === "read" &&
|
|
6659
|
-
else if (e.name === "edit" &&
|
|
6660
|
-
else if (e.name === "write" &&
|
|
6661
|
-
this.writtenPaths.add(
|
|
7539
|
+
const path21 = typeof input?.path === "string" ? input.path : void 0;
|
|
7540
|
+
if (e.name === "read" && path21) this.readPaths.add(path21);
|
|
7541
|
+
else if (e.name === "edit" && path21) this.editedPaths.add(path21);
|
|
7542
|
+
else if (e.name === "write" && path21) {
|
|
7543
|
+
this.writtenPaths.add(path21);
|
|
6662
7544
|
const content = typeof input?.content === "string" ? input.content : "";
|
|
6663
7545
|
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
6664
7546
|
}
|
|
@@ -6989,12 +7871,12 @@ async function setupSession(params) {
|
|
|
6989
7871
|
}
|
|
6990
7872
|
const sessionRef = { current: session };
|
|
6991
7873
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
6992
|
-
const attachments = new DefaultAttachmentStore({ spoolDir:
|
|
6993
|
-
const queueStore = new QueueStore({ dir:
|
|
7874
|
+
const attachments = new DefaultAttachmentStore({ spoolDir: path20.join(wpaths.projectSessions, session.id, "attachments") });
|
|
7875
|
+
const queueStore = new QueueStore({ dir: path20.join(wpaths.projectSessions, session.id) });
|
|
6994
7876
|
const ctxSignal = new AbortController().signal;
|
|
6995
7877
|
const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
|
|
6996
7878
|
if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
|
|
6997
|
-
const todosCheckpointPath =
|
|
7879
|
+
const todosCheckpointPath = path20.join(wpaths.projectSessions, `${session.id}.todos.json`);
|
|
6998
7880
|
if (resumeId) {
|
|
6999
7881
|
try {
|
|
7000
7882
|
const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
|
|
@@ -7006,12 +7888,12 @@ async function setupSession(params) {
|
|
|
7006
7888
|
}
|
|
7007
7889
|
}
|
|
7008
7890
|
const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
|
|
7009
|
-
const planPath =
|
|
7891
|
+
const planPath = path20.join(wpaths.projectSessions, `${session.id}.plan.json`);
|
|
7010
7892
|
context.state.setMeta("plan.path", planPath);
|
|
7011
7893
|
if (resumeId) {
|
|
7012
7894
|
try {
|
|
7013
|
-
const fleetRoot =
|
|
7014
|
-
const dirState = await loadDirectorState(
|
|
7895
|
+
const fleetRoot = path20.join(wpaths.projectSessions, session.id);
|
|
7896
|
+
const dirState = await loadDirectorState(path20.join(fleetRoot, "director-state.json"));
|
|
7015
7897
|
if (dirState) {
|
|
7016
7898
|
const tCounts = {};
|
|
7017
7899
|
for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
|
|
@@ -7038,7 +7920,7 @@ function resolveBundledSkillsDir2() {
|
|
|
7038
7920
|
try {
|
|
7039
7921
|
const req2 = createRequire(import.meta.url);
|
|
7040
7922
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
7041
|
-
return
|
|
7923
|
+
return path20.join(path20.dirname(corePkg), "skills");
|
|
7042
7924
|
} catch {
|
|
7043
7925
|
return void 0;
|
|
7044
7926
|
}
|
|
@@ -7069,8 +7951,28 @@ async function main(argv) {
|
|
|
7069
7951
|
modelsRegistry,
|
|
7070
7952
|
renderer,
|
|
7071
7953
|
reader,
|
|
7072
|
-
logger
|
|
7954
|
+
logger,
|
|
7955
|
+
updateInfo
|
|
7073
7956
|
} = ctx;
|
|
7957
|
+
if (!updateInfo?.outdated) {
|
|
7958
|
+
const ac = new AbortController();
|
|
7959
|
+
const timer = setTimeout(() => ac.abort(), 2e3);
|
|
7960
|
+
try {
|
|
7961
|
+
const { checkForUpdate: checkForUpdate2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
|
|
7962
|
+
updateInfo = await checkForUpdate2(ac.signal);
|
|
7963
|
+
} catch {
|
|
7964
|
+
} finally {
|
|
7965
|
+
clearTimeout(timer);
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
if (updateInfo?.outdated) {
|
|
7969
|
+
process.stderr.write(
|
|
7970
|
+
`
|
|
7971
|
+
\x1B[33m\u2191 Update available: v${updateInfo.current} \u2192 v${updateInfo.latest}\x1B[0m Run \`wrongstack update\` to upgrade.
|
|
7972
|
+
|
|
7973
|
+
`
|
|
7974
|
+
);
|
|
7975
|
+
}
|
|
7074
7976
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
7075
7977
|
const container = createDefaultContainer({
|
|
7076
7978
|
config,
|
|
@@ -7125,7 +8027,7 @@ async function main(argv) {
|
|
|
7125
8027
|
modeId,
|
|
7126
8028
|
modePrompt,
|
|
7127
8029
|
modelCapabilities,
|
|
7128
|
-
planPath: () => sessionRef.current ?
|
|
8030
|
+
planPath: () => sessionRef.current ? path20.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
|
|
7129
8031
|
})
|
|
7130
8032
|
);
|
|
7131
8033
|
const toolRegistry = new ToolRegistry();
|
|
@@ -7153,7 +8055,7 @@ async function main(argv) {
|
|
|
7153
8055
|
name: "session-store",
|
|
7154
8056
|
check: async () => {
|
|
7155
8057
|
try {
|
|
7156
|
-
await
|
|
8058
|
+
await fs3.access(wpaths.projectSessions);
|
|
7157
8059
|
return { status: "healthy" };
|
|
7158
8060
|
} catch (e) {
|
|
7159
8061
|
return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
|
|
@@ -7170,7 +8072,7 @@ async function main(argv) {
|
|
|
7170
8072
|
const dumpMetrics = () => {
|
|
7171
8073
|
if (!metricsSink) return;
|
|
7172
8074
|
try {
|
|
7173
|
-
const out =
|
|
8075
|
+
const out = path20.join(wpaths.projectSessions, "metrics.json");
|
|
7174
8076
|
const snap = metricsSink.snapshot();
|
|
7175
8077
|
writeFileSync(out, JSON.stringify(snap, null, 2));
|
|
7176
8078
|
} catch {
|
|
@@ -7389,12 +8291,12 @@ async function main(argv) {
|
|
|
7389
8291
|
const directorMode = flags["director"] === true;
|
|
7390
8292
|
let director = null;
|
|
7391
8293
|
let autonomyMode = "off";
|
|
7392
|
-
const fleetRoot = directorMode ?
|
|
7393
|
-
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] :
|
|
7394
|
-
const sharedScratchpadPath = directorMode ?
|
|
7395
|
-
const subagentSessionsRoot = directorMode ?
|
|
7396
|
-
const stateCheckpointPath = directorMode ?
|
|
7397
|
-
const fleetRootForPromotion =
|
|
8294
|
+
const fleetRoot = directorMode ? path20.join(wpaths.projectSessions, session.id) : void 0;
|
|
8295
|
+
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path20.join(fleetRoot, "fleet.json") : void 0;
|
|
8296
|
+
const sharedScratchpadPath = directorMode ? path20.join(fleetRoot, "shared") : void 0;
|
|
8297
|
+
const subagentSessionsRoot = directorMode ? path20.join(fleetRoot, "subagents") : void 0;
|
|
8298
|
+
const stateCheckpointPath = directorMode ? path20.join(fleetRoot, "director-state.json") : void 0;
|
|
8299
|
+
const fleetRootForPromotion = path20.join(wpaths.projectSessions, session.id);
|
|
7398
8300
|
const multiAgentHost = new MultiAgentHost(
|
|
7399
8301
|
{
|
|
7400
8302
|
container,
|
|
@@ -7573,27 +8475,27 @@ async function main(argv) {
|
|
|
7573
8475
|
return `Unknown fleet action: ${action}`;
|
|
7574
8476
|
},
|
|
7575
8477
|
onFleetLog: async (subagentId, mode) => {
|
|
7576
|
-
const subagentsRoot =
|
|
8478
|
+
const subagentsRoot = path20.join(fleetRootForPromotion, "subagents");
|
|
7577
8479
|
let runDirs;
|
|
7578
8480
|
try {
|
|
7579
|
-
runDirs = await
|
|
8481
|
+
runDirs = await fs3.readdir(subagentsRoot);
|
|
7580
8482
|
} catch {
|
|
7581
8483
|
return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
|
|
7582
8484
|
}
|
|
7583
8485
|
const found = [];
|
|
7584
8486
|
for (const runId of runDirs) {
|
|
7585
|
-
const runDir =
|
|
8487
|
+
const runDir = path20.join(subagentsRoot, runId);
|
|
7586
8488
|
let files;
|
|
7587
8489
|
try {
|
|
7588
|
-
files = await
|
|
8490
|
+
files = await fs3.readdir(runDir);
|
|
7589
8491
|
} catch {
|
|
7590
8492
|
continue;
|
|
7591
8493
|
}
|
|
7592
8494
|
for (const f of files) {
|
|
7593
8495
|
if (!f.endsWith(".jsonl")) continue;
|
|
7594
|
-
const full =
|
|
8496
|
+
const full = path20.join(runDir, f);
|
|
7595
8497
|
try {
|
|
7596
|
-
const stat2 = await
|
|
8498
|
+
const stat2 = await fs3.stat(full);
|
|
7597
8499
|
found.push({
|
|
7598
8500
|
runId,
|
|
7599
8501
|
subagentId: f.replace(/\.jsonl$/, ""),
|
|
@@ -7632,7 +8534,7 @@ async function main(argv) {
|
|
|
7632
8534
|
].join("\n");
|
|
7633
8535
|
}
|
|
7634
8536
|
const t = matches[0];
|
|
7635
|
-
const raw = await
|
|
8537
|
+
const raw = await fs3.readFile(t.file, "utf8");
|
|
7636
8538
|
if (mode === "raw") return raw;
|
|
7637
8539
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
7638
8540
|
const counts = {};
|
|
@@ -7688,7 +8590,7 @@ async function main(argv) {
|
|
|
7688
8590
|
}
|
|
7689
8591
|
const dir = await multiAgentHost.ensureDirector();
|
|
7690
8592
|
if (!dir) return "Director is not available.";
|
|
7691
|
-
const dirStatePath =
|
|
8593
|
+
const dirStatePath = path20.join(fleetRootForPromotion, "director-state.json");
|
|
7692
8594
|
const prior = await loadDirectorState(dirStatePath);
|
|
7693
8595
|
if (!prior) {
|
|
7694
8596
|
return "No prior director-state.json found \u2014 nothing to retry.";
|
|
@@ -7759,9 +8661,9 @@ async function main(argv) {
|
|
|
7759
8661
|
for (const tool of director2.tools(FLEET_ROSTER)) {
|
|
7760
8662
|
toolRegistry.register(tool);
|
|
7761
8663
|
}
|
|
7762
|
-
const mp =
|
|
7763
|
-
const sp =
|
|
7764
|
-
const ss =
|
|
8664
|
+
const mp = path20.join(fleetRootForPromotion, "fleet.json");
|
|
8665
|
+
const sp = path20.join(fleetRootForPromotion, "shared");
|
|
8666
|
+
const ss = path20.join(fleetRootForPromotion, "subagents");
|
|
7765
8667
|
const lines = [
|
|
7766
8668
|
`${color.green("\u2713")} Promoted to director mode.`,
|
|
7767
8669
|
` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
|
|
@@ -7807,6 +8709,24 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
7807
8709
|
onExit: () => {
|
|
7808
8710
|
void mcpRegistry.stopAll();
|
|
7809
8711
|
},
|
|
8712
|
+
onBeforeExit: async () => {
|
|
8713
|
+
const { spawn: spawn3 } = await import('child_process');
|
|
8714
|
+
const cwd2 = projectRoot;
|
|
8715
|
+
const statusResult = await new Promise((resolve4) => {
|
|
8716
|
+
const child = spawn3("git", ["status", "--porcelain"], { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
|
|
8717
|
+
let stdout = "";
|
|
8718
|
+
child.stdout?.on("data", (d) => stdout += d);
|
|
8719
|
+
child.on("close", (code) => resolve4({ stdout, code: code ?? 0 }));
|
|
8720
|
+
});
|
|
8721
|
+
if (statusResult.stdout.trim().length > 0) {
|
|
8722
|
+
const lines = statusResult.stdout.split("\n").filter(Boolean);
|
|
8723
|
+
return {
|
|
8724
|
+
abort: true,
|
|
8725
|
+
// signals there are uncommitted changes (used only for the message)
|
|
8726
|
+
message: `\u26A0 ${color.yellow(`${lines.length} uncommitted change${lines.length > 1 ? "s" : ""}`)} \u2014 session ended without commit`
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
},
|
|
7810
8730
|
onClear: () => {
|
|
7811
8731
|
if (flags.tui && !flags["no-tui"]) return;
|
|
7812
8732
|
try {
|
|
@@ -7834,7 +8754,13 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
7834
8754
|
...errSection
|
|
7835
8755
|
].join("\n");
|
|
7836
8756
|
},
|
|
7837
|
-
onStats: () => stats.format()
|
|
8757
|
+
onStats: () => stats.format(),
|
|
8758
|
+
generateCommitMessage: async (diff) => {
|
|
8759
|
+
return generateCommitMessageWithLLM(diff, {
|
|
8760
|
+
provider: context.provider,
|
|
8761
|
+
model: context.model
|
|
8762
|
+
});
|
|
8763
|
+
}
|
|
7838
8764
|
});
|
|
7839
8765
|
for (const cmd of slashCmds) slashRegistry.register(cmd);
|
|
7840
8766
|
const savedProviderCfg = config.providers?.[config.provider];
|