@wrongstack/cli 1.1.4 → 1.1.5
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/{acp-MZ5MQBFN.js → acp-NKUEYJDZ.js} +6 -5
- package/dist/acp-leader/acp-leader-confirm.d.ts +2 -0
- package/dist/acp-leader/acp-leader-host-binding.d.ts +1 -0
- package/dist/acp-leader/acp-leader-host.d.ts +2 -0
- package/dist/acp-leader/acp-leader-picker.d.ts +6 -3
- package/dist/acp-leader/acp-leader-stream.d.ts +1 -0
- package/dist/{auth-IHQS6AFW.js → auth-2ZG5DBHS.js} +4 -4
- package/dist/boot/launch-menu-arrow.d.ts +20 -0
- package/dist/boot/launch-menu-types.d.ts +36 -0
- package/dist/boot/launch-menu.d.ts +1 -57
- package/dist/boot/tui-session-resume-rollback.d.ts +31 -0
- package/dist/{boot-auto-thin-7D4OVEWA.js → boot-auto-thin-N2CIGTXE.js} +3 -3
- package/dist/{chunk-TZPKOS5G.js → chunk-2XQ6SRKG.js} +2 -2
- package/dist/{chunk-JKTJSLUT.js → chunk-5SGMUDP5.js} +242 -18
- package/dist/{chunk-3EXRPTAU.js → chunk-5TQ65PXQ.js} +17 -17
- package/dist/{chunk-2LTHDKZX.js → chunk-7BM4LGKQ.js} +18 -18
- package/dist/{chunk-VAQXOZKP.js → chunk-AJM4EFCM.js} +139 -136
- package/dist/{chunk-C2BBH4S4.js → chunk-E3MJFAFR.js} +2 -2
- package/dist/{chunk-AWRZWPJ3.js → chunk-JFYQGMMM.js} +99 -75
- package/dist/{chunk-BIGSVBPI.js → chunk-KNYIEQAY.js} +64 -61
- package/dist/{chunk-GKGWPFW2.js → chunk-LUEO5HLZ.js} +1 -1
- package/dist/chunk-LYWOV4OX.js +252 -0
- package/dist/{chunk-COGI7EQU.js → chunk-M7FNHCE3.js} +621 -9
- package/dist/{chunk-75OLD563.js → chunk-M7PSENGF.js} +3 -1
- package/dist/{chunk-TAE3EGBM.js → chunk-MEBXWI7U.js} +5 -5
- package/dist/{chunk-I5LJLKA3.js → chunk-MKMM5RIW.js} +12 -5
- package/dist/{chunk-ZF4HRR5F.js → chunk-P7MEEUGT.js} +275 -250
- package/dist/chunk-QLYPIJ7X.js +258 -0
- package/dist/chunk-RN5CFAVQ.js +292 -0
- package/dist/{chunk-WDXNBYGD.js → chunk-TDP7CZGE.js} +6 -1
- package/dist/{chunk-6VPWQ4TG.js → chunk-UROB7MK5.js} +2 -1
- package/dist/chunk-VEESMPTX.js +254 -0
- package/dist/{chunk-IJLIAPCW.js → chunk-VPSAKGLK.js} +47 -47
- package/dist/{cli-context-E44HQ4ZO.js → cli-context-CQYGIWOU.js} +247 -236
- package/dist/{cli-main-OT63I2FX.js → cli-main-VRUJNUBX.js} +1743 -1491
- package/dist/cli-main-boot.d.ts +1 -1
- package/dist/config-doctor-known-keys.d.ts +33 -0
- package/dist/config-doctor.d.ts +2 -47
- package/dist/config-history-support.d.ts +36 -0
- package/dist/config-history.d.ts +1 -1
- package/dist/{config-transfer-Y7XJMJRN.js → config-transfer-B4GOZTFC.js} +2 -2
- package/dist/{execution-OK34CJKJ.js → execution-IV72RUSM.js} +5883 -5792
- package/dist/execution-tui.d.ts +30 -0
- package/dist/fleet/acp-wrongstack-bridge.d.ts +11 -3
- package/dist/fleet/host-acp.d.ts +3 -0
- package/dist/fleet/wrongforge-client.d.ts +116 -0
- package/dist/fleet/wrongforge-gateway.d.ts +20 -0
- package/dist/fleet/wrongforge-info.d.ts +32 -0
- package/dist/fleet/wrongforge-quota.d.ts +10 -0
- package/dist/fleet/wrongforge-runner.d.ts +37 -0
- package/dist/fleet/wrongforge-runtime.d.ts +10 -0
- package/dist/{hq-JD5AZVMG.js → hq-54PQYR5O.js} +311 -307
- package/dist/hq-server/sweeps.d.ts +28 -0
- package/dist/{hq-server-BI23EPM7.js → hq-server-YK7FBLL6.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/mcp-22CFBX4H.js +236 -0
- package/dist/mcp-serve.d.ts +0 -63
- package/dist/{modeldiag-HDIKG37X.js → modeldiag-5PBB777J.js} +4 -4
- package/dist/{per-subcommand-help-4GYSCMLU.js → per-subcommand-help-MQGERD4E.js} +2 -2
- package/dist/plugin-llm-command.d.ts +9 -0
- package/dist/{plugin-usage-TYF5HY4K.js → plugin-usage-UWKYMNQU.js} +3 -3
- package/dist/{plugins-XGAXDLPT.js → plugins-VP7JQH47.js} +4 -4
- package/dist/{sdd-runtime-6MFUXWJK.js → sdd-runtime-HIJ4Z5TF.js} +2 -2
- package/dist/services/sdd/state.d.ts +2 -0
- package/dist/session-event-wiring-types.d.ts +62 -0
- package/dist/session-event-wiring.d.ts +2 -52
- package/dist/{sessions-config-SJVQXCIE.js → sessions-config-7MK742OW.js} +2 -2
- package/dist/short-circuit-flags-AGHURHMQ.js +11 -0
- package/dist/slash-commands/command-context-fleet.d.ts +187 -0
- package/dist/slash-commands/command-context-sdd-goal.d.ts +102 -0
- package/dist/slash-commands/command-context.d.ts +3 -282
- package/dist/slash-commands/project-switch-interactive.d.ts +11 -0
- package/dist/slash-commands/setmodel-helpers.d.ts +36 -0
- package/dist/subcommands/handlers/hq-token.d.ts +3 -0
- package/dist/subcommands/handlers/hq.d.ts +0 -21
- package/dist/subcommands/handlers/per-subcommand-help-operations.d.ts +8 -0
- package/dist/subcommands/handlers/tools-cli-args.d.ts +57 -0
- package/dist/subcommands/handlers/tools-cli.d.ts +5 -0
- package/dist/subcommands/handlers/tools-serve.d.ts +4 -0
- package/dist/{subcommands-YADUV7IJ.js → subcommands-OCJNO57E.js} +2 -2
- package/dist/tool-gateway-http.d.ts +30 -0
- package/dist/tool-gateway.d.ts +185 -0
- package/dist/tools-cli-44MLIUQO.js +509 -0
- package/dist/tools-serve-TRCET2M7.js +102 -0
- package/dist/{tools-skills-55QVTMSK.js → tools-skills-WDTMCE46.js} +7 -2
- package/dist/tuneup-runtime-checks.d.ts +7 -0
- package/dist/tuneup-types.d.ts +118 -0
- package/dist/tuneup.d.ts +3 -122
- package/dist/{typesafe-YBEADNAH.js → typesafe-QLSMFU7J.js} +1 -1
- package/dist/{version-help-RBZC5GZZ.js → version-help-X6CC4CFY.js} +2 -2
- package/dist/{webui-server-CB64CFDG.js → webui-server-V2R5WQUK.js} +44 -6
- package/dist/wiring/provider-runtime-setup.d.ts +3 -1
- package/dist/wiring/session-runtime.d.ts +1 -1
- package/dist/wrongforge-gateway-UOFBK56J.js +92 -0
- package/dist/wrongforge-leader/wrongforge-leader-gateway.d.ts +9 -0
- package/dist/wrongforge-leader/wrongforge-leader-picker.d.ts +12 -0
- package/dist/wrongforge-leader/wrongforge-leader-provider.d.ts +49 -0
- package/package.json +1 -1
- package/dist/mcp-W7A5HUXV.js +0 -560
- package/dist/short-circuit-flags-QEXUS3BY.js +0 -11
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
|
|
2
2
|
// src/config-history.ts
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import * as fs2 from "node:fs/promises";
|
|
5
|
+
import * as path2 from "node:path";
|
|
6
|
+
import { isSecretField as isSecretField2 } from "@wrongstack/core/security";
|
|
7
|
+
import { ERROR_CODES as ERROR_CODES2, FsError as FsError2 } from "@wrongstack/core/types";
|
|
8
|
+
import { atomicWrite, toErrorMessage, withFileLock, writeErr } from "@wrongstack/core/utils";
|
|
9
|
+
|
|
10
|
+
// src/config-history-support.ts
|
|
4
11
|
import * as fs from "node:fs/promises";
|
|
5
12
|
import os from "node:os";
|
|
6
13
|
import * as path from "node:path";
|
|
7
14
|
import { isSecretField } from "@wrongstack/core/security";
|
|
8
15
|
import { ERROR_CODES, FsError } from "@wrongstack/core/types";
|
|
9
|
-
import {
|
|
10
|
-
atomicWrite,
|
|
11
|
-
resolveWstackPaths,
|
|
12
|
-
toErrorMessage,
|
|
13
|
-
withFileLock,
|
|
14
|
-
writeErr
|
|
15
|
-
} from "@wrongstack/core/utils";
|
|
16
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
16
17
|
var defaultUidFn = () => os.userInfo().uid;
|
|
17
18
|
async function getFileUid(filePath) {
|
|
18
19
|
try {
|
|
@@ -32,8 +33,6 @@ async function checkConfigOwnership(homeFn, uidFn = defaultUidFn, targetConfigPa
|
|
|
32
33
|
return fileUid === callerUid;
|
|
33
34
|
}
|
|
34
35
|
var PROTECTED_BASENAMES = /* @__PURE__ */ new Set(["config.json", ".key", "index.json"]);
|
|
35
|
-
var MAX_CONFIG_BACKUPS = 10;
|
|
36
|
-
var MAX_CONFIG_HISTORY_ENTRIES = 50;
|
|
37
36
|
function assertSafeToDelete(filename, parentDir) {
|
|
38
37
|
if (PROTECTED_BASENAMES.has(filename)) {
|
|
39
38
|
throw new FsError({
|
|
@@ -72,19 +71,6 @@ function assertSafeToDelete(filename, parentDir) {
|
|
|
72
71
|
});
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
|
-
async function safeDelete(filePath) {
|
|
76
|
-
const dir = path.dirname(filePath);
|
|
77
|
-
const filename = path.basename(filePath);
|
|
78
|
-
try {
|
|
79
|
-
assertSafeToDelete(filename, dir);
|
|
80
|
-
await fs.unlink(filePath);
|
|
81
|
-
} catch (err) {
|
|
82
|
-
if (err instanceof Error && err.message.startsWith("Refusing")) {
|
|
83
|
-
writeErr(`[config-history] SAFETY: ${err.message}
|
|
84
|
-
`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
74
|
var REDACTED = "[REDACTED]";
|
|
89
75
|
function maskValue(v) {
|
|
90
76
|
if (Array.isArray(v)) return v.map(maskValue);
|
|
@@ -130,6 +116,31 @@ function reviveSecrets(masked, current) {
|
|
|
130
116
|
}
|
|
131
117
|
return out;
|
|
132
118
|
}
|
|
119
|
+
var defaultHomeDir = () => os.homedir();
|
|
120
|
+
function activeProfileConfigPath(homeFn) {
|
|
121
|
+
const paths = homeFn === defaultHomeDir ? resolveWstackPaths({ projectRoot: process.cwd() }) : resolveWstackPaths({ projectRoot: process.cwd(), userHome: homeFn() });
|
|
122
|
+
return paths.profileConfig(paths.profileName);
|
|
123
|
+
}
|
|
124
|
+
function configPath(homeFn = defaultHomeDir, targetConfigPath) {
|
|
125
|
+
return targetConfigPath ?? activeProfileConfigPath(homeFn);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/config-history.ts
|
|
129
|
+
var MAX_CONFIG_BACKUPS = 10;
|
|
130
|
+
var MAX_CONFIG_HISTORY_ENTRIES = 50;
|
|
131
|
+
async function safeDelete(filePath) {
|
|
132
|
+
const dir = path2.dirname(filePath);
|
|
133
|
+
const filename = path2.basename(filePath);
|
|
134
|
+
try {
|
|
135
|
+
assertSafeToDelete(filename, dir);
|
|
136
|
+
await fs2.unlink(filePath);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err instanceof Error && err.message.startsWith("Refusing")) {
|
|
139
|
+
writeErr(`[config-history] SAFETY: ${err.message}
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
133
144
|
function diffSummary(oldCfg, newCfg) {
|
|
134
145
|
const changes = [];
|
|
135
146
|
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldCfg), ...Object.keys(newCfg)]);
|
|
@@ -137,7 +148,7 @@ function diffSummary(oldCfg, newCfg) {
|
|
|
137
148
|
const o = JSON.stringify(oldCfg[k]);
|
|
138
149
|
const n = JSON.stringify(newCfg[k]);
|
|
139
150
|
if (o !== n) {
|
|
140
|
-
if (
|
|
151
|
+
if (isSecretField2(k)) {
|
|
141
152
|
changes.push(`${k}: [CHANGED]`);
|
|
142
153
|
} else if (typeof newCfg[k] !== "object") {
|
|
143
154
|
changes.push(`${k}: ${oldCfg[k] ?? "(unset)"} \u2192 ${newCfg[k]}`);
|
|
@@ -148,24 +159,16 @@ function diffSummary(oldCfg, newCfg) {
|
|
|
148
159
|
}
|
|
149
160
|
return changes.length > 0 ? changes.slice(0, 5).join(", ") : "no changes";
|
|
150
161
|
}
|
|
151
|
-
var defaultHomeDir = () => os.homedir();
|
|
152
|
-
function activeProfileConfigPath(homeFn) {
|
|
153
|
-
const paths = homeFn === defaultHomeDir ? resolveWstackPaths({ projectRoot: process.cwd() }) : resolveWstackPaths({ projectRoot: process.cwd(), userHome: homeFn() });
|
|
154
|
-
return paths.profileConfig(paths.profileName);
|
|
155
|
-
}
|
|
156
162
|
function historyDir(homeFn = defaultHomeDir, targetConfigPath) {
|
|
157
|
-
return
|
|
163
|
+
return path2.join(path2.dirname(configPath(homeFn, targetConfigPath)), "config.history", "entries");
|
|
158
164
|
}
|
|
159
165
|
function historyIndexPath(homeFn = defaultHomeDir, targetConfigPath) {
|
|
160
|
-
return
|
|
161
|
-
|
|
166
|
+
return path2.join(
|
|
167
|
+
path2.dirname(configPath(homeFn, targetConfigPath)),
|
|
162
168
|
"config.history",
|
|
163
169
|
"index.json"
|
|
164
170
|
);
|
|
165
171
|
}
|
|
166
|
-
function configPath(homeFn = defaultHomeDir, targetConfigPath) {
|
|
167
|
-
return targetConfigPath ?? activeProfileConfigPath(homeFn);
|
|
168
|
-
}
|
|
169
172
|
function backupLastPath(homeFn = defaultHomeDir, targetConfigPath) {
|
|
170
173
|
return `${configPath(homeFn, targetConfigPath)}.last`;
|
|
171
174
|
}
|
|
@@ -174,11 +177,11 @@ function entryId(ts) {
|
|
|
174
177
|
}
|
|
175
178
|
async function ensureHistoryDir(homeFn = defaultHomeDir, targetConfigPath) {
|
|
176
179
|
try {
|
|
177
|
-
await
|
|
180
|
+
await fs2.mkdir(historyDir(homeFn, targetConfigPath), { recursive: true });
|
|
178
181
|
} catch (err) {
|
|
179
|
-
throw new
|
|
182
|
+
throw new FsError2({
|
|
180
183
|
message: toErrorMessage(err),
|
|
181
|
-
code:
|
|
184
|
+
code: ERROR_CODES2.FS_MKDIR_FAILED,
|
|
182
185
|
path: historyDir(homeFn, targetConfigPath),
|
|
183
186
|
cause: err
|
|
184
187
|
});
|
|
@@ -187,7 +190,7 @@ async function ensureHistoryDir(homeFn = defaultHomeDir, targetConfigPath) {
|
|
|
187
190
|
async function readIndex(homeFn = defaultHomeDir, targetConfigPath) {
|
|
188
191
|
let raw;
|
|
189
192
|
try {
|
|
190
|
-
raw = await
|
|
193
|
+
raw = await fs2.readFile(historyIndexPath(homeFn, targetConfigPath), "utf8");
|
|
191
194
|
} catch (err) {
|
|
192
195
|
if (err.code !== "ENOENT") throw err;
|
|
193
196
|
return rebuildIndexFromEntries(homeFn, targetConfigPath);
|
|
@@ -208,7 +211,7 @@ async function rebuildIndexFromEntries(homeFn = defaultHomeDir, targetConfigPath
|
|
|
208
211
|
const dir = historyDir(homeFn, targetConfigPath);
|
|
209
212
|
let files;
|
|
210
213
|
try {
|
|
211
|
-
files = (await
|
|
214
|
+
files = (await fs2.readdir(dir)).filter((file) => file.endsWith(".json"));
|
|
212
215
|
} catch (err) {
|
|
213
216
|
if (err.code === "ENOENT") return { version: 1, entries: [] };
|
|
214
217
|
throw err;
|
|
@@ -218,7 +221,7 @@ async function rebuildIndexFromEntries(homeFn = defaultHomeDir, targetConfigPath
|
|
|
218
221
|
const id = file.slice(0, -".json".length);
|
|
219
222
|
let raw;
|
|
220
223
|
try {
|
|
221
|
-
raw = await
|
|
224
|
+
raw = await fs2.readFile(path2.join(dir, file), "utf8");
|
|
222
225
|
} catch (err) {
|
|
223
226
|
if (err.code === "ENOENT") continue;
|
|
224
227
|
entries.push({ id, timestamp: "", description: "" });
|
|
@@ -244,9 +247,9 @@ async function writeIndex(idx, homeFn = defaultHomeDir, targetConfigPath) {
|
|
|
244
247
|
try {
|
|
245
248
|
await atomicWrite(historyIndexPath(homeFn, targetConfigPath), JSON.stringify(idx, null, 2));
|
|
246
249
|
} catch (err) {
|
|
247
|
-
throw new
|
|
250
|
+
throw new FsError2({
|
|
248
251
|
message: toErrorMessage(err),
|
|
249
|
-
code:
|
|
252
|
+
code: ERROR_CODES2.FS_ATOMIC_WRITE_FAILED,
|
|
250
253
|
path: historyIndexPath(homeFn, targetConfigPath),
|
|
251
254
|
cause: err
|
|
252
255
|
});
|
|
@@ -258,16 +261,16 @@ async function pruneHistoryEntries(idx, homeFn = defaultHomeDir, targetConfigPat
|
|
|
258
261
|
const dir = historyDir(homeFn, targetConfigPath);
|
|
259
262
|
for (const entry of removed) {
|
|
260
263
|
try {
|
|
261
|
-
await
|
|
264
|
+
await fs2.unlink(path2.join(dir, `${entry.id}.json`));
|
|
262
265
|
} catch {
|
|
263
266
|
}
|
|
264
267
|
}
|
|
265
268
|
try {
|
|
266
|
-
const files = await
|
|
269
|
+
const files = await fs2.readdir(dir);
|
|
267
270
|
await Promise.all(
|
|
268
271
|
files.filter((file) => file.endsWith(".json") && !keep.has(file)).map(async (file) => {
|
|
269
272
|
try {
|
|
270
|
-
await
|
|
273
|
+
await fs2.unlink(path2.join(dir, file));
|
|
271
274
|
} catch {
|
|
272
275
|
}
|
|
273
276
|
})
|
|
@@ -281,7 +284,7 @@ async function backupCurrent(homeFn = defaultHomeDir, targetConfigPath) {
|
|
|
281
284
|
const ts = Date.now();
|
|
282
285
|
let content;
|
|
283
286
|
try {
|
|
284
|
-
content = await
|
|
287
|
+
content = await fs2.readFile(cfg, "utf8");
|
|
285
288
|
} catch {
|
|
286
289
|
}
|
|
287
290
|
if (content !== void 0) {
|
|
@@ -300,11 +303,11 @@ async function backupCurrent(homeFn = defaultHomeDir, targetConfigPath) {
|
|
|
300
303
|
}
|
|
301
304
|
}
|
|
302
305
|
try {
|
|
303
|
-
const dir =
|
|
304
|
-
const files = await
|
|
306
|
+
const dir = path2.dirname(cfg);
|
|
307
|
+
const files = await fs2.readdir(dir);
|
|
305
308
|
const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
|
|
306
309
|
for (const f of baks.slice(MAX_CONFIG_BACKUPS)) {
|
|
307
|
-
await safeDelete(
|
|
310
|
+
await safeDelete(path2.join(dir, f));
|
|
308
311
|
}
|
|
309
312
|
} catch (err) {
|
|
310
313
|
writeErr(`[config-history] backup prune failed: ${toErrorMessage(err)}`);
|
|
@@ -322,16 +325,16 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
|
|
|
322
325
|
diffSummary: diffSummary(oldCfg, newCfg)
|
|
323
326
|
};
|
|
324
327
|
try {
|
|
325
|
-
await
|
|
326
|
-
|
|
328
|
+
await fs2.writeFile(
|
|
329
|
+
path2.join(historyDir(homeFn, targetConfigPath), `${id}.json`),
|
|
327
330
|
JSON.stringify(entry, null, 2),
|
|
328
331
|
{ encoding: "utf8", mode: 384 }
|
|
329
332
|
);
|
|
330
333
|
} catch (err) {
|
|
331
|
-
throw new
|
|
334
|
+
throw new FsError2({
|
|
332
335
|
message: toErrorMessage(err),
|
|
333
|
-
code:
|
|
334
|
-
path:
|
|
336
|
+
code: ERROR_CODES2.FS_WRITE_FAILED,
|
|
337
|
+
path: path2.join(historyDir(homeFn, targetConfigPath), `${id}.json`),
|
|
335
338
|
cause: err
|
|
336
339
|
});
|
|
337
340
|
}
|
|
@@ -350,10 +353,10 @@ async function listHistory(homeFn = defaultHomeDir, targetConfigPath) {
|
|
|
350
353
|
}
|
|
351
354
|
async function getHistoryEntry(id, homeFn = defaultHomeDir, targetConfigPath) {
|
|
352
355
|
const dir = historyDir(homeFn, targetConfigPath);
|
|
353
|
-
const filePath =
|
|
354
|
-
if (
|
|
356
|
+
const filePath = path2.join(dir, `${id}.json`);
|
|
357
|
+
if (path2.dirname(path2.resolve(filePath)) !== path2.resolve(dir)) return null;
|
|
355
358
|
try {
|
|
356
|
-
const raw = await
|
|
359
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
357
360
|
return JSON.parse(raw);
|
|
358
361
|
} catch {
|
|
359
362
|
return null;
|
|
@@ -374,7 +377,7 @@ async function restoreFromHistory(id, homeFn = defaultHomeDir, targetConfigPath)
|
|
|
374
377
|
await backupCurrent(homeFn, targetConfigPath);
|
|
375
378
|
let oldCfg = {};
|
|
376
379
|
try {
|
|
377
|
-
const raw = await
|
|
380
|
+
const raw = await fs2.readFile(cfg, "utf8");
|
|
378
381
|
oldCfg = JSON.parse(raw);
|
|
379
382
|
} catch {
|
|
380
383
|
}
|
|
@@ -399,7 +402,7 @@ async function restoreLast(homeFn = defaultHomeDir, targetConfigPath) {
|
|
|
399
402
|
const cfg = configPath(homeFn, targetConfigPath);
|
|
400
403
|
let lastCfg = {};
|
|
401
404
|
try {
|
|
402
|
-
const raw = await
|
|
405
|
+
const raw = await fs2.readFile(last, "utf8");
|
|
403
406
|
lastCfg = JSON.parse(raw);
|
|
404
407
|
} catch {
|
|
405
408
|
return { ok: false, error: "No prior backup found" };
|
|
@@ -410,7 +413,7 @@ async function restoreLast(homeFn = defaultHomeDir, targetConfigPath) {
|
|
|
410
413
|
return withFileLock(cfg, async () => {
|
|
411
414
|
let oldCfg = {};
|
|
412
415
|
try {
|
|
413
|
-
const raw = await
|
|
416
|
+
const raw = await fs2.readFile(cfg, "utf8");
|
|
414
417
|
oldCfg = JSON.parse(raw);
|
|
415
418
|
} catch {
|
|
416
419
|
}
|
|
@@ -439,4 +442,4 @@ export {
|
|
|
439
442
|
restoreFromHistory,
|
|
440
443
|
restoreLast
|
|
441
444
|
};
|
|
442
|
-
//# sourceMappingURL=chunk-
|
|
445
|
+
//# sourceMappingURL=chunk-KNYIEQAY.js.map
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
|
|
2
|
+
// src/tool-gateway.ts
|
|
3
|
+
import { Context } from "@wrongstack/core/agent";
|
|
4
|
+
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
5
|
+
import { ToolRegistry } from "@wrongstack/core/registry";
|
|
6
|
+
import {
|
|
7
|
+
AutoApprovePermissionPolicy,
|
|
8
|
+
DefaultSecretScrubber,
|
|
9
|
+
ToolCapabilities
|
|
10
|
+
} from "@wrongstack/core/security";
|
|
11
|
+
import { normalizeTokenSavingTier } from "@wrongstack/core/types";
|
|
12
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
13
|
+
import { systemSessionId } from "@wrongstack/primitives";
|
|
14
|
+
import { wireKanbanPorts } from "@wrongstack/runtime";
|
|
15
|
+
import { BROWSER_TOOL_NAMES } from "@wrongstack/tools/browser";
|
|
16
|
+
import { runStartupIndex, shutdownCodebaseIndexHost } from "@wrongstack/tools/codebase-index";
|
|
17
|
+
import { registerBuiltinToolTier } from "@wrongstack/tools/tool-tier";
|
|
18
|
+
function resolveGatewayRegistry(existing, config) {
|
|
19
|
+
wireKanbanPorts();
|
|
20
|
+
if (existing) return existing;
|
|
21
|
+
const registry = new ToolRegistry();
|
|
22
|
+
registerBuiltinToolTier({
|
|
23
|
+
registry,
|
|
24
|
+
tier: normalizeTokenSavingTier(config.features.tokenSavingMode)
|
|
25
|
+
});
|
|
26
|
+
return registry;
|
|
27
|
+
}
|
|
28
|
+
var READ_ONLY_CAPABILITIES = [
|
|
29
|
+
ToolCapabilities.FS_READ,
|
|
30
|
+
ToolCapabilities.NET_OUTBOUND
|
|
31
|
+
];
|
|
32
|
+
function parseCapabilityGrants(raw) {
|
|
33
|
+
if (raw === void 0 || raw === false) return [];
|
|
34
|
+
if (raw === true || raw.trim() === "") {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`--allow needs a comma-separated capability list, e.g. --allow fs.write (known: ${knownCapabilities()})`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const known = new Set(Object.values(ToolCapabilities));
|
|
40
|
+
const grants = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
41
|
+
const unknown = grants.filter((g) => !known.has(g));
|
|
42
|
+
if (unknown.length > 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`--allow: unknown capability ${unknown.map((u) => `"${u}"`).join(", ")} (known: ${knownCapabilities()})`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return [...new Set(grants)];
|
|
48
|
+
}
|
|
49
|
+
function knownCapabilities() {
|
|
50
|
+
return Object.values(ToolCapabilities).join(", ");
|
|
51
|
+
}
|
|
52
|
+
function yoloServePolicy(yoloPlus) {
|
|
53
|
+
return new AutoApprovePermissionPolicy(
|
|
54
|
+
Object.values(ToolCapabilities),
|
|
55
|
+
yoloPlus ? { yoloPlus } : void 0
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function resolveServeFsRestriction(config) {
|
|
59
|
+
return config.tools?.restrictToProjectRoot ?? true;
|
|
60
|
+
}
|
|
61
|
+
var GATEWAY_SESSION_ID = systemSessionId("tool-gateway");
|
|
62
|
+
function makeServeContext(cwd, projectRoot, signal, restrictFsToRoot = true) {
|
|
63
|
+
const provider = {
|
|
64
|
+
id: "mcp-serve",
|
|
65
|
+
capabilities: { maxContext: 0 },
|
|
66
|
+
complete: async () => {
|
|
67
|
+
throw new Error("no model provider in tool-gateway mode");
|
|
68
|
+
},
|
|
69
|
+
stream: () => {
|
|
70
|
+
throw new Error("no model provider in tool-gateway mode");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const session = {
|
|
74
|
+
id: GATEWAY_SESSION_ID,
|
|
75
|
+
append: async () => {
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const tokenCounter = {
|
|
79
|
+
account: () => {
|
|
80
|
+
},
|
|
81
|
+
total: () => ({ input: 0, output: 0 }),
|
|
82
|
+
estimateCost: () => ({ total: 0 })
|
|
83
|
+
};
|
|
84
|
+
return new Context({
|
|
85
|
+
systemPrompt: [],
|
|
86
|
+
provider,
|
|
87
|
+
session,
|
|
88
|
+
signal,
|
|
89
|
+
tokenCounter,
|
|
90
|
+
cwd,
|
|
91
|
+
projectRoot,
|
|
92
|
+
allowOutsideProjectRoot: !restrictFsToRoot,
|
|
93
|
+
model: "mcp-serve",
|
|
94
|
+
tools: []
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function selectExposedTools(registry, ctx, policy, whitelist) {
|
|
98
|
+
const allowed = [];
|
|
99
|
+
for (const tool of registry.list()) {
|
|
100
|
+
if (whitelist && !whitelist.has(tool.name)) continue;
|
|
101
|
+
const decision = await policy.evaluate(tool, {}, ctx);
|
|
102
|
+
if (decision.permission === "auto") allowed.push(tool);
|
|
103
|
+
}
|
|
104
|
+
return allowed;
|
|
105
|
+
}
|
|
106
|
+
var SESSION_BOUND_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
107
|
+
"clarify",
|
|
108
|
+
"tool_use",
|
|
109
|
+
"tool_script",
|
|
110
|
+
"tool_search",
|
|
111
|
+
"present_artifact",
|
|
112
|
+
"todo",
|
|
113
|
+
"plan"
|
|
114
|
+
]);
|
|
115
|
+
var PROCESS_BOUND_TOOL_NAMES = new Set(BROWSER_TOOL_NAMES);
|
|
116
|
+
var INDEX_READING_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
117
|
+
"codebase-search",
|
|
118
|
+
"codebase-context",
|
|
119
|
+
"codebase-impact-analysis",
|
|
120
|
+
"codebase-incoming-calls",
|
|
121
|
+
"codebase-outgoing-calls",
|
|
122
|
+
"codebase-repo-map",
|
|
123
|
+
"codebase-stats"
|
|
124
|
+
]);
|
|
125
|
+
var REMOTE_CATCH_UP_INTERVAL_MS = 1e3;
|
|
126
|
+
function readsCodebaseIndex(name) {
|
|
127
|
+
return INDEX_READING_TOOL_NAMES.has(name);
|
|
128
|
+
}
|
|
129
|
+
function oneShotWithholdReason(name) {
|
|
130
|
+
if (SESSION_BOUND_TOOL_NAMES.has(name)) return "session";
|
|
131
|
+
if (PROCESS_BOUND_TOOL_NAMES.has(name)) return "process";
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
function withheldReason(gateway, registry, name) {
|
|
135
|
+
const bound = gateway.withheldBy(name);
|
|
136
|
+
if (bound === "session") {
|
|
137
|
+
return `Tool "${name}" only works inside an agent session and is not exposed here.`;
|
|
138
|
+
}
|
|
139
|
+
if (bound === "process") {
|
|
140
|
+
return `Tool "${name}" keeps its state in a long-lived process; use \`wstack mcp serve\` or \`wstack tools serve\` for it.`;
|
|
141
|
+
}
|
|
142
|
+
const tool = registry.get(name);
|
|
143
|
+
if (!tool) return `Unknown tool "${name}". List the exposed tools first.`;
|
|
144
|
+
const missing = (tool.capabilities ?? []).filter((c) => !gateway.capabilities.includes(c));
|
|
145
|
+
return gateway.mode === "yolo" || missing.length === 0 ? `Tool "${name}" is withheld by policy.` : `Tool "${name}" is withheld: it needs ${missing.join(", ")}. Pass --allow ${missing.join(",")} (or --yolo for every capability); deleting outside the project, git-history and download-and-run commands stay refused.`;
|
|
146
|
+
}
|
|
147
|
+
async function createToolGateway(options) {
|
|
148
|
+
const { registry, config, cwd, projectRoot } = options;
|
|
149
|
+
const restrictFsToRoot = resolveServeFsRestriction(config);
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const grants = options.yolo ? [] : [...new Set(options.allow ?? [])];
|
|
152
|
+
const mode = options.yolo ? "yolo" : grants.length > 0 ? "granted" : "safe";
|
|
153
|
+
const capabilities = options.yolo ? Object.values(ToolCapabilities) : [.../* @__PURE__ */ new Set([...READ_ONLY_CAPABILITIES, ...grants])];
|
|
154
|
+
const permissionPolicy = options.yolo ? yoloServePolicy(options.yoloPlus) : new AutoApprovePermissionPolicy(capabilities);
|
|
155
|
+
const executor = new ToolExecutor(registry, {
|
|
156
|
+
permissionPolicy,
|
|
157
|
+
secretScrubber: new DefaultSecretScrubber(),
|
|
158
|
+
maxToolTimeoutMs: config.tools?.maxToolTimeoutMs ?? 3e5,
|
|
159
|
+
perIterationOutputCapBytes: 1e6,
|
|
160
|
+
// Kanban tracks work; it does not gate it. Off unless the operator opts in.
|
|
161
|
+
// See wiring/pipeline.ts for why all four hosts must agree.
|
|
162
|
+
requireKanbanGovernance: config.tools?.kanbanGovernance ?? false
|
|
163
|
+
});
|
|
164
|
+
const selectionCtx = makeServeContext(cwd, projectRoot, controller.signal, restrictFsToRoot);
|
|
165
|
+
const selected = await selectExposedTools(
|
|
166
|
+
registry,
|
|
167
|
+
selectionCtx,
|
|
168
|
+
permissionPolicy,
|
|
169
|
+
options.whitelist
|
|
170
|
+
);
|
|
171
|
+
const withheld = (name) => {
|
|
172
|
+
const reason = oneShotWithholdReason(name);
|
|
173
|
+
if (options.oneShot) return reason !== void 0;
|
|
174
|
+
if (options.remoteAgent) return reason === "session";
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
const tools = selected.filter((t) => !withheld(t.name));
|
|
178
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
179
|
+
const catchUpIndex = options.catchUpIndex ?? (async (root, signal) => {
|
|
180
|
+
await runStartupIndex({ projectRoot: root, signal });
|
|
181
|
+
});
|
|
182
|
+
let indexCurrent = false;
|
|
183
|
+
let lastCatchUpAt = 0;
|
|
184
|
+
let catchUpInFlight;
|
|
185
|
+
const runCatchUp = async () => {
|
|
186
|
+
try {
|
|
187
|
+
await catchUpIndex(projectRoot, controller.signal);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
options.onWarning?.(`codebase index catch-up failed: ${toErrorMessage(err)}`);
|
|
190
|
+
} finally {
|
|
191
|
+
indexCurrent = true;
|
|
192
|
+
lastCatchUpAt = Date.now();
|
|
193
|
+
catchUpInFlight = void 0;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const indexNeedsCatchUp = () => {
|
|
197
|
+
if (options.oneShot) return !indexCurrent;
|
|
198
|
+
if (options.remoteAgent) return Date.now() - lastCatchUpAt >= REMOTE_CATCH_UP_INTERVAL_MS;
|
|
199
|
+
return false;
|
|
200
|
+
};
|
|
201
|
+
let counter = 0;
|
|
202
|
+
return {
|
|
203
|
+
mode,
|
|
204
|
+
capabilities,
|
|
205
|
+
projectRoot,
|
|
206
|
+
tools,
|
|
207
|
+
get: (name) => byName.get(name),
|
|
208
|
+
withheldBy: (name) => withheld(name) ? oneShotWithholdReason(name) : void 0,
|
|
209
|
+
async call(name, input, callOptions) {
|
|
210
|
+
const tool = byName.get(name);
|
|
211
|
+
if (!tool) return { kind: "not_exposed" };
|
|
212
|
+
if (readsCodebaseIndex(name) && (catchUpInFlight || indexNeedsCatchUp())) {
|
|
213
|
+
catchUpInFlight ??= runCatchUp();
|
|
214
|
+
await catchUpInFlight;
|
|
215
|
+
}
|
|
216
|
+
if (tool.mutating) indexCurrent = false;
|
|
217
|
+
const use = { type: "tool_use", id: `srv_${++counter}`, name, input };
|
|
218
|
+
const requestCtx = makeServeContext(
|
|
219
|
+
callOptions?.cwd ?? cwd,
|
|
220
|
+
projectRoot,
|
|
221
|
+
controller.signal,
|
|
222
|
+
restrictFsToRoot
|
|
223
|
+
);
|
|
224
|
+
const batch = await executor.executeBatch([use], requestCtx, "sequential");
|
|
225
|
+
const output = batch.outputs[0];
|
|
226
|
+
const result = output?.result;
|
|
227
|
+
if (!output || !result || result.type === "tool_confirm_pending") {
|
|
228
|
+
return { kind: "confirm_required" };
|
|
229
|
+
}
|
|
230
|
+
const isError = Boolean(result.is_error);
|
|
231
|
+
return {
|
|
232
|
+
kind: "result",
|
|
233
|
+
content: result.content,
|
|
234
|
+
isError,
|
|
235
|
+
settlement: output.settlement ?? (isError ? "failed" : "completed"),
|
|
236
|
+
durationMs: output.durationMs
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
async close() {
|
|
240
|
+
controller.abort();
|
|
241
|
+
await shutdownCodebaseIndexHost();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export {
|
|
247
|
+
resolveGatewayRegistry,
|
|
248
|
+
parseCapabilityGrants,
|
|
249
|
+
withheldReason,
|
|
250
|
+
createToolGateway
|
|
251
|
+
};
|
|
252
|
+
//# sourceMappingURL=chunk-LYWOV4OX.js.map
|