stitchkit 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -10
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +5 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +17 -0
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +21 -0
- package/dist/contract/define.d.ts +35 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -1
- package/dist/index-0ed3bx43.js +33 -0
- package/dist/index-0ma1eqv4.js +1007 -0
- package/dist/index-1cf8jkhf.js +219 -0
- package/dist/index-37x76zdn.js +4 -0
- package/dist/{index-v2z2v3mq.js → index-3qmtdfpa.js} +304 -62
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/{index-5sxnvwb1.js → index-78q1qm7v.js} +6 -3
- package/dist/{index-kckky6zw.js → index-afzt3nmx.js} +1 -1
- package/dist/{index-n7bmdwmz.js → index-d4rwrjbc.js} +8 -4
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/index-mwmpw6j1.js +60 -0
- package/dist/{index-a35v22fh.js → index-vhdvv00d.js} +13 -5
- package/dist/index-x3fcszf8.js +8 -0
- package/dist/index.js +17 -4
- package/dist/internal/errors.d.ts +13 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/http-input.d.ts +13 -0
- package/dist/internal/http-input.d.ts.map +1 -0
- package/dist/internal/safe-json.d.ts +19 -0
- package/dist/internal/safe-json.d.ts.map +1 -0
- package/dist/internal/secure-fetch.d.ts +27 -0
- package/dist/internal/secure-fetch.d.ts.map +1 -0
- package/dist/internal/within-dir.d.ts +7 -0
- package/dist/internal/within-dir.d.ts.map +1 -0
- package/dist/node.d.ts +10 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +41 -7
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/context.d.ts +9 -1
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/event.d.ts +4 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +38 -15
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/react.js +2 -0
- package/dist/server/context.d.ts +3 -2
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/event-bus.d.ts +10 -1
- package/dist/server/event-bus.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +203 -193
- package/dist/server/logger.d.ts +5 -4
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware/auth.d.ts +41 -1
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/middleware/cookies.d.ts.map +1 -1
- package/dist/server/middleware/cors.d.ts +7 -0
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/middleware/pkce.d.ts +17 -0
- package/dist/server/middleware/pkce.d.ts.map +1 -0
- package/dist/server/multipart.d.ts +2 -1
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/node.d.ts +9 -0
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +52 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/rate-limit.d.ts +7 -2
- package/dist/server/rate-limit.d.ts.map +1 -1
- package/dist/server/request.d.ts +27 -4
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/router.d.ts +4 -2
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +60 -15
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/server/stream.d.ts +3 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/types.d.ts +17 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/websocket.d.ts +73 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/tools/cli-args.d.ts +68 -0
- package/dist/tools/cli-args.d.ts.map +1 -0
- package/dist/tools/cli-format.d.ts +39 -0
- package/dist/tools/cli-format.d.ts.map +1 -0
- package/dist/tools/cli-wait.d.ts +45 -0
- package/dist/tools/cli-wait.d.ts.map +1 -0
- package/dist/tools/cli.d.ts +77 -0
- package/dist/tools/cli.d.ts.map +1 -0
- package/dist/tools/coerce.d.ts +8 -4
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +1 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts +29 -4
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/manifest.d.ts +4 -0
- package/dist/tools/manifest.d.ts.map +1 -1
- package/dist/tools/mcp-app.d.ts +59 -0
- package/dist/tools/mcp-app.d.ts.map +1 -0
- package/dist/tools/mcp-handler.d.ts +18 -0
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +9 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +30 -0
- package/dist/tools/mount-download.d.ts.map +1 -0
- package/dist/tools/mount-upload.d.ts +19 -0
- package/dist/tools/mount-upload.d.ts.map +1 -0
- package/dist/tools/mount-wait.d.ts +36 -0
- package/dist/tools/mount-wait.d.ts.map +1 -0
- package/dist/tools/mount.d.ts +8 -4
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/native-result.d.ts +17 -0
- package/dist/tools/native-result.d.ts.map +1 -0
- package/dist/tools/oauth-metadata.d.ts +28 -0
- package/dist/tools/oauth-metadata.d.ts.map +1 -0
- package/dist/tools/oauth-provider.d.ts +85 -0
- package/dist/tools/oauth-provider.d.ts.map +1 -0
- package/dist/tools/remote.d.ts +2 -2
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/toolkit.d.ts +51 -0
- package/dist/tools/toolkit.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +12 -0
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools/wait-core.d.ts +33 -0
- package/dist/tools/wait-core.d.ts.map +1 -0
- package/dist/tools.d.ts +13 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +637 -424
- package/package.json +22 -9
- package/dist/index-ke4mx4ea.js +0 -38
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
import {
|
|
2
|
+
jsonSchemaFields,
|
|
3
|
+
toJsonSchema
|
|
4
|
+
} from "./index-0ed3bx43.js";
|
|
5
|
+
import {
|
|
6
|
+
isWithinDir
|
|
7
|
+
} from "./index-x3fcszf8.js";
|
|
8
|
+
import {
|
|
9
|
+
formatZodError,
|
|
10
|
+
normalizeError,
|
|
11
|
+
validateHandlerOutput
|
|
12
|
+
} from "./index-vhdvv00d.js";
|
|
13
|
+
import {
|
|
14
|
+
isUnsafeKey,
|
|
15
|
+
safeJsonParse
|
|
16
|
+
} from "./index-kzfs85xp.js";
|
|
17
|
+
import {
|
|
18
|
+
isRecord
|
|
19
|
+
} from "./index-809wc1tt.js";
|
|
20
|
+
|
|
21
|
+
// src/tools/coerce.ts
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
function needsJsonCoercion(field) {
|
|
24
|
+
if (field instanceof z.ZodArray || field instanceof z.ZodObject || field instanceof z.ZodRecord) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (field instanceof z.ZodOptional || field instanceof z.ZodNullable || field instanceof z.ZodDefault) {
|
|
28
|
+
return needsJsonCoercion(field.unwrap());
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function coerceJsonArgs(args, schema) {
|
|
33
|
+
if (!(schema instanceof z.ZodObject))
|
|
34
|
+
return args;
|
|
35
|
+
const shape = schema.shape;
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [key, value] of Object.entries(args)) {
|
|
38
|
+
const field = shape[key];
|
|
39
|
+
if (field && needsJsonCoercion(field) && typeof value === "string") {
|
|
40
|
+
try {
|
|
41
|
+
out[key] = safeJsonParse(value);
|
|
42
|
+
continue;
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
out[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/tools/flatten.ts
|
|
51
|
+
import { z as z2 } from "zod";
|
|
52
|
+
function flattenDiscriminatedUnion(union) {
|
|
53
|
+
const discriminator = union.def.discriminator;
|
|
54
|
+
const literalValues = [];
|
|
55
|
+
const fieldToVariants = new Map;
|
|
56
|
+
const fieldSchemas = {};
|
|
57
|
+
for (const opt of union.def.options) {
|
|
58
|
+
if (!(opt instanceof z2.ZodObject)) {
|
|
59
|
+
throw new Error(`flattenDiscriminatedUnion: variant for discriminator '${discriminator}' is not a ZodObject`);
|
|
60
|
+
}
|
|
61
|
+
const discField = opt.shape[discriminator];
|
|
62
|
+
if (!(discField instanceof z2.ZodLiteral)) {
|
|
63
|
+
throw new Error(`flattenDiscriminatedUnion: discriminator field '${discriminator}' must be z.literal()`);
|
|
64
|
+
}
|
|
65
|
+
const [literalValue] = discField.def.values;
|
|
66
|
+
if (typeof literalValue !== "string") {
|
|
67
|
+
throw new Error("flattenDiscriminatedUnion: discriminator literal must be a string");
|
|
68
|
+
}
|
|
69
|
+
literalValues.push(literalValue);
|
|
70
|
+
for (const [key, field] of Object.entries(opt.shape)) {
|
|
71
|
+
if (key === discriminator)
|
|
72
|
+
continue;
|
|
73
|
+
const variants = fieldToVariants.get(key) ?? [];
|
|
74
|
+
variants.push(literalValue);
|
|
75
|
+
fieldToVariants.set(key, variants);
|
|
76
|
+
if (!(key in fieldSchemas)) {
|
|
77
|
+
fieldSchemas[key] = field instanceof z2.ZodOptional ? field : z2.optional(field);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const [firstLiteral, ...restLiterals] = literalValues;
|
|
82
|
+
if (!firstLiteral) {
|
|
83
|
+
throw new Error("flattenDiscriminatedUnion: union has no options");
|
|
84
|
+
}
|
|
85
|
+
const shape = {
|
|
86
|
+
[discriminator]: z2.enum([firstLiteral, ...restLiterals])
|
|
87
|
+
};
|
|
88
|
+
for (const [key, field] of Object.entries(fieldSchemas)) {
|
|
89
|
+
const variants = fieldToVariants.get(key) ?? [];
|
|
90
|
+
const hint = `Required if ${discriminator} = ${variants.join(" | ")}`;
|
|
91
|
+
shape[key] = field.describe(hint);
|
|
92
|
+
}
|
|
93
|
+
return z2.object(shape);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/tools/mount.ts
|
|
97
|
+
import { z as z4 } from "zod";
|
|
98
|
+
|
|
99
|
+
// src/tools/schema.ts
|
|
100
|
+
import { z as z3 } from "zod";
|
|
101
|
+
function objectShapeKeys(schema) {
|
|
102
|
+
return schema instanceof z3.ZodObject ? Object.keys(schema.shape) : [];
|
|
103
|
+
}
|
|
104
|
+
function mergeSchemas(paramsSchema, inputSchema) {
|
|
105
|
+
if (paramsSchema && !(paramsSchema instanceof z3.ZodObject)) {
|
|
106
|
+
throw new Error("Tool params schema must be a z.object()");
|
|
107
|
+
}
|
|
108
|
+
const paramsObject = paramsSchema instanceof z3.ZodObject ? paramsSchema : undefined;
|
|
109
|
+
if (!inputSchema) {
|
|
110
|
+
return paramsObject ?? z3.object({});
|
|
111
|
+
}
|
|
112
|
+
if (inputSchema instanceof z3.ZodObject) {
|
|
113
|
+
if (paramsObject) {
|
|
114
|
+
const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
|
|
115
|
+
if (conflicts.length > 0) {
|
|
116
|
+
throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return z3.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
|
|
120
|
+
}
|
|
121
|
+
return paramsObject ? z3.intersection(paramsObject, inputSchema) : inputSchema;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/tools/execute.ts
|
|
125
|
+
function toolResultFromError(err) {
|
|
126
|
+
const appErr = normalizeError(err);
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
code: appErr.code,
|
|
130
|
+
details: appErr.details ?? { message: appErr.message },
|
|
131
|
+
...appErr.hint && { hint: appErr.hint }
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle, coerceJson = false) {
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
const finish = async (result) => {
|
|
137
|
+
await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
|
|
138
|
+
return result;
|
|
139
|
+
};
|
|
140
|
+
if (hooks?.beforeToolCall) {
|
|
141
|
+
try {
|
|
142
|
+
await hooks.beforeToolCall(toolName, rawArgs, context);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return finish(toolResultFromError(err));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const paramKeys = new Set(objectShapeKeys(method.paramsSchema));
|
|
148
|
+
let paramArgs = {};
|
|
149
|
+
let inputArgs = {};
|
|
150
|
+
for (const [key, value] of Object.entries(rawArgs)) {
|
|
151
|
+
if (isUnsafeKey(key))
|
|
152
|
+
continue;
|
|
153
|
+
if (paramKeys.has(key))
|
|
154
|
+
paramArgs[key] = value;
|
|
155
|
+
else
|
|
156
|
+
inputArgs[key] = value;
|
|
157
|
+
}
|
|
158
|
+
if (coerceJson) {
|
|
159
|
+
paramArgs = coerceJsonArgs(paramArgs, method.paramsSchema);
|
|
160
|
+
inputArgs = coerceJsonArgs(inputArgs, method.inputSchema);
|
|
161
|
+
}
|
|
162
|
+
let params;
|
|
163
|
+
if (method.paramsSchema) {
|
|
164
|
+
const result = method.paramsSchema.safeParse(paramArgs);
|
|
165
|
+
if (!result.success) {
|
|
166
|
+
return finish({
|
|
167
|
+
ok: false,
|
|
168
|
+
code: "VALIDATION_ERROR",
|
|
169
|
+
details: { message: `Invalid params: ${formatZodError(result.error)}` }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
params = result.data;
|
|
173
|
+
}
|
|
174
|
+
let input;
|
|
175
|
+
if (method.inputSchema) {
|
|
176
|
+
const result = method.inputSchema.safeParse(inputArgs);
|
|
177
|
+
if (!result.success) {
|
|
178
|
+
return finish({
|
|
179
|
+
ok: false,
|
|
180
|
+
code: "VALIDATION_ERROR",
|
|
181
|
+
details: { message: `Invalid input: ${formatZodError(result.error)}` }
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
input = result.data;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const ctx = { ...context, params, input, source: context.source };
|
|
188
|
+
if (lifecycle?.beforeHandle) {
|
|
189
|
+
await lifecycle.beforeHandle(ctx, method);
|
|
190
|
+
}
|
|
191
|
+
let data = await method.handler(ctx);
|
|
192
|
+
if (lifecycle?.afterHandle) {
|
|
193
|
+
const transformed = await lifecycle.afterHandle(ctx, data, method);
|
|
194
|
+
if (transformed !== undefined)
|
|
195
|
+
data = transformed;
|
|
196
|
+
}
|
|
197
|
+
if (method.outputSchema) {
|
|
198
|
+
const checked = validateHandlerOutput(method.outputSchema, data);
|
|
199
|
+
if (!checked.ok) {
|
|
200
|
+
return finish({
|
|
201
|
+
ok: false,
|
|
202
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
203
|
+
details: { message: checked.message }
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
data = checked.data;
|
|
207
|
+
}
|
|
208
|
+
const output = (data === undefined || data === null) && !method.outputSchema ? { status: "ok" } : data;
|
|
209
|
+
return finish({ ok: true, data: output });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return finish(toolResultFromError(err));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/tools/names.ts
|
|
216
|
+
var SINGULAR_EXCEPTIONS = new Set([
|
|
217
|
+
"analytics",
|
|
218
|
+
"status",
|
|
219
|
+
"stats",
|
|
220
|
+
"settings",
|
|
221
|
+
"media",
|
|
222
|
+
"progress",
|
|
223
|
+
"news"
|
|
224
|
+
]);
|
|
225
|
+
function singularize(name) {
|
|
226
|
+
if (SINGULAR_EXCEPTIONS.has(name))
|
|
227
|
+
return name;
|
|
228
|
+
if (name.endsWith("ies"))
|
|
229
|
+
return `${name.slice(0, -3)}y`;
|
|
230
|
+
if (name.endsWith("s") && !name.endsWith("ss"))
|
|
231
|
+
return name.slice(0, -1);
|
|
232
|
+
return name;
|
|
233
|
+
}
|
|
234
|
+
function toToolName(serviceName, methodName) {
|
|
235
|
+
const normalized = serviceName.replace(/-/g, "_");
|
|
236
|
+
const singular = singularize(normalized);
|
|
237
|
+
if (methodName === "list")
|
|
238
|
+
return `list_${normalized}`;
|
|
239
|
+
if (methodName === "get")
|
|
240
|
+
return `get_${singular}`;
|
|
241
|
+
if (methodName === "create")
|
|
242
|
+
return `create_${singular}`;
|
|
243
|
+
if (methodName === "update")
|
|
244
|
+
return `update_${singular}`;
|
|
245
|
+
if (methodName === "delete")
|
|
246
|
+
return `delete_${singular}`;
|
|
247
|
+
const snake = methodName.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
248
|
+
return `${snake}_${singular}`.replace(/^_/, "");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/tools/mount.ts
|
|
252
|
+
function applyExtend(base, extra) {
|
|
253
|
+
if (base instanceof z4.ZodObject) {
|
|
254
|
+
const conflicts = Object.keys(extra).filter((key) => (key in base.shape));
|
|
255
|
+
if (conflicts.length > 0) {
|
|
256
|
+
throw new Error(`Tool extend conflict: ${conflicts.join(", ")} already declared by the contract`);
|
|
257
|
+
}
|
|
258
|
+
return z4.object({ ...extra, ...base.shape });
|
|
259
|
+
}
|
|
260
|
+
return z4.intersection(z4.object(extra), base);
|
|
261
|
+
}
|
|
262
|
+
function collectTools(service, transport, config = {}) {
|
|
263
|
+
const { extend, flattenUnionInput = false } = config;
|
|
264
|
+
const tools = [];
|
|
265
|
+
for (const [methodName, method] of Object.entries(service.methods)) {
|
|
266
|
+
if (transport === "CLI") {
|
|
267
|
+
if (!method.expose?.includes("CLI"))
|
|
268
|
+
continue;
|
|
269
|
+
} else if (method.expose && !method.expose.includes(transport)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (method.multipart)
|
|
273
|
+
continue;
|
|
274
|
+
const name = method.toolName ?? toToolName(service.name, methodName);
|
|
275
|
+
let baseSchema = mergeSchemas(method.paramsSchema, method.inputSchema);
|
|
276
|
+
if (flattenUnionInput && baseSchema instanceof z4.ZodDiscriminatedUnion) {
|
|
277
|
+
baseSchema = flattenDiscriminatedUnion(baseSchema);
|
|
278
|
+
}
|
|
279
|
+
const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
|
|
280
|
+
const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
|
|
281
|
+
tools.push({ method, name, schema, shouldExtend });
|
|
282
|
+
}
|
|
283
|
+
return tools;
|
|
284
|
+
}
|
|
285
|
+
function createToolRunner(config) {
|
|
286
|
+
const extendKeys = config.extend ? new Set(Object.keys(config.extend.schema)) : null;
|
|
287
|
+
return async (tool, rawArgs) => {
|
|
288
|
+
let extraContext = {};
|
|
289
|
+
if (tool.shouldExtend && config.extend) {
|
|
290
|
+
extraContext = await config.extend.resolve(rawArgs);
|
|
291
|
+
}
|
|
292
|
+
const cleanArgs = extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
|
|
293
|
+
return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function formatToolError(result, toolName, errorHint) {
|
|
297
|
+
const err = { error: result.code };
|
|
298
|
+
if (result.details)
|
|
299
|
+
err.details = result.details;
|
|
300
|
+
const hints = [];
|
|
301
|
+
if (result.hint)
|
|
302
|
+
hints.push(result.hint);
|
|
303
|
+
if (errorHint && toolName) {
|
|
304
|
+
const global = errorHint(toolName, result.code);
|
|
305
|
+
if (global)
|
|
306
|
+
hints.push(global);
|
|
307
|
+
}
|
|
308
|
+
if (hints.length > 0)
|
|
309
|
+
err._hint = hints.join(" ");
|
|
310
|
+
return err;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/tools/cli-args.ts
|
|
314
|
+
import { z as z5 } from "zod";
|
|
315
|
+
var BOOL_OPTIONS = new Set(["json", "wait", "quiet", "dry-run", "help"]);
|
|
316
|
+
var VALUE_OPTIONS = new Set(["wait-timeout", "output-dir"]);
|
|
317
|
+
function unwrap(field) {
|
|
318
|
+
if (field instanceof z5.ZodOptional || field instanceof z5.ZodNullable || field instanceof z5.ZodDefault) {
|
|
319
|
+
return unwrap(field.unwrap());
|
|
320
|
+
}
|
|
321
|
+
return field;
|
|
322
|
+
}
|
|
323
|
+
function classify(field) {
|
|
324
|
+
if (field instanceof z5.ZodBoolean)
|
|
325
|
+
return "boolean";
|
|
326
|
+
if (field instanceof z5.ZodNumber)
|
|
327
|
+
return "number";
|
|
328
|
+
if (field instanceof z5.ZodBigInt)
|
|
329
|
+
return "bigint";
|
|
330
|
+
if (field instanceof z5.ZodDate)
|
|
331
|
+
return "date";
|
|
332
|
+
if (field instanceof z5.ZodEnum)
|
|
333
|
+
return "enum";
|
|
334
|
+
if (field instanceof z5.ZodArray)
|
|
335
|
+
return "array";
|
|
336
|
+
if (field instanceof z5.ZodObject)
|
|
337
|
+
return "object";
|
|
338
|
+
if (field instanceof z5.ZodString || field instanceof z5.ZodLiteral)
|
|
339
|
+
return "string";
|
|
340
|
+
return "other";
|
|
341
|
+
}
|
|
342
|
+
function describeSchemaFields(schema) {
|
|
343
|
+
const fields = new Map;
|
|
344
|
+
if (!(schema instanceof z5.ZodObject))
|
|
345
|
+
return fields;
|
|
346
|
+
for (const [name, raw] of Object.entries(schema.shape)) {
|
|
347
|
+
const base = unwrap(raw);
|
|
348
|
+
const kind = classify(base);
|
|
349
|
+
if (kind === "array" && base instanceof z5.ZodArray) {
|
|
350
|
+
fields.set(name, { kind, elementKind: classify(unwrap(base.element)) });
|
|
351
|
+
} else {
|
|
352
|
+
fields.set(name, { kind });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return fields;
|
|
356
|
+
}
|
|
357
|
+
function parseBool(value) {
|
|
358
|
+
const v = value.toLowerCase();
|
|
359
|
+
if (v === "false" || v === "0" || v === "no" || v === "off")
|
|
360
|
+
return false;
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
function coerceScalar(kind, value) {
|
|
364
|
+
switch (kind) {
|
|
365
|
+
case "boolean":
|
|
366
|
+
return parseBool(value);
|
|
367
|
+
case "number": {
|
|
368
|
+
const n = Number(value);
|
|
369
|
+
return value.trim() !== "" && !Number.isNaN(n) ? n : value;
|
|
370
|
+
}
|
|
371
|
+
case "bigint":
|
|
372
|
+
try {
|
|
373
|
+
return BigInt(value);
|
|
374
|
+
} catch {
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
case "date": {
|
|
378
|
+
const d = new Date(value);
|
|
379
|
+
return Number.isNaN(d.getTime()) ? value : d;
|
|
380
|
+
}
|
|
381
|
+
default:
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function looseCoerce(value) {
|
|
386
|
+
if (value === "true")
|
|
387
|
+
return true;
|
|
388
|
+
if (value === "false")
|
|
389
|
+
return false;
|
|
390
|
+
const n = Number(value);
|
|
391
|
+
return value.trim() !== "" && !Number.isNaN(n) ? n : value;
|
|
392
|
+
}
|
|
393
|
+
function looksLikeJson(value) {
|
|
394
|
+
const t = value.trim();
|
|
395
|
+
return t.startsWith("[") || t.startsWith("{");
|
|
396
|
+
}
|
|
397
|
+
function coerceField(info, values) {
|
|
398
|
+
const last = values[values.length - 1] ?? "";
|
|
399
|
+
if (!info)
|
|
400
|
+
return values.length > 1 ? values : last;
|
|
401
|
+
if (info.kind === "array") {
|
|
402
|
+
if (values.length === 1 && looksLikeJson(last))
|
|
403
|
+
return last;
|
|
404
|
+
return values.map((v) => coerceScalar(info.elementKind ?? "string", v));
|
|
405
|
+
}
|
|
406
|
+
if (info.kind === "object")
|
|
407
|
+
return last;
|
|
408
|
+
return coerceScalar(info.kind, last);
|
|
409
|
+
}
|
|
410
|
+
function setNested(target, path, value) {
|
|
411
|
+
if (path.some(isUnsafeKey))
|
|
412
|
+
return;
|
|
413
|
+
let node = target;
|
|
414
|
+
for (let i = 0;i < path.length - 1; i++) {
|
|
415
|
+
const key = path[i];
|
|
416
|
+
if (key === undefined)
|
|
417
|
+
return;
|
|
418
|
+
const next = node[key];
|
|
419
|
+
if (isRecord(next)) {
|
|
420
|
+
node = next;
|
|
421
|
+
} else {
|
|
422
|
+
const created = {};
|
|
423
|
+
node[key] = created;
|
|
424
|
+
node = created;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const leaf = path[path.length - 1];
|
|
428
|
+
if (leaf !== undefined)
|
|
429
|
+
node[leaf] = value;
|
|
430
|
+
}
|
|
431
|
+
function parseCliArgs(argv, schema) {
|
|
432
|
+
const options = {
|
|
433
|
+
json: false,
|
|
434
|
+
wait: false,
|
|
435
|
+
quiet: false,
|
|
436
|
+
dryRun: false,
|
|
437
|
+
help: false
|
|
438
|
+
};
|
|
439
|
+
const rest = [];
|
|
440
|
+
for (let i = 0;i < argv.length; i++) {
|
|
441
|
+
const tok = argv[i];
|
|
442
|
+
if (tok === undefined)
|
|
443
|
+
continue;
|
|
444
|
+
if (tok === "-h") {
|
|
445
|
+
options.help = true;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (tok.startsWith("--")) {
|
|
449
|
+
const eq = tok.indexOf("=");
|
|
450
|
+
const name = eq >= 0 ? tok.slice(2, eq) : tok.slice(2);
|
|
451
|
+
const inline = eq >= 0 ? tok.slice(eq + 1) : undefined;
|
|
452
|
+
if (BOOL_OPTIONS.has(name)) {
|
|
453
|
+
const on = inline === undefined ? true : parseBool(inline);
|
|
454
|
+
if (name === "dry-run")
|
|
455
|
+
options.dryRun = on;
|
|
456
|
+
else if (name === "json")
|
|
457
|
+
options.json = on;
|
|
458
|
+
else if (name === "wait")
|
|
459
|
+
options.wait = on;
|
|
460
|
+
else if (name === "quiet")
|
|
461
|
+
options.quiet = on;
|
|
462
|
+
else if (name === "help")
|
|
463
|
+
options.help = on;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (VALUE_OPTIONS.has(name)) {
|
|
467
|
+
const value = inline ?? argv[++i];
|
|
468
|
+
if (value !== undefined) {
|
|
469
|
+
if (name === "wait-timeout") {
|
|
470
|
+
const n = Number(value);
|
|
471
|
+
if (!Number.isNaN(n))
|
|
472
|
+
options.waitTimeout = n;
|
|
473
|
+
} else if (name === "output-dir") {
|
|
474
|
+
options.outputDir = value;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
rest.push(tok);
|
|
481
|
+
}
|
|
482
|
+
const fields = describeSchemaFields(schema);
|
|
483
|
+
const flags = new Map;
|
|
484
|
+
const boolFlags = new Map;
|
|
485
|
+
const positionals = [];
|
|
486
|
+
const pushFlag = (name, value) => {
|
|
487
|
+
const existing = flags.get(name);
|
|
488
|
+
if (existing)
|
|
489
|
+
existing.push(value);
|
|
490
|
+
else
|
|
491
|
+
flags.set(name, [value]);
|
|
492
|
+
};
|
|
493
|
+
for (let i = 0;i < rest.length; i++) {
|
|
494
|
+
const tok = rest[i];
|
|
495
|
+
if (tok === undefined)
|
|
496
|
+
continue;
|
|
497
|
+
if (!tok.startsWith("--")) {
|
|
498
|
+
positionals.push(tok);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const eq = tok.indexOf("=");
|
|
502
|
+
const name = eq >= 0 ? tok.slice(2, eq) : tok.slice(2);
|
|
503
|
+
let value = eq >= 0 ? tok.slice(eq + 1) : undefined;
|
|
504
|
+
if (value === undefined && name.startsWith("no-") && fields.get(name.slice(3))?.kind === "boolean") {
|
|
505
|
+
boolFlags.set(name.slice(3), false);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const info = fields.get(name);
|
|
509
|
+
if (value === undefined) {
|
|
510
|
+
if (info?.kind === "boolean") {
|
|
511
|
+
boolFlags.set(name, true);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const next = rest[i + 1];
|
|
515
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
516
|
+
value = next;
|
|
517
|
+
i++;
|
|
518
|
+
} else {
|
|
519
|
+
boolFlags.set(name, true);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
pushFlag(name, value);
|
|
524
|
+
}
|
|
525
|
+
const toolArgs = {};
|
|
526
|
+
const fillable = [...fields.entries()].filter(([, info]) => info.kind !== "boolean").map(([name]) => name);
|
|
527
|
+
let pi = 0;
|
|
528
|
+
for (const key of fillable) {
|
|
529
|
+
if (pi >= positionals.length)
|
|
530
|
+
break;
|
|
531
|
+
if (flags.has(key))
|
|
532
|
+
continue;
|
|
533
|
+
const value = positionals[pi++];
|
|
534
|
+
if (value !== undefined)
|
|
535
|
+
toolArgs[key] = coerceField(fields.get(key), [value]);
|
|
536
|
+
}
|
|
537
|
+
for (const [key, value] of boolFlags) {
|
|
538
|
+
if (isUnsafeKey(key))
|
|
539
|
+
continue;
|
|
540
|
+
toolArgs[key] = value;
|
|
541
|
+
}
|
|
542
|
+
for (const [key, values] of flags) {
|
|
543
|
+
if (key.includes(".")) {
|
|
544
|
+
setNested(toolArgs, key.split("."), looseCoerce(values[values.length - 1] ?? ""));
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (isUnsafeKey(key))
|
|
548
|
+
continue;
|
|
549
|
+
toolArgs[key] = coerceField(fields.get(key), values);
|
|
550
|
+
}
|
|
551
|
+
return { toolArgs, options };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/tools/cli-format.ts
|
|
555
|
+
var DEFAULT_EXIT_CODES = {
|
|
556
|
+
VALIDATION_ERROR: 1,
|
|
557
|
+
BAD_REQUEST: 1,
|
|
558
|
+
UNAUTHORIZED: 2,
|
|
559
|
+
FORBIDDEN: 3,
|
|
560
|
+
NOT_FOUND: 4,
|
|
561
|
+
CONFLICT: 5,
|
|
562
|
+
RATE_LIMITED: 6,
|
|
563
|
+
TIMEOUT: 7,
|
|
564
|
+
INTERNAL_SERVER_ERROR: 1
|
|
565
|
+
};
|
|
566
|
+
function emitResult(result, writers, opts) {
|
|
567
|
+
if (result.ok) {
|
|
568
|
+
if (result.data === undefined)
|
|
569
|
+
return 0;
|
|
570
|
+
const text = opts.json ? JSON.stringify(result.data) : JSON.stringify(result.data, null, 2);
|
|
571
|
+
writers.stdout(`${text}
|
|
572
|
+
`);
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
const error = formatToolError(result, opts.toolName, opts.errorHint);
|
|
576
|
+
writers.stderr(`${JSON.stringify(error, null, 2)}
|
|
577
|
+
`);
|
|
578
|
+
const codes = opts.exitCodes ?? DEFAULT_EXIT_CODES;
|
|
579
|
+
return codes[result.code] ?? 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/tools/wait-core.ts
|
|
583
|
+
var DEFAULT_BACKOFF = [2, 3, 5, 5, 8, 10];
|
|
584
|
+
var DEFAULT_TIMEOUT = 600;
|
|
585
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
586
|
+
async function pollUntil(params) {
|
|
587
|
+
const backoff = params.backoff?.length ? params.backoff : DEFAULT_BACKOFF;
|
|
588
|
+
const lastBackoff = backoff[backoff.length - 1] ?? 5;
|
|
589
|
+
const timeoutSec = params.timeoutSec ?? DEFAULT_TIMEOUT;
|
|
590
|
+
const sleep = params.sleepFn ?? defaultSleep;
|
|
591
|
+
const startedAt = Date.now();
|
|
592
|
+
for (let attempt = 0;; attempt++) {
|
|
593
|
+
const state = await params.poll();
|
|
594
|
+
if (params.done(state))
|
|
595
|
+
return { state, timedOut: false };
|
|
596
|
+
const elapsedSec = (Date.now() - startedAt) / 1000;
|
|
597
|
+
params.onTick?.(attempt + 1, elapsedSec);
|
|
598
|
+
if (elapsedSec >= timeoutSec)
|
|
599
|
+
return { state, timedOut: true };
|
|
600
|
+
const waitSec = backoff[Math.min(attempt, backoff.length - 1)] ?? lastBackoff;
|
|
601
|
+
await sleep(waitSec * 1000);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/tools/cli-wait.ts
|
|
606
|
+
var DEFAULT_TIMEOUT2 = 600;
|
|
607
|
+
async function pollUntilDone(params) {
|
|
608
|
+
const { initial, wait, call } = params;
|
|
609
|
+
if (!initial.ok)
|
|
610
|
+
return initial;
|
|
611
|
+
if (wait.done(initial.data))
|
|
612
|
+
return initial;
|
|
613
|
+
const pollArgs = wait.poll(initial.data);
|
|
614
|
+
if (!pollArgs)
|
|
615
|
+
return initial;
|
|
616
|
+
const timeoutSec = params.timeoutSec ?? wait.timeout ?? DEFAULT_TIMEOUT2;
|
|
617
|
+
const { state, timedOut } = await pollUntil({
|
|
618
|
+
poll: () => call(wait.tool, pollArgs),
|
|
619
|
+
done: (result) => !result.ok || wait.done(result.data),
|
|
620
|
+
backoff: wait.backoff,
|
|
621
|
+
timeoutSec,
|
|
622
|
+
sleepFn: params.sleepFn,
|
|
623
|
+
onTick: params.onTick
|
|
624
|
+
});
|
|
625
|
+
if (timedOut) {
|
|
626
|
+
return {
|
|
627
|
+
ok: false,
|
|
628
|
+
code: "TIMEOUT",
|
|
629
|
+
details: { message: `Timed out after ${timeoutSec}s waiting for "${wait.tool}"` }
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return state;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/tools/cli.ts
|
|
636
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
637
|
+
import { basename, resolve } from "node:path";
|
|
638
|
+
import process from "node:process";
|
|
639
|
+
|
|
640
|
+
// src/internal/secure-fetch.ts
|
|
641
|
+
import { lookup } from "node:dns/promises";
|
|
642
|
+
import { isIP } from "node:net";
|
|
643
|
+
var NUMERIC_HOST = /^(0x[0-9a-f]+|[0-9.]+)$/i;
|
|
644
|
+
var MAX_REDIRECTS = 5;
|
|
645
|
+
function assertHttpUrl(url) {
|
|
646
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
647
|
+
throw new Error(`refusing to fetch a non-http(s) URL (${url.protocol})`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function mappedIpv4FromIpv6(ip) {
|
|
651
|
+
const rest = ip.startsWith("::ffff:") ? ip.slice("::ffff:".length) : ip.startsWith("0:0:0:0:0:ffff:") ? ip.slice("0:0:0:0:0:ffff:".length) : null;
|
|
652
|
+
if (rest === null)
|
|
653
|
+
return null;
|
|
654
|
+
if (isIP(rest) === 4)
|
|
655
|
+
return rest;
|
|
656
|
+
const [hiRaw, loRaw, extra] = rest.split(":");
|
|
657
|
+
if (hiRaw === undefined || loRaw === undefined || extra !== undefined)
|
|
658
|
+
return null;
|
|
659
|
+
if (!/^[0-9a-f]{1,4}$/i.test(hiRaw) || !/^[0-9a-f]{1,4}$/i.test(loRaw))
|
|
660
|
+
return null;
|
|
661
|
+
const hi = Number.parseInt(hiRaw, 16);
|
|
662
|
+
const lo = Number.parseInt(loRaw, 16);
|
|
663
|
+
return [hi >> 8, hi & 255, lo >> 8, lo & 255].join(".");
|
|
664
|
+
}
|
|
665
|
+
function isPrivateIp(ip) {
|
|
666
|
+
const normalized = ip.replace(/^\[|\]$/g, "");
|
|
667
|
+
const family = isIP(normalized);
|
|
668
|
+
if (family === 4) {
|
|
669
|
+
const [a, b] = normalized.split(".").map(Number);
|
|
670
|
+
if (a === undefined || b === undefined)
|
|
671
|
+
return true;
|
|
672
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
|
|
673
|
+
}
|
|
674
|
+
if (family === 6) {
|
|
675
|
+
const lower = normalized.toLowerCase();
|
|
676
|
+
const mapped = mappedIpv4FromIpv6(lower);
|
|
677
|
+
if (mapped)
|
|
678
|
+
return isPrivateIp(mapped);
|
|
679
|
+
if (lower === "::1" || lower === "::")
|
|
680
|
+
return true;
|
|
681
|
+
if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb") || lower.startsWith("fc") || lower.startsWith("fd")) {
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
async function assertPublicUrl(url) {
|
|
689
|
+
assertHttpUrl(url);
|
|
690
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
691
|
+
if (!host)
|
|
692
|
+
throw new Error("refusing to fetch a URL with no host");
|
|
693
|
+
if (isIP(host)) {
|
|
694
|
+
if (isPrivateIp(host))
|
|
695
|
+
throw new Error("refusing to fetch a private address");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (NUMERIC_HOST.test(host)) {
|
|
699
|
+
throw new Error("refusing to fetch a non-canonical numeric host");
|
|
700
|
+
}
|
|
701
|
+
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
702
|
+
throw new Error("refusing to fetch an internal host");
|
|
703
|
+
}
|
|
704
|
+
const records = await lookup(host, { all: true });
|
|
705
|
+
for (const record of records) {
|
|
706
|
+
if (isPrivateIp(record.address)) {
|
|
707
|
+
throw new Error("refusing to fetch a host that resolves to a private address");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function fetchGuarded(start, allowPrivate) {
|
|
712
|
+
let url = start;
|
|
713
|
+
for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
|
|
714
|
+
if (allowPrivate)
|
|
715
|
+
assertHttpUrl(url);
|
|
716
|
+
else
|
|
717
|
+
await assertPublicUrl(url);
|
|
718
|
+
const res = await fetch(url, { redirect: "manual" });
|
|
719
|
+
if (res.status < 300 || res.status >= 400)
|
|
720
|
+
return res;
|
|
721
|
+
const location = res.headers.get("location");
|
|
722
|
+
if (!location)
|
|
723
|
+
return res;
|
|
724
|
+
await res.body?.cancel();
|
|
725
|
+
url = new URL(location, url);
|
|
726
|
+
}
|
|
727
|
+
throw new Error("too many redirects");
|
|
728
|
+
}
|
|
729
|
+
async function readCapped(res, max) {
|
|
730
|
+
const reader = res.body?.getReader();
|
|
731
|
+
if (!reader)
|
|
732
|
+
return Buffer.alloc(0);
|
|
733
|
+
const chunks = [];
|
|
734
|
+
let total = 0;
|
|
735
|
+
try {
|
|
736
|
+
while (true) {
|
|
737
|
+
const { done, value } = await reader.read();
|
|
738
|
+
if (done)
|
|
739
|
+
break;
|
|
740
|
+
total += value.length;
|
|
741
|
+
if (total > max) {
|
|
742
|
+
await reader.cancel();
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
chunks.push(value);
|
|
746
|
+
}
|
|
747
|
+
} finally {
|
|
748
|
+
reader.releaseLock();
|
|
749
|
+
}
|
|
750
|
+
return Buffer.concat(chunks);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/tools/cli.ts
|
|
754
|
+
var GLOBAL_OPTIONS = [
|
|
755
|
+
["--json", "Emit raw JSON on stdout (for piping / scripts)"],
|
|
756
|
+
["--wait", "Block-poll an async result to a terminal state"],
|
|
757
|
+
["--wait-timeout <s>", "Override the --wait timeout in seconds"],
|
|
758
|
+
["--output-dir <dir>", "Download result media into a directory"],
|
|
759
|
+
["--quiet", "Suppress non-essential stderr output"],
|
|
760
|
+
["--dry-run", "Print the resolved call without executing it"],
|
|
761
|
+
["--help, -h", "Show help for a command"]
|
|
762
|
+
];
|
|
763
|
+
async function readPipedStdin() {
|
|
764
|
+
if (process.stdin.isTTY)
|
|
765
|
+
return null;
|
|
766
|
+
const chunks = [];
|
|
767
|
+
for await (const chunk of process.stdin) {
|
|
768
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
769
|
+
}
|
|
770
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
771
|
+
return text.length > 0 ? text : null;
|
|
772
|
+
}
|
|
773
|
+
function typeLabel(schema) {
|
|
774
|
+
if (Array.isArray(schema.enum))
|
|
775
|
+
return schema.enum.join("|");
|
|
776
|
+
if (schema.type === "array")
|
|
777
|
+
return "value…";
|
|
778
|
+
if (typeof schema.type === "string")
|
|
779
|
+
return schema.type;
|
|
780
|
+
return "value";
|
|
781
|
+
}
|
|
782
|
+
function padRight(text, width) {
|
|
783
|
+
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
784
|
+
}
|
|
785
|
+
function summarize(desc) {
|
|
786
|
+
const firstLine = desc.split(`
|
|
787
|
+
`)[0]?.trim() ?? "";
|
|
788
|
+
const firstSentence = firstLine.split(". ")[0]?.trim() ?? firstLine;
|
|
789
|
+
const max = 72;
|
|
790
|
+
return firstSentence.length > max ? `${firstSentence.slice(0, max - 1).trimEnd()}…` : firstSentence;
|
|
791
|
+
}
|
|
792
|
+
function looseCoerceValue(value) {
|
|
793
|
+
if (Array.isArray(value))
|
|
794
|
+
return value.map(looseCoerceValue);
|
|
795
|
+
if (typeof value !== "string")
|
|
796
|
+
return value;
|
|
797
|
+
if (value === "true")
|
|
798
|
+
return true;
|
|
799
|
+
if (value === "false")
|
|
800
|
+
return false;
|
|
801
|
+
const n = Number(value);
|
|
802
|
+
return value.trim() !== "" && !Number.isNaN(n) ? n : value;
|
|
803
|
+
}
|
|
804
|
+
function passthroughBase(value) {
|
|
805
|
+
if (isRecord(value))
|
|
806
|
+
return value;
|
|
807
|
+
if (typeof value === "string") {
|
|
808
|
+
try {
|
|
809
|
+
const parsed = safeJsonParse(value);
|
|
810
|
+
if (isRecord(parsed))
|
|
811
|
+
return parsed;
|
|
812
|
+
} catch {}
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
function collectPassthrough(toolArgs, field, knownKeys) {
|
|
817
|
+
const known = new Set(knownKeys);
|
|
818
|
+
const bag = {};
|
|
819
|
+
for (const key of Object.keys(toolArgs)) {
|
|
820
|
+
if (key === field || known.has(key))
|
|
821
|
+
continue;
|
|
822
|
+
bag[key] = looseCoerceValue(toolArgs[key]);
|
|
823
|
+
delete toolArgs[key];
|
|
824
|
+
}
|
|
825
|
+
if (Object.keys(bag).length === 0)
|
|
826
|
+
return;
|
|
827
|
+
const base = passthroughBase(toolArgs[field]);
|
|
828
|
+
toolArgs[field] = base ? { ...base, ...bag } : bag;
|
|
829
|
+
}
|
|
830
|
+
function renderTopHelp(name, version, tools) {
|
|
831
|
+
const lines = [
|
|
832
|
+
`${name} ${version}`,
|
|
833
|
+
"",
|
|
834
|
+
`Usage: ${name} <command> [args] [--flags]`,
|
|
835
|
+
"",
|
|
836
|
+
"Commands:"
|
|
837
|
+
];
|
|
838
|
+
const width = Math.max(0, ...[...tools.keys()].map((k) => k.length));
|
|
839
|
+
for (const [command, tool] of tools) {
|
|
840
|
+
lines.push(` ${padRight(command, width)} ${summarize(tool.method.desc)}`);
|
|
841
|
+
}
|
|
842
|
+
lines.push("", "Global options:");
|
|
843
|
+
const optWidth = Math.max(...GLOBAL_OPTIONS.map(([flag]) => flag.length));
|
|
844
|
+
for (const [flag, desc] of GLOBAL_OPTIONS)
|
|
845
|
+
lines.push(` ${padRight(flag, optWidth)} ${desc}`);
|
|
846
|
+
lines.push("", `Run "${name} <command> --help" for command-specific flags.`);
|
|
847
|
+
return `${lines.join(`
|
|
848
|
+
`)}
|
|
849
|
+
`;
|
|
850
|
+
}
|
|
851
|
+
function renderCommandHelp(name, command, tool) {
|
|
852
|
+
const lines = [tool.method.desc, "", `Usage: ${name} ${command} [args] [--flags]`, ""];
|
|
853
|
+
let fields = [];
|
|
854
|
+
try {
|
|
855
|
+
fields = jsonSchemaFields(toJsonSchema(tool.schema, "input"));
|
|
856
|
+
} catch {
|
|
857
|
+
lines.push("(arguments: pass a JSON object — this command has a complex schema)", "");
|
|
858
|
+
}
|
|
859
|
+
if (fields.length > 0) {
|
|
860
|
+
lines.push("Arguments:");
|
|
861
|
+
const width = Math.max(...fields.map((f) => f.name.length));
|
|
862
|
+
for (const f of fields) {
|
|
863
|
+
const req = f.required ? " (required)" : "";
|
|
864
|
+
const desc = f.description ? ` — ${f.description}` : "";
|
|
865
|
+
lines.push(` --${padRight(f.name, width)} <${typeLabel(f.schema)}>${req}${desc}`);
|
|
866
|
+
}
|
|
867
|
+
lines.push("");
|
|
868
|
+
}
|
|
869
|
+
return `${lines.join(`
|
|
870
|
+
`)}
|
|
871
|
+
`;
|
|
872
|
+
}
|
|
873
|
+
var DEFAULT_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
874
|
+
async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes) {
|
|
875
|
+
const root = resolve(dir);
|
|
876
|
+
await mkdir(root, { recursive: true });
|
|
877
|
+
for (const file of files) {
|
|
878
|
+
try {
|
|
879
|
+
const res = await fetchGuarded(new URL(file.url), allowPrivate);
|
|
880
|
+
if (!res.ok)
|
|
881
|
+
throw new Error(`HTTP ${res.status}`);
|
|
882
|
+
const buffer = await readCapped(res, maxBytes);
|
|
883
|
+
if (!buffer)
|
|
884
|
+
throw new Error(`file exceeds the ${maxBytes}-byte cap`);
|
|
885
|
+
const target = resolve(root, basename(file.name));
|
|
886
|
+
if (!isWithinDir(root, target))
|
|
887
|
+
throw new Error("download name escapes the output dir");
|
|
888
|
+
await writeFile(target, buffer);
|
|
889
|
+
if (!quiet)
|
|
890
|
+
stderr(`saved ${target} (${(buffer.length / 1024).toFixed(0)}KB)
|
|
891
|
+
`);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
stderr(`failed to download ${file.url}: ${err instanceof Error ? err.message : String(err)}
|
|
894
|
+
`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function createCli(config) {
|
|
899
|
+
const stdout = config.stdout ?? ((text) => void process.stdout.write(text));
|
|
900
|
+
const stderr = config.stderr ?? ((text) => void process.stderr.write(text));
|
|
901
|
+
const exit = config.exit ?? ((code) => void process.exit(code));
|
|
902
|
+
const argv = config.argv ?? process.argv.slice(2);
|
|
903
|
+
const readStdin = config.stdin ?? readPipedStdin;
|
|
904
|
+
const auth = config.auth === undefined ? undefined : await config.auth;
|
|
905
|
+
const services = typeof config.services === "function" ? config.services(auth) : config.services;
|
|
906
|
+
const context = config.context?.(auth);
|
|
907
|
+
const tools = new Map;
|
|
908
|
+
for (const service of services) {
|
|
909
|
+
for (const mountable of collectTools(service, "CLI")) {
|
|
910
|
+
if (tools.has(mountable.name)) {
|
|
911
|
+
throw new Error(`Duplicate CLI command "${mountable.name}" across mounted services`);
|
|
912
|
+
}
|
|
913
|
+
tools.set(mountable.name, mountable);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const runTool = createToolRunner({
|
|
917
|
+
source: "cli",
|
|
918
|
+
context,
|
|
919
|
+
hooks: config.hooks,
|
|
920
|
+
lifecycle: config.lifecycle,
|
|
921
|
+
errorHint: config.errorHint,
|
|
922
|
+
coerceJsonArgs: config.coerceJsonArgs
|
|
923
|
+
});
|
|
924
|
+
const [command, ...commandArgv] = argv;
|
|
925
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
926
|
+
stdout(renderTopHelp(config.name, config.version, tools));
|
|
927
|
+
return exit(0);
|
|
928
|
+
}
|
|
929
|
+
if (command === "--version" || command === "version") {
|
|
930
|
+
stdout(`${config.name} ${config.version}
|
|
931
|
+
`);
|
|
932
|
+
return exit(0);
|
|
933
|
+
}
|
|
934
|
+
const tool = tools.get(command);
|
|
935
|
+
if (!tool) {
|
|
936
|
+
stderr(`Unknown command "${command}". Run "${config.name} --help" for the command list.
|
|
937
|
+
`);
|
|
938
|
+
return exit(1);
|
|
939
|
+
}
|
|
940
|
+
const { toolArgs, options } = parseCliArgs(commandArgv, tool.schema);
|
|
941
|
+
if (options.help) {
|
|
942
|
+
stdout(renderCommandHelp(config.name, command, tool));
|
|
943
|
+
return exit(0);
|
|
944
|
+
}
|
|
945
|
+
const fields = jsonSchemaFields(safeInputSchema(tool));
|
|
946
|
+
const firstUnset = fields.find((f) => !(f.name in toolArgs) && f.schema.type !== "boolean");
|
|
947
|
+
if (firstUnset) {
|
|
948
|
+
const piped = await readStdin();
|
|
949
|
+
if (piped !== null)
|
|
950
|
+
toolArgs[firstUnset.name] = piped;
|
|
951
|
+
}
|
|
952
|
+
const passthroughField = config.passthrough?.[command];
|
|
953
|
+
if (passthroughField) {
|
|
954
|
+
collectPassthrough(toolArgs, passthroughField, objectShapeKeys(tool.schema));
|
|
955
|
+
}
|
|
956
|
+
if (options.dryRun) {
|
|
957
|
+
stdout(`${JSON.stringify({ command, args: toolArgs }, null, 2)}
|
|
958
|
+
`);
|
|
959
|
+
return exit(0);
|
|
960
|
+
}
|
|
961
|
+
let result;
|
|
962
|
+
try {
|
|
963
|
+
result = await runTool(tool, toolArgs);
|
|
964
|
+
} catch (err) {
|
|
965
|
+
result = toolResultFromError(err);
|
|
966
|
+
}
|
|
967
|
+
const waitConfig = config.wait?.[command];
|
|
968
|
+
if (options.wait && waitConfig) {
|
|
969
|
+
result = await pollUntilDone({
|
|
970
|
+
initial: result,
|
|
971
|
+
wait: waitConfig,
|
|
972
|
+
call: async (toolName, args) => {
|
|
973
|
+
const pollTool = tools.get(toolName);
|
|
974
|
+
if (!pollTool) {
|
|
975
|
+
return {
|
|
976
|
+
ok: false,
|
|
977
|
+
code: "NOT_FOUND",
|
|
978
|
+
details: { message: `--wait: no command "${toolName}" to poll` }
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
return runTool(pollTool, args);
|
|
982
|
+
},
|
|
983
|
+
timeoutSec: options.waitTimeout,
|
|
984
|
+
onTick: options.quiet ? undefined : (attempt) => stderr(`waiting… (poll ${attempt})
|
|
985
|
+
`)
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
if (options.outputDir && result.ok && config.download) {
|
|
989
|
+
await downloadResults(config.download(result.data), options.outputDir, stderr, options.quiet, config.allowPrivateDownloadHosts ?? false, config.maxDownloadBytes ?? DEFAULT_DOWNLOAD_MAX_BYTES);
|
|
990
|
+
}
|
|
991
|
+
const exitCode = emitResult(result, { stdout, stderr }, {
|
|
992
|
+
json: options.json,
|
|
993
|
+
toolName: command,
|
|
994
|
+
errorHint: config.errorHint,
|
|
995
|
+
exitCodes: { ...DEFAULT_EXIT_CODES, ...config.exitCodes }
|
|
996
|
+
});
|
|
997
|
+
return exit(exitCode);
|
|
998
|
+
}
|
|
999
|
+
function safeInputSchema(tool) {
|
|
1000
|
+
try {
|
|
1001
|
+
return toJsonSchema(tool.schema, "input");
|
|
1002
|
+
} catch {
|
|
1003
|
+
return {};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export { coerceJsonArgs, toolResultFromError, flattenDiscriminatedUnion, collectTools, createToolRunner, formatToolError, fetchGuarded, readCapped, parseCliArgs, DEFAULT_EXIT_CODES, emitResult, pollUntil, pollUntilDone, createCli };
|