@wrongstack/cli 1.1.2 → 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-HCGUTFNE.js → acp-NKUEYJDZ.js} +14 -5
- package/dist/acp-leader/acp-leader-confirm.d.ts +20 -0
- package/dist/acp-leader/acp-leader-context.d.ts +29 -0
- package/dist/acp-leader/acp-leader-fleet.d.ts +26 -0
- package/dist/acp-leader/acp-leader-host-binding.d.ts +17 -0
- package/dist/acp-leader/acp-leader-host.d.ts +82 -0
- package/dist/acp-leader/acp-leader-picker.d.ts +24 -0
- package/dist/acp-leader/acp-leader-provider.d.ts +85 -0
- package/dist/acp-leader/acp-leader-resume.d.ts +10 -0
- package/dist/acp-leader/acp-leader-stream.d.ts +83 -0
- package/dist/{auth-ANOVZVHE.js → auth-2ZG5DBHS.js} +62 -66
- package/dist/auth-menu/index.d.ts +1 -2
- package/dist/boot/dispatch-webui.d.ts +16 -0
- 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-3LEIFO5D.js +65 -0
- package/dist/chunk-5SGMUDP5.js +752 -0
- package/dist/{chunk-3EXRPTAU.js → chunk-5TQ65PXQ.js} +17 -17
- package/dist/{chunk-GRAGMA4B.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-5TRZZHMN.js → chunk-JCZ2D3VV.js} +4 -65
- package/dist/{chunk-AWRZWPJ3.js → chunk-JFYQGMMM.js} +99 -75
- package/dist/{chunk-BIGSVBPI.js → chunk-KNYIEQAY.js} +64 -61
- package/dist/chunk-LUEO5HLZ.js +50 -0
- package/dist/chunk-LYWOV4OX.js +252 -0
- package/dist/chunk-M7FNHCE3.js +2070 -0
- package/dist/{chunk-75OLD563.js → chunk-M7PSENGF.js} +3 -1
- package/dist/{chunk-NMXQFQ26.js → chunk-MEBXWI7U.js} +5 -5
- package/dist/{chunk-RAWHRRUJ.js → chunk-MKMM5RIW.js} +209 -201
- package/dist/{chunk-3R3OADC5.js → chunk-P7MEEUGT.js} +281 -252
- 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-YIT4FEDM.js → cli-context-CQYGIWOU.js} +254 -237
- package/dist/{cli-main-JIADJ5OL.js → cli-main-VRUJNUBX.js} +2081 -1594
- 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-BAP566QC.js → execution-IV72RUSM.js} +5354 -5258
- package/dist/execution-tui.d.ts +30 -0
- package/dist/fleet/acp-live-steer.d.ts +28 -0
- package/dist/fleet/acp-permission-bridge.d.ts +49 -0
- package/dist/fleet/acp-quota-hold.d.ts +3 -0
- package/dist/fleet/acp-quota-reading.d.ts +3 -0
- package/dist/fleet/acp-turn-reminders.d.ts +15 -0
- package/dist/fleet/acp-wrongstack-bridge.d.ts +50 -0
- package/dist/fleet/host-acp.d.ts +23 -2
- 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 +8 -5
- package/dist/mcp-22CFBX4H.js +236 -0
- package/dist/mcp-serve-project.d.ts +18 -0
- package/dist/mcp-serve.d.ts +0 -63
- package/dist/{modeldiag-HDIKG37X.js → modeldiag-5PBB777J.js} +4 -4
- package/dist/{per-subcommand-help-D6J4ZXW2.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-VTMIZQBP.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-HIUY3RHD.js → webui-server-V2R5WQUK.js} +79 -3
- 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 +34 -30
- package/dist/auth-menu/anthropic-oauth.d.ts +0 -43
- package/dist/chunk-XT3NAHRX.js +0 -337
- package/dist/chunk-ZNNFEKDZ.js +0 -319
- package/dist/mcp-2XWBJEQN.js +0 -464
- package/dist/short-circuit-flags-CPCT5MWB.js +0 -11
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createToolGateway,
|
|
3
|
+
parseCapabilityGrants,
|
|
4
|
+
resolveGatewayRegistry
|
|
5
|
+
} from "./chunk-LYWOV4OX.js";
|
|
6
|
+
|
|
7
|
+
// src/mcp-serve.ts
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_MCP_INSERTION_MAX_BYTES,
|
|
14
|
+
MCPServer,
|
|
15
|
+
serveHttp,
|
|
16
|
+
serveStdio
|
|
17
|
+
} from "@wrongstack/mcp";
|
|
18
|
+
function parseToolsFlag(flags, positional) {
|
|
19
|
+
const raw = flags["tools"];
|
|
20
|
+
const csv = typeof raw === "string" ? raw : raw === true ? positional?.[0] ?? "" : "";
|
|
21
|
+
if (!csv) return null;
|
|
22
|
+
const set = new Set(
|
|
23
|
+
csv.split(",").map((s) => s.trim()).filter(Boolean)
|
|
24
|
+
);
|
|
25
|
+
return set.size > 0 ? set : null;
|
|
26
|
+
}
|
|
27
|
+
function resolveServeHttpPort(flags) {
|
|
28
|
+
const raw = typeof flags["port"] === "string" ? flags["port"] : typeof flags["http"] === "string" ? flags["http"] : void 0;
|
|
29
|
+
if (raw === void 0 || raw.trim() === "") return 0;
|
|
30
|
+
const port = Number(raw);
|
|
31
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
32
|
+
throw new Error(`invalid --port "${raw}" \u2014 expected an integer between 0 and 65535`);
|
|
33
|
+
}
|
|
34
|
+
return port;
|
|
35
|
+
}
|
|
36
|
+
async function loadSelectedMcpServeContent(flags, cwd) {
|
|
37
|
+
const resources = [];
|
|
38
|
+
const prompts = [];
|
|
39
|
+
const resourceUris = /* @__PURE__ */ new Set();
|
|
40
|
+
const promptNames = /* @__PURE__ */ new Set();
|
|
41
|
+
for (const selected of commaSeparatedFlag(flags, "resources")) {
|
|
42
|
+
const file = path.resolve(cwd, selected);
|
|
43
|
+
const data = await readSelectedFile(file);
|
|
44
|
+
const uri = pathToFileURL(file).toString();
|
|
45
|
+
if (resourceUris.has(uri)) throw new Error(`Duplicate MCP resource selection: ${selected}`);
|
|
46
|
+
resourceUris.add(uri);
|
|
47
|
+
const mimeType = mimeTypeFor(file);
|
|
48
|
+
resources.push({
|
|
49
|
+
uri,
|
|
50
|
+
name: path.basename(file),
|
|
51
|
+
mimeType,
|
|
52
|
+
size: data.byteLength,
|
|
53
|
+
contents: [
|
|
54
|
+
isTextMime(mimeType) ? { uri, mimeType, text: data.toString("utf8") } : { uri, mimeType, blob: data.toString("base64") }
|
|
55
|
+
]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
for (const selected of commaSeparatedFlag(flags, "prompts")) {
|
|
59
|
+
const file = path.resolve(cwd, selected);
|
|
60
|
+
const data = await readSelectedFile(file);
|
|
61
|
+
const name = path.basename(file, path.extname(file)).replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
62
|
+
if (!name) throw new Error(`Cannot derive an MCP prompt name from: ${selected}`);
|
|
63
|
+
if (promptNames.has(name)) throw new Error(`Duplicate MCP prompt name: ${name}`);
|
|
64
|
+
promptNames.add(name);
|
|
65
|
+
const template = data.toString("utf8");
|
|
66
|
+
const argumentNames = Array.from(
|
|
67
|
+
template.matchAll(/\{\{([A-Za-z_][A-Za-z0-9_.-]*)\}\}/g),
|
|
68
|
+
(match) => match[1]
|
|
69
|
+
).filter((value, index, all) => !!value && all.indexOf(value) === index);
|
|
70
|
+
prompts.push({
|
|
71
|
+
name,
|
|
72
|
+
description: `Explicitly selected local prompt: ${path.basename(file)}`,
|
|
73
|
+
arguments: argumentNames.map((argument) => ({ name: argument, required: true })),
|
|
74
|
+
template
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return { resources, prompts };
|
|
78
|
+
}
|
|
79
|
+
function commaSeparatedFlag(flags, name) {
|
|
80
|
+
const value = flags[name];
|
|
81
|
+
if (value === void 0) return [];
|
|
82
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
83
|
+
throw new Error(`--${name} requires a comma-separated file list`);
|
|
84
|
+
}
|
|
85
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
async function readSelectedFile(file) {
|
|
88
|
+
const stat2 = await fs.stat(file);
|
|
89
|
+
if (!stat2.isFile()) throw new Error(`Selected MCP content is not a regular file: ${file}`);
|
|
90
|
+
if (stat2.size > DEFAULT_MCP_INSERTION_MAX_BYTES) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Selected MCP content exceeds ${DEFAULT_MCP_INSERTION_MAX_BYTES} bytes: ${file}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return fs.readFile(file);
|
|
96
|
+
}
|
|
97
|
+
function mimeTypeFor(file) {
|
|
98
|
+
switch (path.extname(file).toLowerCase()) {
|
|
99
|
+
case ".md":
|
|
100
|
+
case ".mdx":
|
|
101
|
+
return "text/markdown";
|
|
102
|
+
case ".json":
|
|
103
|
+
return "application/json";
|
|
104
|
+
case ".yaml":
|
|
105
|
+
case ".yml":
|
|
106
|
+
return "application/yaml";
|
|
107
|
+
case ".html":
|
|
108
|
+
return "text/html";
|
|
109
|
+
case ".css":
|
|
110
|
+
return "text/css";
|
|
111
|
+
case ".js":
|
|
112
|
+
case ".mjs":
|
|
113
|
+
return "text/javascript";
|
|
114
|
+
case ".ts":
|
|
115
|
+
case ".tsx":
|
|
116
|
+
return "text/typescript";
|
|
117
|
+
case ".txt":
|
|
118
|
+
case ".log":
|
|
119
|
+
return "text/plain";
|
|
120
|
+
case ".png":
|
|
121
|
+
return "image/png";
|
|
122
|
+
case ".jpg":
|
|
123
|
+
case ".jpeg":
|
|
124
|
+
return "image/jpeg";
|
|
125
|
+
case ".gif":
|
|
126
|
+
return "image/gif";
|
|
127
|
+
case ".webp":
|
|
128
|
+
return "image/webp";
|
|
129
|
+
default:
|
|
130
|
+
return "application/octet-stream";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function isTextMime(mimeType) {
|
|
134
|
+
return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/yaml";
|
|
135
|
+
}
|
|
136
|
+
async function serveMcpStdio(deps, positional) {
|
|
137
|
+
const flags = deps.flags ?? {};
|
|
138
|
+
const yolo = flags["yolo"] === true || flags["allow-all"] === true;
|
|
139
|
+
const whitelist = parseToolsFlag(flags, positional);
|
|
140
|
+
if (flags["tools"] !== void 0 && whitelist === null) {
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
"wrongstack MCP server: --tools requires a comma-separated tool list (e.g. `--tools read,grep` or `--tools=read,grep`)\n"
|
|
143
|
+
);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
const log = (m) => process.stderr.write(`${m}
|
|
147
|
+
`);
|
|
148
|
+
let selectedContent;
|
|
149
|
+
try {
|
|
150
|
+
selectedContent = await loadSelectedMcpServeContent(flags, deps.cwd);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
log(`wrongstack MCP server: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
let allow;
|
|
156
|
+
try {
|
|
157
|
+
allow = parseCapabilityGrants(flags["allow"]);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
log(`wrongstack MCP server: ${toErrorMessage(err)}`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
const gateway = await createToolGateway({
|
|
163
|
+
registry: resolveGatewayRegistry(deps.toolRegistry, deps.config),
|
|
164
|
+
config: deps.config,
|
|
165
|
+
cwd: deps.cwd,
|
|
166
|
+
projectRoot: deps.projectRoot,
|
|
167
|
+
yolo,
|
|
168
|
+
allow,
|
|
169
|
+
whitelist
|
|
170
|
+
});
|
|
171
|
+
const allowed = gateway.tools;
|
|
172
|
+
if (allowed.length === 0) {
|
|
173
|
+
log(
|
|
174
|
+
"wrongstack MCP server: no tools to expose (all withheld by policy or filtered out). Pass --yolo to expose write/exec tools, or --tools <names> to whitelist."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const host = {
|
|
178
|
+
listTools: () => allowed.map((t) => ({
|
|
179
|
+
name: t.name,
|
|
180
|
+
description: t.description,
|
|
181
|
+
inputSchema: t.inputSchema ?? { type: "object" }
|
|
182
|
+
})),
|
|
183
|
+
callTool: async (name, callArgs) => {
|
|
184
|
+
const outcome = await gateway.call(name, callArgs);
|
|
185
|
+
if (outcome.kind === "not_exposed") {
|
|
186
|
+
return { content: `Tool "${name}" is not exposed by this server`, isError: true };
|
|
187
|
+
}
|
|
188
|
+
if (outcome.kind === "confirm_required") {
|
|
189
|
+
return {
|
|
190
|
+
content: `Tool "${name}" requires interactive confirmation, which is unavailable over MCP`,
|
|
191
|
+
isError: true
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { content: outcome.content, isError: outcome.isError };
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const server = new MCPServer({
|
|
198
|
+
host,
|
|
199
|
+
resources: selectedContent.resources,
|
|
200
|
+
prompts: selectedContent.prompts,
|
|
201
|
+
logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
|
|
202
|
+
});
|
|
203
|
+
const mode = gateway.mode === "yolo" ? "yolo: all tools" : gateway.mode === "granted" ? `granted: ${gateway.capabilities.join(", ")}` : "safe: read-only tools";
|
|
204
|
+
if (flags["http"] === true || typeof flags["http"] === "string" || flags["port"] || flags["host"]) {
|
|
205
|
+
let port;
|
|
206
|
+
try {
|
|
207
|
+
port = resolveServeHttpPort(flags);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
log(`wrongstack MCP server: ${err instanceof Error ? err.message : String(err)}`);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
const httpHost = typeof flags["host"] === "string" ? flags["host"] : "127.0.0.1";
|
|
213
|
+
const tokenFromArg = typeof flags["token"] === "string" ? flags["token"] : void 0;
|
|
214
|
+
const token = tokenFromArg ?? process.env.WRONGSTACK_MCP_TOKEN?.trim() ?? void 0;
|
|
215
|
+
if (tokenFromArg) {
|
|
216
|
+
log(
|
|
217
|
+
"[mcp-serve] --token puts the auth token in this process command line, which other local processes can read. Prefer WRONGSTACK_MCP_TOKEN."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
let handle2;
|
|
221
|
+
try {
|
|
222
|
+
handle2 = await serveHttp(server, {
|
|
223
|
+
port,
|
|
224
|
+
host: httpHost,
|
|
225
|
+
token,
|
|
226
|
+
logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
|
|
227
|
+
});
|
|
228
|
+
} catch (err) {
|
|
229
|
+
log(`wrongstack MCP server: ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
log(
|
|
233
|
+
`wrongstack MCP server ready at ${handle2.url} \u2014 exposing ${allowed.length} tool(s), ${selectedContent.resources.length} resource(s), ${selectedContent.prompts.length} prompt(s) (${mode})${token ? " [token auth]" : ""}.`
|
|
234
|
+
);
|
|
235
|
+
await new Promise((resolve2) => {
|
|
236
|
+
const stop = () => resolve2();
|
|
237
|
+
process.once("SIGINT", stop);
|
|
238
|
+
process.once("SIGTERM", stop);
|
|
239
|
+
});
|
|
240
|
+
await handle2.close();
|
|
241
|
+
await gateway.close();
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
log(
|
|
245
|
+
`wrongstack MCP server ready on stdio \u2014 exposing ${allowed.length} tool(s), ${selectedContent.resources.length} resource(s), ${selectedContent.prompts.length} prompt(s) (${mode}).`
|
|
246
|
+
);
|
|
247
|
+
const handle = serveStdio(server);
|
|
248
|
+
await handle.done;
|
|
249
|
+
await gateway.close();
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export {
|
|
254
|
+
parseToolsFlag,
|
|
255
|
+
resolveServeHttpPort,
|
|
256
|
+
serveMcpStdio
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=chunk-QLYPIJ7X.js.map
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withheldReason
|
|
3
|
+
} from "./chunk-LYWOV4OX.js";
|
|
4
|
+
|
|
5
|
+
// src/tool-gateway-http.ts
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { realpath } from "node:fs/promises";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
11
|
+
import {
|
|
12
|
+
bearerTokenMatches,
|
|
13
|
+
hostHeaderIsAcceptable,
|
|
14
|
+
isJsonContentType,
|
|
15
|
+
isLoopbackHost,
|
|
16
|
+
originIsAcceptable
|
|
17
|
+
} from "@wrongstack/mcp";
|
|
18
|
+
var BODY_CAP_BYTES = 4 * 1024 * 1024;
|
|
19
|
+
var DEFAULT_MAX_CONCURRENT_CALLS = 16;
|
|
20
|
+
var CALLBACK_TIMEOUT_MS = 3e4;
|
|
21
|
+
function serveToolGatewayHttp(opts) {
|
|
22
|
+
const host = opts.host ?? "127.0.0.1";
|
|
23
|
+
if (!opts.token || opts.token.length < 16) {
|
|
24
|
+
return Promise.reject(
|
|
25
|
+
new Error("tools serve: a bearer token of at least 16 characters is required")
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const maxConcurrent = opts.maxConcurrentCalls ?? DEFAULT_MAX_CONCURRENT_CALLS;
|
|
29
|
+
const pending = /* @__PURE__ */ new Set();
|
|
30
|
+
let inFlight = 0;
|
|
31
|
+
const server = createServer((req, res) => {
|
|
32
|
+
void handle(req, res).catch((err) => {
|
|
33
|
+
opts.log?.(`tools serve: request failed: ${toErrorMessage(err)}`);
|
|
34
|
+
if (!res.headersSent) send(res, 500, { ok: false, error: "internal_error" });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
async function handle(req, res) {
|
|
38
|
+
if (!originIsAcceptable(req, host))
|
|
39
|
+
return send(res, 403, { ok: false, error: "cross_origin_forbidden" });
|
|
40
|
+
if (!hostHeaderIsAcceptable(req, host))
|
|
41
|
+
return send(res, 403, { ok: false, error: "untrusted_host_header" });
|
|
42
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
43
|
+
if (req.method === "GET" && url.pathname === "/health") return send(res, 200, { ok: true });
|
|
44
|
+
if (!bearerTokenMatches(req.headers.authorization, opts.token)) {
|
|
45
|
+
return send(res, 401, { ok: false, error: "unauthorized" });
|
|
46
|
+
}
|
|
47
|
+
let parts;
|
|
48
|
+
try {
|
|
49
|
+
parts = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
50
|
+
} catch {
|
|
51
|
+
return send(res, 400, { ok: false, error: "invalid_path_encoding" });
|
|
52
|
+
}
|
|
53
|
+
if (parts[0] !== "v1" || parts[1] !== "tools")
|
|
54
|
+
return send(res, 404, { ok: false, error: "not_found" });
|
|
55
|
+
const name = parts[2];
|
|
56
|
+
if (req.method === "GET" && parts.length === 2) {
|
|
57
|
+
return send(res, 200, {
|
|
58
|
+
mode: opts.gateway.mode,
|
|
59
|
+
capabilities: opts.gateway.capabilities,
|
|
60
|
+
tools: opts.gateway.tools.map(describe)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (req.method === "GET" && name && parts.length === 3) {
|
|
64
|
+
const tool = opts.gateway.get(name);
|
|
65
|
+
if (!tool) return send(res, 404, notExposedBody(name));
|
|
66
|
+
return send(res, 200, describe(tool));
|
|
67
|
+
}
|
|
68
|
+
if (req.method === "POST" && name && parts.length === 4 && parts[3] === "call") {
|
|
69
|
+
return call(req, res, name);
|
|
70
|
+
}
|
|
71
|
+
return send(res, req.method === "GET" || req.method === "POST" ? 404 : 405, {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: req.method === "GET" || req.method === "POST" ? "not_found" : "method_not_allowed"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function call(req, res, name) {
|
|
77
|
+
if (!isJsonContentType(req.headers["content-type"])) {
|
|
78
|
+
return send(res, 415, { ok: false, error: "content_type_must_be_json" });
|
|
79
|
+
}
|
|
80
|
+
const body = await readBody(req);
|
|
81
|
+
if (body === "too_large") return send(res, 413, { ok: false, error: "payload_too_large" });
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = parseCallRequest(body, opts.allowRemoteCallbacks === true);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return send(res, 400, { ok: false, error: "bad_request", message: toErrorMessage(err) });
|
|
87
|
+
}
|
|
88
|
+
if (!opts.gateway.get(name)) {
|
|
89
|
+
return send(res, 404, { call_id: parsed.callId, ...notExposedBody(name) });
|
|
90
|
+
}
|
|
91
|
+
let cwd;
|
|
92
|
+
if (parsed.workdir !== void 0) {
|
|
93
|
+
const resolved = await resolveWorkdir(parsed.workdir, opts.gateway.projectRoot);
|
|
94
|
+
if ("error" in resolved) {
|
|
95
|
+
return send(res, 400, {
|
|
96
|
+
call_id: parsed.callId,
|
|
97
|
+
ok: false,
|
|
98
|
+
tool: name,
|
|
99
|
+
error: "bad_workdir",
|
|
100
|
+
message: resolved.error
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
cwd = resolved.cwd;
|
|
104
|
+
}
|
|
105
|
+
if (inFlight >= maxConcurrent) {
|
|
106
|
+
return send(res, 429, { call_id: parsed.callId, ok: false, error: "too_many_calls" });
|
|
107
|
+
}
|
|
108
|
+
inFlight++;
|
|
109
|
+
const settle = run(name, parsed, cwd).finally(() => {
|
|
110
|
+
inFlight--;
|
|
111
|
+
});
|
|
112
|
+
if (!parsed.callback) {
|
|
113
|
+
return send(res, 200, await settle);
|
|
114
|
+
}
|
|
115
|
+
send(res, 202, { call_id: parsed.callId, status: "accepted" });
|
|
116
|
+
const delivery = settle.then((result) => deliverCallback(parsed.callback, result)).catch(
|
|
117
|
+
(err) => opts.log?.(`tools serve: callback for ${parsed.callId} failed: ${toErrorMessage(err)}`)
|
|
118
|
+
);
|
|
119
|
+
pending.add(delivery);
|
|
120
|
+
void delivery.finally(() => pending.delete(delivery));
|
|
121
|
+
}
|
|
122
|
+
async function run(name, req, cwd) {
|
|
123
|
+
const base = { call_id: req.callId, tool: name };
|
|
124
|
+
try {
|
|
125
|
+
const outcome = await opts.gateway.call(name, req.input, { cwd });
|
|
126
|
+
if (outcome.kind === "not_exposed") return { ...base, ...notExposedBody(name) };
|
|
127
|
+
if (outcome.kind === "confirm_required") {
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
ok: false,
|
|
131
|
+
error: "confirm_required",
|
|
132
|
+
message: `Tool "${name}" needs an interactive confirmation for this input, which this server cannot give.`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
ok: !outcome.isError && outcome.settlement === "completed",
|
|
138
|
+
settlement: outcome.settlement,
|
|
139
|
+
duration_ms: outcome.durationMs,
|
|
140
|
+
content: outcome.content
|
|
141
|
+
};
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return { ...base, ok: false, error: "tool_threw", message: toErrorMessage(err) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function deliverCallback(callback, result) {
|
|
147
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
148
|
+
const res = await doFetch(callback.url, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"content-type": "application/json",
|
|
152
|
+
...callback.token ? { authorization: `Bearer ${callback.token}` } : {}
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify(result),
|
|
155
|
+
signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS),
|
|
156
|
+
redirect: "error"
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) throw new Error(`callback answered HTTP ${res.status}`);
|
|
159
|
+
}
|
|
160
|
+
function notExposedBody(name) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
tool: name,
|
|
164
|
+
error: "not_exposed",
|
|
165
|
+
message: withheldReason(opts.gateway, opts.registry, name)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return new Promise((resolve2, reject) => {
|
|
169
|
+
server.once("error", reject);
|
|
170
|
+
server.listen(opts.port ?? 0, host, () => {
|
|
171
|
+
server.removeListener("error", reject);
|
|
172
|
+
if (opts.unref) server.unref();
|
|
173
|
+
const port = server.address().port;
|
|
174
|
+
const shown = host === "::1" ? "[::1]" : host;
|
|
175
|
+
resolve2({
|
|
176
|
+
url: `http://${shown}:${port}/`,
|
|
177
|
+
port,
|
|
178
|
+
host,
|
|
179
|
+
close: async () => {
|
|
180
|
+
await new Promise((done) => server.close(() => done()));
|
|
181
|
+
await Promise.allSettled([...pending]);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function describe(tool) {
|
|
188
|
+
return {
|
|
189
|
+
name: tool.name,
|
|
190
|
+
description: tool.description,
|
|
191
|
+
input_schema: tool.inputSchema ?? { type: "object" },
|
|
192
|
+
capabilities: tool.capabilities ?? [],
|
|
193
|
+
mutating: tool.mutating
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function parseCallRequest(body, allowRemoteCallbacks) {
|
|
197
|
+
const text = body.trim();
|
|
198
|
+
const raw = text === "" ? {} : JSON.parse(text);
|
|
199
|
+
if (!isRecord(raw)) throw new Error("the body must be a JSON object");
|
|
200
|
+
const input = raw["input"] ?? {};
|
|
201
|
+
if (!isRecord(input)) throw new Error(`"input" must be a JSON object (the tool's input)`);
|
|
202
|
+
const callId = raw["call_id"] ?? randomUUID();
|
|
203
|
+
if (typeof callId !== "string" || callId.length === 0 || callId.length > 200) {
|
|
204
|
+
throw new Error('"call_id" must be a non-empty string of at most 200 characters');
|
|
205
|
+
}
|
|
206
|
+
const cb = raw["callback"];
|
|
207
|
+
const workdir = raw["workdir"];
|
|
208
|
+
if (workdir !== void 0 && workdir !== null && (typeof workdir !== "string" || workdir === "")) {
|
|
209
|
+
throw new Error('"workdir" must be a non-empty path');
|
|
210
|
+
}
|
|
211
|
+
const base = { input, callId, ...typeof workdir === "string" ? { workdir } : {} };
|
|
212
|
+
if (cb === void 0) return base;
|
|
213
|
+
if (!isRecord(cb) || typeof cb["url"] !== "string") {
|
|
214
|
+
throw new Error('"callback" must be { "url": string, "token"?: string }');
|
|
215
|
+
}
|
|
216
|
+
let target;
|
|
217
|
+
try {
|
|
218
|
+
target = new URL(cb["url"]);
|
|
219
|
+
} catch {
|
|
220
|
+
throw new Error('"callback.url" is not a valid URL');
|
|
221
|
+
}
|
|
222
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
223
|
+
throw new Error('"callback.url" must be http(s)');
|
|
224
|
+
}
|
|
225
|
+
if (!allowRemoteCallbacks && !isLoopbackHost(target.hostname.replace(/^\[(.*)\]$/, "$1"))) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'"callback.url" must be a loopback address (start the server with --allow-remote-callbacks)'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const token = cb["token"];
|
|
231
|
+
if (token !== void 0 && typeof token !== "string")
|
|
232
|
+
throw new Error('"callback.token" must be a string');
|
|
233
|
+
return { ...base, callback: { url: target.toString(), token } };
|
|
234
|
+
}
|
|
235
|
+
async function resolveWorkdir(workdir, projectRoot) {
|
|
236
|
+
let root;
|
|
237
|
+
let target;
|
|
238
|
+
try {
|
|
239
|
+
root = await realpath(projectRoot);
|
|
240
|
+
target = await realpath(path.resolve(projectRoot, workdir));
|
|
241
|
+
} catch {
|
|
242
|
+
return { error: `workdir "${workdir}" does not exist on the WrongStack side` };
|
|
243
|
+
}
|
|
244
|
+
const fold = (p) => process.platform === "win32" ? p.toLowerCase() : p;
|
|
245
|
+
const rel = path.relative(fold(root), fold(target));
|
|
246
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
247
|
+
return {
|
|
248
|
+
error: `workdir "${workdir}" is outside the project this server serves (${root}); start \`wstack tools serve\` in the task's directory`
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return { cwd: target };
|
|
252
|
+
}
|
|
253
|
+
function readBody(req) {
|
|
254
|
+
return new Promise((resolve2, reject) => {
|
|
255
|
+
const chunks = [];
|
|
256
|
+
let size = 0;
|
|
257
|
+
let tooLarge = false;
|
|
258
|
+
req.on("data", (chunk) => {
|
|
259
|
+
if (tooLarge) return;
|
|
260
|
+
size += chunk.byteLength;
|
|
261
|
+
if (size > BODY_CAP_BYTES) {
|
|
262
|
+
tooLarge = true;
|
|
263
|
+
resolve2("too_large");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
chunks.push(chunk);
|
|
267
|
+
});
|
|
268
|
+
req.on("end", () => {
|
|
269
|
+
if (!tooLarge) resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
270
|
+
});
|
|
271
|
+
req.on("error", reject);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function send(res, status, body) {
|
|
275
|
+
res.writeHead(status, {
|
|
276
|
+
"content-type": "application/json",
|
|
277
|
+
// This endpoint executes tools: nothing about it should be embedded,
|
|
278
|
+
// sniffed into another type, or leak its URL onward. No CORS headers.
|
|
279
|
+
"x-content-type-options": "nosniff",
|
|
280
|
+
"x-frame-options": "DENY",
|
|
281
|
+
"referrer-policy": "no-referrer"
|
|
282
|
+
});
|
|
283
|
+
res.end(JSON.stringify(body));
|
|
284
|
+
}
|
|
285
|
+
function isRecord(value) {
|
|
286
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export {
|
|
290
|
+
serveToolGatewayHttp
|
|
291
|
+
};
|
|
292
|
+
//# sourceMappingURL=chunk-RN5CFAVQ.js.map
|
|
@@ -61,6 +61,11 @@ var SDDState = class {
|
|
|
61
61
|
if (this.versioning === null) this.versioning = new SpecVersioning();
|
|
62
62
|
return this.versioning;
|
|
63
63
|
}
|
|
64
|
+
/** Drain fire-and-forget tracker writes before deleting or replacing the store. */
|
|
65
|
+
async flushTaskWrites() {
|
|
66
|
+
await this.taskTracker?.flushPersistence();
|
|
67
|
+
await this.taskStore?.flush?.();
|
|
68
|
+
}
|
|
64
69
|
clearTaskState() {
|
|
65
70
|
this.taskStore = null;
|
|
66
71
|
this.taskTracker = null;
|
|
@@ -406,4 +411,4 @@ export {
|
|
|
406
411
|
getTaskGraphId,
|
|
407
412
|
getTaskTrackerExport
|
|
408
413
|
};
|
|
409
|
-
//# sourceMappingURL=chunk-
|
|
414
|
+
//# sourceMappingURL=chunk-TDP7CZGE.js.map
|
|
@@ -63,6 +63,7 @@ var helpCmd = async (_args, deps) => {
|
|
|
63
63
|
["wstack sage sync", "Keep project SAGE memory synchronized through HQ"],
|
|
64
64
|
["wstack plugin", "Plugins: list, install, enable, disable, remove"],
|
|
65
65
|
["wstack tools | skills", "List registered tools / discovered skills"],
|
|
66
|
+
["wstack tools run <tool>", "Call a built-in tool from a script or external agent"],
|
|
66
67
|
["wstack typesafe", "TypeSafe account: status, login, test"],
|
|
67
68
|
["wstack update", "Self-update (--check-only)"]
|
|
68
69
|
]),
|
|
@@ -157,4 +158,4 @@ export {
|
|
|
157
158
|
versionCmd,
|
|
158
159
|
helpCmd
|
|
159
160
|
};
|
|
160
|
-
//# sourceMappingURL=chunk-
|
|
161
|
+
//# sourceMappingURL=chunk-UROB7MK5.js.map
|