stitchkit 0.44.1 → 0.46.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 +1 -1
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +16 -0
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +3 -4
- package/dist/contract/errors.d.ts +1 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +2 -2
- package/dist/{index-yh0p5ddn.js → index-5s8b7z6q.js} +31 -11
- package/dist/{index-mvjyq9h9.js → index-62pqb23z.js} +309 -144
- package/dist/index-c40tkxcd.js +181 -0
- package/dist/{index-r6czv7yt.js → index-pwyedf7b.js} +4 -3
- package/dist/{index-6jypn22c.js → index-r1qp4rve.js} +9 -1
- package/dist/{index-s4qsmgwe.js → index-tss6bk5c.js} +4 -1
- package/dist/{index-36f1xkdb.js → index-w1s873ng.js} +2 -2
- package/dist/index-yxpe3phd.js +63 -0
- package/dist/{index-4fyn9f09.js → index-zwqty9zf.js} +396 -246
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +266 -17
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/secure-fetch.d.ts +23 -3
- package/dist/internal/secure-fetch.d.ts.map +1 -1
- package/dist/internal/typed.d.ts +7 -0
- package/dist/internal/typed.d.ts.map +1 -1
- package/dist/internal/within-dir.d.ts +6 -0
- package/dist/internal/within-dir.d.ts.map +1 -1
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +5 -4
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/event.d.ts +7 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +54 -125
- package/dist/observability/sanitize.d.ts +8 -1
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/observability/trace.d.ts +3 -1
- package/dist/observability/trace.d.ts.map +1 -1
- package/dist/react/cache-bridge.d.ts +4 -0
- package/dist/react/cache-bridge.d.ts.map +1 -1
- package/dist/react.js +46 -5
- package/dist/realtime/contract.d.ts +39 -0
- package/dist/realtime/contract.d.ts.map +1 -0
- package/dist/realtime/index.d.ts +3 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/rejection.d.ts +10 -0
- package/dist/realtime/rejection.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +30 -0
- package/dist/realtime/socket.d.ts.map +1 -0
- package/dist/server/cache.d.ts +5 -1
- package/dist/server/cache.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +1 -0
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/file.d.ts +6 -0
- package/dist/server/file.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +110 -11
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/realtime.d.ts +24 -0
- package/dist/server/realtime.d.ts.map +1 -0
- package/dist/server/router.d.ts +0 -8
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/types.d.ts +2 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/cli-args.d.ts +11 -3
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +5 -0
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +4 -3
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp-prepare.d.ts +95 -0
- package/dist/tools/mcp-prepare.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +4 -0
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.d.ts +2 -1
- package/dist/tools/mcp-stdio.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +2 -78
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +5 -0
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +1 -1
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/oauth-provider.d.ts +20 -1
- package/dist/tools/oauth-provider.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts +1 -1
- package/dist/tools/tool-logger.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +9 -0
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +2 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +342 -214
- package/llms-full.txt +175 -44
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/dist/index-frfyw9fa.js +0 -33
- package/dist/index-x3fcszf8.js +0 -8
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
isWithinDir,
|
|
2
3
|
jsonSchemaFields,
|
|
3
4
|
toJsonSchema
|
|
4
|
-
} from "./index-
|
|
5
|
-
import {
|
|
6
|
-
isWithinDir
|
|
7
|
-
} from "./index-x3fcszf8.js";
|
|
5
|
+
} from "./index-yxpe3phd.js";
|
|
8
6
|
import {
|
|
9
7
|
AppError,
|
|
10
8
|
STITCH_ERROR_STATUS,
|
|
@@ -17,7 +15,7 @@ import {
|
|
|
17
15
|
runWithRequestContext,
|
|
18
16
|
safeJsonParse,
|
|
19
17
|
validateDeclaredOutput
|
|
20
|
-
} from "./index-
|
|
18
|
+
} from "./index-5s8b7z6q.js";
|
|
21
19
|
|
|
22
20
|
// src/tools/coerce.ts
|
|
23
21
|
import { z } from "zod";
|
|
@@ -30,6 +28,24 @@ function needsJsonCoercion(schema) {
|
|
|
30
28
|
}
|
|
31
29
|
return false;
|
|
32
30
|
}
|
|
31
|
+
function acceptsRawString(schema) {
|
|
32
|
+
const type = schema._zod.def.type;
|
|
33
|
+
if (type === "string" || type === "template_literal")
|
|
34
|
+
return true;
|
|
35
|
+
if (schema instanceof z.ZodLiteral) {
|
|
36
|
+
return schema.def.values.some((value) => typeof value === "string");
|
|
37
|
+
}
|
|
38
|
+
if (schema instanceof z.ZodEnum) {
|
|
39
|
+
return schema.options.some((value) => typeof value === "string");
|
|
40
|
+
}
|
|
41
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable || schema instanceof z.ZodDefault) {
|
|
42
|
+
return acceptsRawString(schema.unwrap());
|
|
43
|
+
}
|
|
44
|
+
if (schema instanceof z.ZodUnion) {
|
|
45
|
+
return schema.def.options.some((option) => acceptsRawString(option));
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
33
49
|
function matchingVariant(union, value) {
|
|
34
50
|
const disc = union.def.discriminator;
|
|
35
51
|
const tag = value[disc];
|
|
@@ -52,6 +68,8 @@ function coerceValue(value, schema) {
|
|
|
52
68
|
}
|
|
53
69
|
let current = value;
|
|
54
70
|
if (typeof current === "string" && needsJsonCoercion(schema)) {
|
|
71
|
+
if (acceptsRawString(schema))
|
|
72
|
+
return current;
|
|
55
73
|
try {
|
|
56
74
|
current = safeJsonParse(current);
|
|
57
75
|
} catch {
|
|
@@ -574,15 +592,21 @@ async function runToolMethod(method, toolName, rawArgs, context, hooks, lifecycl
|
|
|
574
592
|
const startedAt = Date.now();
|
|
575
593
|
let hookContext = context;
|
|
576
594
|
const finish = async (result, thrown) => {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
595
|
+
try {
|
|
596
|
+
await hooks?.afterToolCall?.({
|
|
597
|
+
toolName,
|
|
598
|
+
args: rawArgs,
|
|
599
|
+
result,
|
|
600
|
+
durationMs: Date.now() - startedAt,
|
|
601
|
+
context: hookContext,
|
|
602
|
+
endpoint: method,
|
|
603
|
+
...thrown !== undefined && { error: thrown }
|
|
604
|
+
});
|
|
605
|
+
} catch (hookError) {
|
|
606
|
+
try {
|
|
607
|
+
console.error("[stitchkit] afterToolCall hook failed:", hookError);
|
|
608
|
+
} catch {}
|
|
609
|
+
}
|
|
586
610
|
return result;
|
|
587
611
|
};
|
|
588
612
|
let beforeRan = false;
|
|
@@ -615,7 +639,9 @@ async function runToolMethod(method, toolName, rawArgs, context, hooks, lifecycl
|
|
|
615
639
|
endpoint: method
|
|
616
640
|
});
|
|
617
641
|
} catch (hookErr) {
|
|
618
|
-
|
|
642
|
+
try {
|
|
643
|
+
console.error("[stitchkit] onToolError hook failed:", hookErr);
|
|
644
|
+
} catch {}
|
|
619
645
|
}
|
|
620
646
|
}
|
|
621
647
|
return finish(toolResultFromError(err), err);
|
|
@@ -994,8 +1020,14 @@ function formatToolError(result, toolName, errorHint) {
|
|
|
994
1020
|
|
|
995
1021
|
// src/tools/cli-args.ts
|
|
996
1022
|
import { z as z5 } from "zod";
|
|
1023
|
+
var NUMERIC_VALUE = /^-(\d+\.?\d*|\.\d+)$/;
|
|
997
1024
|
var BOOL_OPTIONS = new Set(["json", "wait", "quiet", "dry-run", "help"]);
|
|
998
1025
|
var VALUE_OPTIONS = new Set(["wait-timeout", "output-dir"]);
|
|
1026
|
+
var RESERVED_CLI_OPTIONS = new Set([...BOOL_OPTIONS, ...VALUE_OPTIONS]);
|
|
1027
|
+
|
|
1028
|
+
class CliArgumentError extends Error {
|
|
1029
|
+
name = "CliArgumentError";
|
|
1030
|
+
}
|
|
999
1031
|
function unwrap(field) {
|
|
1000
1032
|
if (field instanceof z5.ZodOptional || field instanceof z5.ZodNullable || field instanceof z5.ZodDefault) {
|
|
1001
1033
|
return unwrap(field.unwrap());
|
|
@@ -1021,31 +1053,71 @@ function classify(field) {
|
|
|
1021
1053
|
return "string";
|
|
1022
1054
|
return "other";
|
|
1023
1055
|
}
|
|
1024
|
-
function
|
|
1025
|
-
const
|
|
1026
|
-
if (!
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1056
|
+
function mergeField(fields, name, info) {
|
|
1057
|
+
const existing = fields.get(name);
|
|
1058
|
+
if (!existing) {
|
|
1059
|
+
fields.set(name, info);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (existing.kind === info.kind)
|
|
1063
|
+
return;
|
|
1064
|
+
if (existing.kind === "boolean" || info.kind === "boolean") {
|
|
1065
|
+
fields.set(name, { kind: "boolean" });
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
fields.set(name, { kind: "other" });
|
|
1069
|
+
}
|
|
1070
|
+
function collectSchemaFields(schema, fields) {
|
|
1071
|
+
const base = unwrap(schema);
|
|
1072
|
+
if (base instanceof z5.ZodObject) {
|
|
1073
|
+
for (const [name, raw] of Object.entries(base.shape)) {
|
|
1074
|
+
const fieldBase = unwrap(raw);
|
|
1075
|
+
const kind = classify(fieldBase);
|
|
1076
|
+
if (kind === "array" && fieldBase instanceof z5.ZodArray) {
|
|
1077
|
+
mergeField(fields, name, { kind, elementKind: classify(unwrap(fieldBase.element)) });
|
|
1078
|
+
} else {
|
|
1079
|
+
mergeField(fields, name, { kind });
|
|
1080
|
+
}
|
|
1035
1081
|
}
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (base instanceof z5.ZodUnion) {
|
|
1085
|
+
for (const option of base.def.options)
|
|
1086
|
+
collectSchemaFields(option, fields);
|
|
1087
|
+
return;
|
|
1036
1088
|
}
|
|
1089
|
+
if (base instanceof z5.ZodIntersection) {
|
|
1090
|
+
collectSchemaFields(base.def.left, fields);
|
|
1091
|
+
collectSchemaFields(base.def.right, fields);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function describeSchemaFields(schema) {
|
|
1096
|
+
const fields = new Map;
|
|
1097
|
+
if (schema)
|
|
1098
|
+
collectSchemaFields(schema, fields);
|
|
1037
1099
|
return fields;
|
|
1038
1100
|
}
|
|
1039
|
-
|
|
1101
|
+
var TRUE_WORDS = new Set(["true", "1", "yes", "on"]);
|
|
1102
|
+
var FALSE_WORDS = new Set(["false", "0", "no", "off"]);
|
|
1103
|
+
function parseReservedBool(name, value) {
|
|
1040
1104
|
const v = value.toLowerCase();
|
|
1041
|
-
if (v
|
|
1105
|
+
if (TRUE_WORDS.has(v))
|
|
1106
|
+
return true;
|
|
1107
|
+
if (FALSE_WORDS.has(v))
|
|
1042
1108
|
return false;
|
|
1043
|
-
|
|
1109
|
+
throw new CliArgumentError(`--${name} expects a boolean (true/false), got "${value}"`);
|
|
1044
1110
|
}
|
|
1045
1111
|
function coerceScalar(kind, value) {
|
|
1046
1112
|
switch (kind) {
|
|
1047
|
-
case "boolean":
|
|
1048
|
-
|
|
1113
|
+
case "boolean": {
|
|
1114
|
+
const v = value.toLowerCase();
|
|
1115
|
+
if (TRUE_WORDS.has(v))
|
|
1116
|
+
return true;
|
|
1117
|
+
if (FALSE_WORDS.has(v))
|
|
1118
|
+
return false;
|
|
1119
|
+
return value;
|
|
1120
|
+
}
|
|
1049
1121
|
case "number": {
|
|
1050
1122
|
const n = Number(value);
|
|
1051
1123
|
return value.trim() !== "" && !Number.isNaN(n) ? n : value;
|
|
@@ -1090,8 +1162,10 @@ function coerceField(info, values) {
|
|
|
1090
1162
|
return coerceScalar(info.kind, last);
|
|
1091
1163
|
}
|
|
1092
1164
|
function setNested(target, path, value) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1165
|
+
const unsafe = path.find(isUnsafeKey);
|
|
1166
|
+
if (unsafe !== undefined) {
|
|
1167
|
+
throw new CliArgumentError(`Unsafe option path segment "${unsafe}"`);
|
|
1168
|
+
}
|
|
1095
1169
|
let node = target;
|
|
1096
1170
|
for (let i = 0;i < path.length - 1; i++) {
|
|
1097
1171
|
const key = path[i];
|
|
@@ -1110,7 +1184,7 @@ function setNested(target, path, value) {
|
|
|
1110
1184
|
if (leaf !== undefined)
|
|
1111
1185
|
node[leaf] = value;
|
|
1112
1186
|
}
|
|
1113
|
-
function parseCliArgs(argv, schema) {
|
|
1187
|
+
function parseCliArgs(argv, schema, config = {}) {
|
|
1114
1188
|
const options = {
|
|
1115
1189
|
json: false,
|
|
1116
1190
|
wait: false,
|
|
@@ -1118,50 +1192,11 @@ function parseCliArgs(argv, schema) {
|
|
|
1118
1192
|
dryRun: false,
|
|
1119
1193
|
help: false
|
|
1120
1194
|
};
|
|
1121
|
-
const rest = [];
|
|
1122
|
-
for (let i = 0;i < argv.length; i++) {
|
|
1123
|
-
const tok = argv[i];
|
|
1124
|
-
if (tok === undefined)
|
|
1125
|
-
continue;
|
|
1126
|
-
if (tok === "-h") {
|
|
1127
|
-
options.help = true;
|
|
1128
|
-
continue;
|
|
1129
|
-
}
|
|
1130
|
-
if (tok.startsWith("--")) {
|
|
1131
|
-
const eq = tok.indexOf("=");
|
|
1132
|
-
const name = eq >= 0 ? tok.slice(2, eq) : tok.slice(2);
|
|
1133
|
-
const inline = eq >= 0 ? tok.slice(eq + 1) : undefined;
|
|
1134
|
-
if (BOOL_OPTIONS.has(name)) {
|
|
1135
|
-
const on = inline === undefined ? true : parseBool(inline);
|
|
1136
|
-
if (name === "dry-run")
|
|
1137
|
-
options.dryRun = on;
|
|
1138
|
-
else if (name === "json")
|
|
1139
|
-
options.json = on;
|
|
1140
|
-
else if (name === "wait")
|
|
1141
|
-
options.wait = on;
|
|
1142
|
-
else if (name === "quiet")
|
|
1143
|
-
options.quiet = on;
|
|
1144
|
-
else if (name === "help")
|
|
1145
|
-
options.help = on;
|
|
1146
|
-
continue;
|
|
1147
|
-
}
|
|
1148
|
-
if (VALUE_OPTIONS.has(name)) {
|
|
1149
|
-
const value = inline ?? argv[++i];
|
|
1150
|
-
if (value !== undefined) {
|
|
1151
|
-
if (name === "wait-timeout") {
|
|
1152
|
-
const n = Number(value);
|
|
1153
|
-
if (!Number.isNaN(n))
|
|
1154
|
-
options.waitTimeout = n;
|
|
1155
|
-
} else if (name === "output-dir") {
|
|
1156
|
-
options.outputDir = value;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
continue;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
rest.push(tok);
|
|
1163
|
-
}
|
|
1164
1195
|
const fields = describeSchemaFields(schema);
|
|
1196
|
+
for (const name of config.knownFields ?? []) {
|
|
1197
|
+
if (!fields.has(name))
|
|
1198
|
+
fields.set(name, { kind: "other" });
|
|
1199
|
+
}
|
|
1165
1200
|
const flags = new Map;
|
|
1166
1201
|
const boolFlags = new Map;
|
|
1167
1202
|
const positionals = [];
|
|
@@ -1172,34 +1207,88 @@ function parseCliArgs(argv, schema) {
|
|
|
1172
1207
|
else
|
|
1173
1208
|
flags.set(name, [value]);
|
|
1174
1209
|
};
|
|
1175
|
-
|
|
1176
|
-
|
|
1210
|
+
let optionsEnded = false;
|
|
1211
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1212
|
+
const tok = argv[i];
|
|
1177
1213
|
if (tok === undefined)
|
|
1178
1214
|
continue;
|
|
1179
|
-
if (!tok
|
|
1215
|
+
if (!optionsEnded && tok === "--") {
|
|
1216
|
+
optionsEnded = true;
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (optionsEnded || !tok.startsWith("-") || tok === "-") {
|
|
1180
1220
|
positionals.push(tok);
|
|
1181
1221
|
continue;
|
|
1182
1222
|
}
|
|
1223
|
+
if (tok === "-h") {
|
|
1224
|
+
options.help = true;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (!tok.startsWith("--")) {
|
|
1228
|
+
throw new CliArgumentError(`Unknown option "${tok}"`);
|
|
1229
|
+
}
|
|
1183
1230
|
const eq = tok.indexOf("=");
|
|
1184
1231
|
const name = eq >= 0 ? tok.slice(2, eq) : tok.slice(2);
|
|
1185
1232
|
let value = eq >= 0 ? tok.slice(eq + 1) : undefined;
|
|
1233
|
+
if (name.length === 0)
|
|
1234
|
+
throw new CliArgumentError("Invalid empty option name");
|
|
1235
|
+
if (BOOL_OPTIONS.has(name)) {
|
|
1236
|
+
const enabled = value === undefined ? true : parseReservedBool(name, value);
|
|
1237
|
+
if (name === "dry-run")
|
|
1238
|
+
options.dryRun = enabled;
|
|
1239
|
+
else if (name === "json")
|
|
1240
|
+
options.json = enabled;
|
|
1241
|
+
else if (name === "wait")
|
|
1242
|
+
options.wait = enabled;
|
|
1243
|
+
else if (name === "quiet")
|
|
1244
|
+
options.quiet = enabled;
|
|
1245
|
+
else
|
|
1246
|
+
options.help = enabled;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (VALUE_OPTIONS.has(name)) {
|
|
1250
|
+
const next = argv[i + 1];
|
|
1251
|
+
value = value ?? next;
|
|
1252
|
+
if (value === undefined || value.startsWith("-") && !NUMERIC_VALUE.test(value)) {
|
|
1253
|
+
throw new CliArgumentError(`--${name} requires a value`);
|
|
1254
|
+
}
|
|
1255
|
+
if (eq < 0)
|
|
1256
|
+
i++;
|
|
1257
|
+
if (name === "wait-timeout") {
|
|
1258
|
+
const timeout = Number(value);
|
|
1259
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
1260
|
+
throw new CliArgumentError("--wait-timeout must be a positive number");
|
|
1261
|
+
}
|
|
1262
|
+
options.waitTimeout = timeout;
|
|
1263
|
+
} else {
|
|
1264
|
+
options.outputDir = value;
|
|
1265
|
+
}
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1186
1268
|
if (value === undefined && name.startsWith("no-") && fields.get(name.slice(3))?.kind === "boolean") {
|
|
1187
1269
|
boolFlags.set(name.slice(3), false);
|
|
1188
1270
|
continue;
|
|
1189
1271
|
}
|
|
1272
|
+
const unsafeSegment = name.split(".").find(isUnsafeKey);
|
|
1273
|
+
if (unsafeSegment !== undefined) {
|
|
1274
|
+
throw new CliArgumentError(`Unsafe option name "--${name}"`);
|
|
1275
|
+
}
|
|
1190
1276
|
const info = fields.get(name);
|
|
1277
|
+
const rootName = name.split(".")[0] ?? name;
|
|
1278
|
+
if (!fields.has(rootName) && !config.allowUnknown) {
|
|
1279
|
+
throw new CliArgumentError(`Unknown option "--${name}"`);
|
|
1280
|
+
}
|
|
1191
1281
|
if (value === undefined) {
|
|
1192
1282
|
if (info?.kind === "boolean") {
|
|
1193
1283
|
boolFlags.set(name, true);
|
|
1194
1284
|
continue;
|
|
1195
1285
|
}
|
|
1196
|
-
const next =
|
|
1197
|
-
if (next !== undefined && !next.startsWith("
|
|
1286
|
+
const next = argv[i + 1];
|
|
1287
|
+
if (next !== undefined && (!next.startsWith("-") || NUMERIC_VALUE.test(next))) {
|
|
1198
1288
|
value = next;
|
|
1199
1289
|
i++;
|
|
1200
1290
|
} else {
|
|
1201
|
-
|
|
1202
|
-
continue;
|
|
1291
|
+
throw new CliArgumentError(`--${name} requires a value`);
|
|
1203
1292
|
}
|
|
1204
1293
|
}
|
|
1205
1294
|
pushFlag(name, value);
|
|
@@ -1216,19 +1305,36 @@ function parseCliArgs(argv, schema) {
|
|
|
1216
1305
|
if (value !== undefined)
|
|
1217
1306
|
toolArgs[key] = coerceField(fields.get(key), [value]);
|
|
1218
1307
|
}
|
|
1308
|
+
if (pi < positionals.length) {
|
|
1309
|
+
throw new CliArgumentError(`Unexpected positional argument "${positionals[pi]}"`);
|
|
1310
|
+
}
|
|
1219
1311
|
for (const [key, value] of boolFlags) {
|
|
1220
|
-
if (isUnsafeKey(key))
|
|
1221
|
-
continue;
|
|
1222
1312
|
toolArgs[key] = value;
|
|
1223
1313
|
}
|
|
1314
|
+
const dottedRoots = new Map;
|
|
1315
|
+
for (const key of flags.keys()) {
|
|
1316
|
+
const dot = key.indexOf(".");
|
|
1317
|
+
if (dot > 0)
|
|
1318
|
+
dottedRoots.set(key.slice(0, dot), key);
|
|
1319
|
+
}
|
|
1320
|
+
for (const [root, dotted] of dottedRoots) {
|
|
1321
|
+
if (flags.has(root)) {
|
|
1322
|
+
throw new CliArgumentError(`--${root} conflicts with --${dotted} — pass one form, not both`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1224
1325
|
for (const [key, values] of flags) {
|
|
1225
1326
|
if (key.includes(".")) {
|
|
1226
|
-
|
|
1327
|
+
if (values.length > 1) {
|
|
1328
|
+
throw new CliArgumentError(`--${key} was passed ${values.length} times`);
|
|
1329
|
+
}
|
|
1330
|
+
setNested(toolArgs, key.split("."), looseCoerce(values[0] ?? ""));
|
|
1227
1331
|
continue;
|
|
1228
1332
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1333
|
+
const info = fields.get(key);
|
|
1334
|
+
if (values.length > 1 && info !== undefined && info.kind !== "array" && info.kind !== "other") {
|
|
1335
|
+
throw new CliArgumentError(`--${key} was passed ${values.length} times`);
|
|
1336
|
+
}
|
|
1337
|
+
toolArgs[key] = coerceField(info, values);
|
|
1232
1338
|
}
|
|
1233
1339
|
return { toolArgs, options };
|
|
1234
1340
|
}
|
|
@@ -1415,47 +1521,71 @@ function isPrivateIp(ip) {
|
|
|
1415
1521
|
}
|
|
1416
1522
|
return false;
|
|
1417
1523
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1524
|
+
function raceWithAbort(promise, signal) {
|
|
1525
|
+
if (signal.aborted) {
|
|
1526
|
+
promise.catch(() => {
|
|
1527
|
+
return;
|
|
1528
|
+
});
|
|
1529
|
+
return Promise.reject(signal.reason ?? new Error("aborted"));
|
|
1530
|
+
}
|
|
1531
|
+
return new Promise((resolve, reject) => {
|
|
1532
|
+
const onAbort = () => reject(signal.reason ?? new Error("aborted"));
|
|
1533
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1534
|
+
promise.then((value) => {
|
|
1535
|
+
signal.removeEventListener("abort", onAbort);
|
|
1536
|
+
resolve(value);
|
|
1537
|
+
}, (error) => {
|
|
1538
|
+
signal.removeEventListener("abort", onAbort);
|
|
1539
|
+
reject(error);
|
|
1540
|
+
});
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
async function assertPublicUrl(url, signal) {
|
|
1544
|
+
await resolvePublicAddress(url, signal);
|
|
1545
|
+
}
|
|
1546
|
+
async function fetchGuarded(start, allowPrivate, options = {}) {
|
|
1547
|
+
const timeoutMs = options.timeoutMs ?? 15000;
|
|
1548
|
+
const bodyTimeoutMs = options.bodyTimeoutMs ?? 120000;
|
|
1549
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1550
|
+
throw new Error("timeoutMs must be positive");
|
|
1430
1551
|
}
|
|
1431
|
-
if (
|
|
1432
|
-
throw new Error("
|
|
1552
|
+
if (!Number.isFinite(bodyTimeoutMs) || bodyTimeoutMs <= 0) {
|
|
1553
|
+
throw new Error("bodyTimeoutMs must be positive");
|
|
1433
1554
|
}
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1555
|
+
const controller = new AbortController;
|
|
1556
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
1557
|
+
const headerTimer = setTimeout(() => {
|
|
1558
|
+
controller.abort(new Error(`fetch did not produce response headers within ${timeoutMs}ms`));
|
|
1559
|
+
}, timeoutMs);
|
|
1560
|
+
headerTimer.unref?.();
|
|
1561
|
+
let settled = false;
|
|
1562
|
+
try {
|
|
1563
|
+
let url = start;
|
|
1564
|
+
for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
|
|
1565
|
+
if (allowPrivate)
|
|
1566
|
+
assertHttpUrl(url);
|
|
1567
|
+
else
|
|
1568
|
+
await assertPublicUrl(url, signal);
|
|
1569
|
+
const res = await fetch(url, { redirect: "manual", signal });
|
|
1570
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
1571
|
+
if (location === null) {
|
|
1572
|
+
settled = true;
|
|
1573
|
+
clearTimeout(headerTimer);
|
|
1574
|
+
const bodyTimer = setTimeout(() => {
|
|
1575
|
+
controller.abort(new Error(`fetch body was not consumed within ${bodyTimeoutMs}ms`));
|
|
1576
|
+
}, bodyTimeoutMs);
|
|
1577
|
+
bodyTimer.unref?.();
|
|
1578
|
+
return res;
|
|
1579
|
+
}
|
|
1580
|
+
await res.body?.cancel();
|
|
1581
|
+
url = new URL(location, url);
|
|
1438
1582
|
}
|
|
1583
|
+
throw new Error("too many redirects");
|
|
1584
|
+
} finally {
|
|
1585
|
+
if (!settled)
|
|
1586
|
+
clearTimeout(headerTimer);
|
|
1439
1587
|
}
|
|
1440
1588
|
}
|
|
1441
|
-
async function fetchGuarded(start, allowPrivate) {
|
|
1442
|
-
let url = start;
|
|
1443
|
-
for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
|
|
1444
|
-
if (allowPrivate)
|
|
1445
|
-
assertHttpUrl(url);
|
|
1446
|
-
else
|
|
1447
|
-
await assertPublicUrl(url);
|
|
1448
|
-
const res = await fetch(url, { redirect: "manual" });
|
|
1449
|
-
if (res.status < 300 || res.status >= 400)
|
|
1450
|
-
return res;
|
|
1451
|
-
const location = res.headers.get("location");
|
|
1452
|
-
if (!location)
|
|
1453
|
-
return res;
|
|
1454
|
-
await res.body?.cancel();
|
|
1455
|
-
url = new URL(location, url);
|
|
1456
|
-
}
|
|
1457
|
-
throw new Error("too many redirects");
|
|
1458
|
-
}
|
|
1459
1589
|
async function resolvePublicAddress(url, signal) {
|
|
1460
1590
|
assertHttpUrl(url);
|
|
1461
1591
|
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
@@ -1473,12 +1603,8 @@ async function resolvePublicAddress(url, signal) {
|
|
|
1473
1603
|
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
1474
1604
|
throw new Error("refusing to fetch an internal host");
|
|
1475
1605
|
}
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
new Promise((_resolve, reject) => {
|
|
1479
|
-
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
1480
|
-
})
|
|
1481
|
-
]);
|
|
1606
|
+
const pending = lookup(host, { all: true });
|
|
1607
|
+
const records = signal ? await raceWithAbort(pending, signal) : await pending;
|
|
1482
1608
|
for (const record2 of records) {
|
|
1483
1609
|
if (isPrivateIp(record2.address)) {
|
|
1484
1610
|
throw new Error("refusing to fetch a host that resolves to a private address");
|
|
@@ -1501,8 +1627,8 @@ async function fetchPinnedDocument(start, options) {
|
|
|
1501
1627
|
if (!Number.isInteger(maxRedirects) || maxRedirects < 0) {
|
|
1502
1628
|
throw new Error("maxRedirects must be a non-negative integer");
|
|
1503
1629
|
}
|
|
1630
|
+
const signal = AbortSignal.timeout(options.timeoutMs);
|
|
1504
1631
|
for (let hop = 0;hop <= maxRedirects; hop++) {
|
|
1505
|
-
const signal = AbortSignal.timeout(options.timeoutMs);
|
|
1506
1632
|
if (options.requireHttps && url.protocol !== "https:") {
|
|
1507
1633
|
throw new Error("document URL and every redirect must use https");
|
|
1508
1634
|
}
|
|
@@ -1704,12 +1830,7 @@ function renderTopHelp(name, version, tools) {
|
|
|
1704
1830
|
}
|
|
1705
1831
|
function renderCommandHelp(name, command, tool) {
|
|
1706
1832
|
const lines = [tool.method.desc, "", `Usage: ${name} ${command} [args] [--flags]`, ""];
|
|
1707
|
-
|
|
1708
|
-
try {
|
|
1709
|
-
fields = jsonSchemaFields(tool.presentationSchema);
|
|
1710
|
-
} catch {
|
|
1711
|
-
lines.push("(arguments: pass a JSON object — this command has a complex schema)", "");
|
|
1712
|
-
}
|
|
1833
|
+
const fields = jsonSchemaFields(tool.presentationSchema);
|
|
1713
1834
|
if (fields.length > 0) {
|
|
1714
1835
|
lines.push("Arguments:");
|
|
1715
1836
|
const width = Math.max(...fields.map((f) => f.name.length));
|
|
@@ -1725,11 +1846,12 @@ function renderCommandHelp(name, command, tool) {
|
|
|
1725
1846
|
`;
|
|
1726
1847
|
}
|
|
1727
1848
|
var DEFAULT_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
1728
|
-
async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes) {
|
|
1849
|
+
async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes, timeoutMs) {
|
|
1729
1850
|
const root = resolve(dir);
|
|
1851
|
+
let succeeded = true;
|
|
1730
1852
|
for (const file of files) {
|
|
1731
1853
|
try {
|
|
1732
|
-
const res = await fetchGuarded(new URL(file.url), allowPrivate);
|
|
1854
|
+
const res = await fetchGuarded(new URL(file.url), allowPrivate, { timeoutMs });
|
|
1733
1855
|
if (!res.ok)
|
|
1734
1856
|
throw new Error(`HTTP ${res.status}`);
|
|
1735
1857
|
const buffer = await readCapped(res, maxBytes);
|
|
@@ -1741,10 +1863,12 @@ async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes
|
|
|
1741
1863
|
stderr(`saved ${target} (${(buffer.length / 1024).toFixed(0)}KB)
|
|
1742
1864
|
`);
|
|
1743
1865
|
} catch (err) {
|
|
1866
|
+
succeeded = false;
|
|
1744
1867
|
stderr(`failed to download ${file.url}: ${err instanceof Error ? err.message : String(err)}
|
|
1745
1868
|
`);
|
|
1746
1869
|
}
|
|
1747
1870
|
}
|
|
1871
|
+
return succeeded;
|
|
1748
1872
|
}
|
|
1749
1873
|
async function createCli(config) {
|
|
1750
1874
|
const stdout = config.stdout ?? ((text) => void process.stdout.write(text));
|
|
@@ -1759,6 +1883,13 @@ async function createCli(config) {
|
|
|
1759
1883
|
for (const service of services) {
|
|
1760
1884
|
for (const mountable of collectTools(service, "CLI")) {
|
|
1761
1885
|
assertUniqueToolName(mountable.name, tools.has(mountable.name), "CLI command");
|
|
1886
|
+
if (mountable.name === "help" || mountable.name === "version") {
|
|
1887
|
+
throw new Error(`[stitchkit] CLI command "${mountable.name}" is reserved`);
|
|
1888
|
+
}
|
|
1889
|
+
const conflicting = jsonSchemaFields(mountable.presentationSchema).map((field) => field.name).filter((name) => RESERVED_CLI_OPTIONS.has(name));
|
|
1890
|
+
if (conflicting.length > 0) {
|
|
1891
|
+
throw new Error(`[stitchkit] CLI command "${mountable.name}" declares reserved option field(s): ${conflicting.join(", ")}`);
|
|
1892
|
+
}
|
|
1762
1893
|
tools.set(mountable.name, mountable);
|
|
1763
1894
|
}
|
|
1764
1895
|
}
|
|
@@ -1786,7 +1917,40 @@ async function createCli(config) {
|
|
|
1786
1917
|
`);
|
|
1787
1918
|
return exit(1);
|
|
1788
1919
|
}
|
|
1789
|
-
const
|
|
1920
|
+
const beforeSeparator = commandArgv.indexOf("--") === -1 ? commandArgv : commandArgv.slice(0, commandArgv.indexOf("--"));
|
|
1921
|
+
if (beforeSeparator.includes("--help") || beforeSeparator.includes("-h")) {
|
|
1922
|
+
stdout(renderCommandHelp(config.name, command, tool));
|
|
1923
|
+
return exit(0);
|
|
1924
|
+
}
|
|
1925
|
+
let parsed;
|
|
1926
|
+
try {
|
|
1927
|
+
parsed = parseCliArgs(commandArgv, tool.argumentSchema, {
|
|
1928
|
+
allowUnknown: config.passthrough?.[command] !== undefined,
|
|
1929
|
+
knownFields: jsonSchemaFields(tool.presentationSchema).map((field) => field.name)
|
|
1930
|
+
});
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
if (!(error instanceof CliArgumentError))
|
|
1933
|
+
throw error;
|
|
1934
|
+
stderr(`${error.message}
|
|
1935
|
+
`);
|
|
1936
|
+
return exit(2);
|
|
1937
|
+
}
|
|
1938
|
+
const { toolArgs, options } = parsed;
|
|
1939
|
+
if (options.wait && !config.wait?.[command]) {
|
|
1940
|
+
stderr(`--wait is not configured for command "${command}"
|
|
1941
|
+
`);
|
|
1942
|
+
return exit(2);
|
|
1943
|
+
}
|
|
1944
|
+
if (options.waitTimeout !== undefined && !options.wait) {
|
|
1945
|
+
stderr(`--wait-timeout requires --wait
|
|
1946
|
+
`);
|
|
1947
|
+
return exit(2);
|
|
1948
|
+
}
|
|
1949
|
+
if (options.outputDir !== undefined && !config.download) {
|
|
1950
|
+
stderr(`--output-dir is not configured for this CLI
|
|
1951
|
+
`);
|
|
1952
|
+
return exit(2);
|
|
1953
|
+
}
|
|
1790
1954
|
if (options.help) {
|
|
1791
1955
|
stdout(renderCommandHelp(config.name, command, tool));
|
|
1792
1956
|
return exit(0);
|
|
@@ -1834,8 +1998,9 @@ async function createCli(config) {
|
|
|
1834
1998
|
`)
|
|
1835
1999
|
});
|
|
1836
2000
|
}
|
|
2001
|
+
let downloadsOk = true;
|
|
1837
2002
|
if (options.outputDir && result.ok && config.download) {
|
|
1838
|
-
await downloadResults(config.download(result.data), options.outputDir, stderr, options.quiet, config.allowPrivateDownloadHosts ?? false, config.maxDownloadBytes ?? DEFAULT_DOWNLOAD_MAX_BYTES);
|
|
2003
|
+
downloadsOk = await downloadResults(config.download(result.data), options.outputDir, stderr, options.quiet, config.allowPrivateDownloadHosts ?? false, config.maxDownloadBytes ?? DEFAULT_DOWNLOAD_MAX_BYTES, config.downloadTimeoutMs);
|
|
1839
2004
|
}
|
|
1840
2005
|
const exitCode = emitResult(result, { stdout, stderr }, {
|
|
1841
2006
|
json: options.json,
|
|
@@ -1843,7 +2008,7 @@ async function createCli(config) {
|
|
|
1843
2008
|
errorHint: config.errorHint,
|
|
1844
2009
|
exitCodes: { ...DEFAULT_EXIT_CODES, ...config.exitCodes }
|
|
1845
2010
|
});
|
|
1846
|
-
return exit(exitCode);
|
|
2011
|
+
return exit(downloadsOk ? exitCode : 1);
|
|
1847
2012
|
}
|
|
1848
2013
|
function safeInputSchema(tool) {
|
|
1849
2014
|
return tool.presentationSchema;
|