callman-mcp 0.7.1 → 0.8.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/CHANGELOG.md +17 -0
- package/README.md +22 -5
- package/dist/client/api.d.ts +59 -1
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +67 -0
- package/dist/client/api.js.map +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +18 -11
- package/dist/client/errors.js.map +1 -1
- package/dist/client/types.d.ts +105 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +1 -0
- package/dist/context.js.map +1 -1
- package/dist/domain/requestShape.d.ts +4 -4
- package/dist/resources/docs/mocks.d.ts +2 -0
- package/dist/resources/docs/mocks.d.ts.map +1 -0
- package/dist/resources/docs/mocks.js +210 -0
- package/dist/resources/docs/mocks.js.map +1 -0
- package/dist/resources/docs/overview.d.ts +1 -1
- package/dist/resources/docs/overview.d.ts.map +1 -1
- package/dist/resources/docs/overview.js +7 -1
- package/dist/resources/docs/overview.js.map +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +2 -0
- package/dist/resources/index.js.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/catalog.d.ts +1 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +18 -0
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/meta.d.ts.map +1 -1
- package/dist/tools/meta.js +29 -2
- package/dist/tools/meta.js.map +1 -1
- package/dist/tools/mockShared.d.ts +983 -0
- package/dist/tools/mockShared.d.ts.map +1 -0
- package/dist/tools/mockShared.js +387 -0
- package/dist/tools/mockShared.js.map +1 -0
- package/dist/tools/mocks.d.ts +4 -0
- package/dist/tools/mocks.d.ts.map +1 -0
- package/dist/tools/mocks.js +331 -0
- package/dist/tools/mocks.js.map +1 -0
- package/dist/tools/mocksLocal.d.ts +24 -0
- package/dist/tools/mocksLocal.d.ts.map +1 -0
- package/dist/tools/mocksLocal.js +169 -0
- package/dist/tools/mocksLocal.js.map +1 -0
- package/dist-cjs/client/api.js +67 -0
- package/dist-cjs/client/errors.js +18 -11
- package/dist-cjs/context.js +1 -0
- package/dist-cjs/resources/docs/mocks.js +212 -0
- package/dist-cjs/resources/docs/overview.js +7 -1
- package/dist-cjs/resources/index.js +2 -0
- package/dist-cjs/server.js +3 -2
- package/dist-cjs/tools/catalog.js +18 -0
- package/dist-cjs/tools/index.js +2 -0
- package/dist-cjs/tools/meta.js +28 -1
- package/dist-cjs/tools/mockShared.js +414 -0
- package/dist-cjs/tools/mocks.js +334 -0
- package/dist-cjs/tools/mocksLocal.js +179 -0
- package/package.json +1 -1
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logView = exports.resolveLocalMock = exports.localCalls = exports.localMockView = exports.localRouteView = exports.localBaseUrl = exports.toLocalRoute = exports.LOCAL_DELAY_MAX_MS = exports.LOCAL_BODY_MAX_BYTES = exports.resolveRoute = exports.findMock = exports.resolveMock = exports.loadMocks = exports.requireTarget = exports.targetArg = exports.mockRefArg = exports.curlLine = exports.sampleRoutePath = exports.mockDetailView = exports.routesView = exports.routeView = exports.alternativeUrl = exports.mockSummaryView = exports.mockListView = exports.duplicateRoutes = exports.toServerRoutePatch = exports.toServerRoute = exports.assertRoutePath = exports.mockRoutePatchSchema = exports.mockRouteInputSchema = exports.mockResponseSchema = exports.MOCK_METHODS = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const errors_js_1 = require("../client/errors.js");
|
|
6
|
+
const resolve_js_1 = require("../domain/resolve.js");
|
|
7
|
+
const ids_js_1 = require("../lib/ids.js");
|
|
8
|
+
const strict_js_1 = require("../lib/strict.js");
|
|
9
|
+
const truncate_js_1 = require("../lib/truncate.js");
|
|
10
|
+
const lookups_js_1 = require("./lookups.js");
|
|
11
|
+
const shared_js_1 = require("./shared.js");
|
|
12
|
+
// Shared by the mock tools: the route input schema (typed, but the backend
|
|
13
|
+
// stays the validator), conversion to the server's route body, the shape of a
|
|
14
|
+
// mock as the model reads it, reference resolution (mock by name / slug / id,
|
|
15
|
+
// route by id / "METHOD /path" / name) and the ready-to-run curl lines.
|
|
16
|
+
exports.MOCK_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "ANY"];
|
|
17
|
+
const strictObject = (shape, where) => zod_1.z.object(shape, { errorMap: (0, strict_js_1.unknownKeysErrorMap)("field", Object.keys(shape), where) }).strict();
|
|
18
|
+
const statusCode = zod_1.z.coerce.number().int().min(100).max(599);
|
|
19
|
+
const headersSchema = zod_1.z.record(zod_1.z.string().max(8192));
|
|
20
|
+
// ── route input ─────────────────────────────────────────────────────────
|
|
21
|
+
const delaySchema = zod_1.z.discriminatedUnion("mode", [
|
|
22
|
+
strictObject({ mode: zod_1.z.literal("none") }, "delay"),
|
|
23
|
+
strictObject({ mode: zod_1.z.literal("fixed"), ms: zod_1.z.number().int().min(0).max(60_000) }, "delay"),
|
|
24
|
+
strictObject({ mode: zod_1.z.literal("random"), minMs: zod_1.z.number().int().min(0).max(60_000), maxMs: zod_1.z.number().int().min(0).max(60_000) }, "delay"),
|
|
25
|
+
]);
|
|
26
|
+
const staticOrTemplateResponse = (type) => strictObject({
|
|
27
|
+
type: zod_1.z.literal(type),
|
|
28
|
+
statusCode,
|
|
29
|
+
headers: headersSchema.optional(),
|
|
30
|
+
body: zod_1.z.unknown().optional().describe(type === "template" ? 'JSON value; every string may contain {{request.params.id}}, {{request.query.x}}, {{request.body.x}}, {{state.key}}, {{faker.email}}, {{random.int(1,9)}}, {{now}} …' : "Any JSON value (object, array, string, number, null). Omit for an empty body."),
|
|
31
|
+
}, `${type} response`);
|
|
32
|
+
const builderResponse = strictObject({
|
|
33
|
+
type: zod_1.z.literal("builder"),
|
|
34
|
+
preset: zod_1.z.enum(["list", "detail", "create", "update", "delete", "echo", "custom"]),
|
|
35
|
+
config: zod_1.z.record(zod_1.z.unknown()).describe("The preset's config; config.preset must equal preset. See callman://docs/mocks"),
|
|
36
|
+
}, "builder response");
|
|
37
|
+
exports.mockResponseSchema = zod_1.z.discriminatedUnion("type", [staticOrTemplateResponse("static"), staticOrTemplateResponse("template"), builderResponse]);
|
|
38
|
+
const variantSchema = strictObject({
|
|
39
|
+
variantId: zod_1.z.string().max(64).optional(),
|
|
40
|
+
priority: zod_1.z.number().int().min(0).max(10_000).describe("Lower runs first; the first matching variant wins"),
|
|
41
|
+
condition: strictObject({
|
|
42
|
+
type: zod_1.z.enum(["all", "any"]),
|
|
43
|
+
rules: zod_1.z
|
|
44
|
+
.array(strictObject({
|
|
45
|
+
field: zod_1.z.string().min(1).max(200).describe("headers.<name>, params.<name>, query.<name> or body.<dotted.path>"),
|
|
46
|
+
operator: zod_1.z.enum(["exists", "not-exists", "equals", "not-equals", "contains", "starts-with", "matches"]),
|
|
47
|
+
value: zod_1.z.string().max(2000).optional(),
|
|
48
|
+
}, "condition rule"))
|
|
49
|
+
.min(1)
|
|
50
|
+
.max(20),
|
|
51
|
+
}, "condition"),
|
|
52
|
+
response: exports.mockResponseSchema,
|
|
53
|
+
}, "variant");
|
|
54
|
+
const requestValidationSchema = strictObject({ mode: zod_1.z.enum(["any", "schema"]), schema: zod_1.z.unknown().optional().describe("JSON Schema (draft-07) the request body must match when mode is schema; a mismatch answers 400") }, "requestValidation");
|
|
55
|
+
const routeShape = {
|
|
56
|
+
method: zod_1.z.enum(exports.MOCK_METHODS),
|
|
57
|
+
path: zod_1.z.string().min(1).max(256).describe("Starts with /; :name segments are path parameters, a trailing /* is a catch-all. Example: /api/users/:id"),
|
|
58
|
+
name: zod_1.z.string().max(120).optional(),
|
|
59
|
+
response: exports.mockResponseSchema.optional().describe("The response. Shorthand instead: statusCode / headers / body (a static response)"),
|
|
60
|
+
statusCode: statusCode.optional().describe("Shorthand for a static response (default 200)"),
|
|
61
|
+
headers: headersSchema.optional().describe("Shorthand: static response headers"),
|
|
62
|
+
body: zod_1.z.unknown().optional().describe("Shorthand: static response body (any JSON value; on target desktop a string is sent as is and anything else as formatted JSON)"),
|
|
63
|
+
variants: zod_1.z.array(variantSchema).max(20).optional().describe("Server mocks only: other responses chosen by a condition on the request"),
|
|
64
|
+
delay: delaySchema.optional().describe("Wait before answering (server: ≤ 60 s, desktop: ≤ 30 s)"),
|
|
65
|
+
requestValidation: requestValidationSchema.nullish().describe("Server mocks only: validate the request body against a JSON Schema"),
|
|
66
|
+
};
|
|
67
|
+
exports.mockRouteInputSchema = strictObject(routeShape, "route").describe("One mock route");
|
|
68
|
+
exports.mockRoutePatchSchema = exports.mockRouteInputSchema.partial().describe("The fields to change; the rest of the route is kept");
|
|
69
|
+
// ── path / response validation (early, readable errors) ────────────────
|
|
70
|
+
const PATH_PATTERN = /^\/[A-Za-z0-9/_\-:*.]*$/;
|
|
71
|
+
const assertRoutePath = (path, where) => {
|
|
72
|
+
const problem = !PATH_PATTERN.test(path)
|
|
73
|
+
? "must start with / and use only letters, digits and / _ - : * ."
|
|
74
|
+
: path.includes("..")
|
|
75
|
+
? "may not contain '..'"
|
|
76
|
+
: path.split("/").some((segment) => segment.startsWith(":") && !/^:[A-Za-z_][A-Za-z0-9_]*$/.test(segment))
|
|
77
|
+
? "path parameters are :name with letters, digits or underscore (e.g. :userId)"
|
|
78
|
+
: null;
|
|
79
|
+
if (problem)
|
|
80
|
+
throw new errors_js_1.ToolInputError("MOCK_ROUTE_PATH_INVALID", `${where}: path "${path}" ${problem}.`, { path });
|
|
81
|
+
};
|
|
82
|
+
exports.assertRoutePath = assertRoutePath;
|
|
83
|
+
const SHORTHAND = ["statusCode", "headers", "body"];
|
|
84
|
+
const hasShorthand = (route) => SHORTHAND.some((key) => route[key] !== undefined);
|
|
85
|
+
// A route as the server's createRouteSchema takes it.
|
|
86
|
+
const toServerRoute = (input, where) => {
|
|
87
|
+
(0, exports.assertRoutePath)(input.path, where);
|
|
88
|
+
if (input.response && hasShorthand(input)) {
|
|
89
|
+
throw new errors_js_1.ToolInputError("MOCK_ROUTE_RESPONSE_CONFLICT", `${where}: give either \`response\` or the shorthand statusCode / headers / body, not both.`);
|
|
90
|
+
}
|
|
91
|
+
const response = input.response ?? { type: "static", statusCode: input.statusCode ?? 200, headers: input.headers ?? {}, ...(input.body !== undefined ? { body: input.body } : {}) };
|
|
92
|
+
return {
|
|
93
|
+
method: input.method,
|
|
94
|
+
path: input.path,
|
|
95
|
+
...(input.name ? { name: input.name } : {}),
|
|
96
|
+
response,
|
|
97
|
+
...(input.variants ? { variants: input.variants } : {}),
|
|
98
|
+
behavior: { delay: input.delay ?? { mode: "none" } },
|
|
99
|
+
...(input.requestValidation ? { requestValidation: input.requestValidation } : {}),
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
exports.toServerRoute = toServerRoute;
|
|
103
|
+
// A patch as the server's updateRouteSchema takes it. Shorthand fields are
|
|
104
|
+
// merged over the route's current static/template response so that
|
|
105
|
+
// `{ statusCode: 404 }` alone keeps the body.
|
|
106
|
+
const toServerRoutePatch = (patch, current, where) => {
|
|
107
|
+
if (patch.path !== undefined)
|
|
108
|
+
(0, exports.assertRoutePath)(patch.path, where);
|
|
109
|
+
if (patch.response && hasShorthand(patch)) {
|
|
110
|
+
throw new errors_js_1.ToolInputError("MOCK_ROUTE_RESPONSE_CONFLICT", `${where}: give either \`response\` or the shorthand statusCode / headers / body, not both.`);
|
|
111
|
+
}
|
|
112
|
+
const body = {};
|
|
113
|
+
if (patch.method !== undefined)
|
|
114
|
+
body.method = patch.method;
|
|
115
|
+
if (patch.path !== undefined)
|
|
116
|
+
body.path = patch.path;
|
|
117
|
+
if (patch.name !== undefined)
|
|
118
|
+
body.name = patch.name;
|
|
119
|
+
if (patch.response)
|
|
120
|
+
body.response = patch.response;
|
|
121
|
+
else if (hasShorthand(patch)) {
|
|
122
|
+
const base = current.response.type === "static" || current.response.type === "template" ? current.response : { type: "static", statusCode: 200, headers: {} };
|
|
123
|
+
if (current.response.type === "builder") {
|
|
124
|
+
throw new errors_js_1.ToolInputError("MOCK_ROUTE_RESPONSE_CONFLICT", `${where}: this route uses a builder response; pass a complete \`response\` to replace it.`);
|
|
125
|
+
}
|
|
126
|
+
body.response = { ...base, ...(patch.statusCode !== undefined ? { statusCode: patch.statusCode } : {}), ...(patch.headers !== undefined ? { headers: patch.headers } : {}), ...(patch.body !== undefined ? { body: patch.body } : {}) };
|
|
127
|
+
}
|
|
128
|
+
if (patch.variants !== undefined)
|
|
129
|
+
body.variants = patch.variants;
|
|
130
|
+
if (patch.delay !== undefined)
|
|
131
|
+
body.behavior = { delay: patch.delay };
|
|
132
|
+
if (patch.requestValidation !== undefined)
|
|
133
|
+
body.requestValidation = patch.requestValidation ?? { mode: "any" };
|
|
134
|
+
if (Object.keys(body).length === 0)
|
|
135
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", `${where}: no field to change was given.`);
|
|
136
|
+
return body;
|
|
137
|
+
};
|
|
138
|
+
exports.toServerRoutePatch = toServerRoutePatch;
|
|
139
|
+
const duplicateRoutes = (routes) => {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const duplicates = [];
|
|
142
|
+
for (const route of routes) {
|
|
143
|
+
const key = `${route.method} ${route.path}`;
|
|
144
|
+
if (seen.has(key))
|
|
145
|
+
duplicates.push(key);
|
|
146
|
+
seen.add(key);
|
|
147
|
+
}
|
|
148
|
+
return duplicates;
|
|
149
|
+
};
|
|
150
|
+
exports.duplicateRoutes = duplicateRoutes;
|
|
151
|
+
// ── views ───────────────────────────────────────────────────────────────
|
|
152
|
+
// The summary rows of GET /api/mocks come without routes and state, so their
|
|
153
|
+
// routeCount / stateSizeBytes are always 0 — a list shows neither (get_mock does).
|
|
154
|
+
const mockListView = (mock) => ({
|
|
155
|
+
mockId: mock.mockId,
|
|
156
|
+
id: mock.id,
|
|
157
|
+
name: mock.name,
|
|
158
|
+
...(mock.description ? { description: mock.description } : {}),
|
|
159
|
+
auth: mock.auth.type,
|
|
160
|
+
publicUrl: mock.publicUrl,
|
|
161
|
+
requestCount: mock.requestCount,
|
|
162
|
+
expiresAt: mock.expiresAt,
|
|
163
|
+
expired: Date.parse(mock.expiresAt) < Date.now(),
|
|
164
|
+
createdAt: mock.createdAt,
|
|
165
|
+
});
|
|
166
|
+
exports.mockListView = mockListView;
|
|
167
|
+
const mockSummaryView = (mock) => ({
|
|
168
|
+
...(0, exports.mockListView)(mock),
|
|
169
|
+
routeCount: mock.routeCount,
|
|
170
|
+
stateSizeBytes: mock.stateSizeBytes,
|
|
171
|
+
});
|
|
172
|
+
exports.mockSummaryView = mockSummaryView;
|
|
173
|
+
// The server announces its own public address (MOCK_PUBLIC_BASE_URL); an
|
|
174
|
+
// installation that left it at the default announces a host nobody can reach.
|
|
175
|
+
// When it differs from the API address we were configured with, offer the
|
|
176
|
+
// address that certainly answers from here.
|
|
177
|
+
const alternativeUrl = (apiUrl, mock) => {
|
|
178
|
+
try {
|
|
179
|
+
if (new URL(mock.publicUrl).origin === new URL(apiUrl).origin)
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
return { alternativeUrl: `${apiUrl.replace(/\/+$/, "")}/mock/${mock.mockId}`, urlNote: "publicUrl is the address the server announces; if it does not answer from here, the same mock is served at alternativeUrl (the API address)." };
|
|
186
|
+
};
|
|
187
|
+
exports.alternativeUrl = alternativeUrl;
|
|
188
|
+
const routeView = (route) => ({
|
|
189
|
+
routeId: route.routeId,
|
|
190
|
+
method: route.method,
|
|
191
|
+
path: route.path,
|
|
192
|
+
...(route.name ? { name: route.name } : {}),
|
|
193
|
+
response: route.response,
|
|
194
|
+
...(route.variants?.length ? { variants: route.variants } : {}),
|
|
195
|
+
delay: route.behavior?.delay ?? { mode: "none" },
|
|
196
|
+
...(route.requestValidation ? { requestValidation: route.requestValidation } : {}),
|
|
197
|
+
});
|
|
198
|
+
exports.routeView = routeView;
|
|
199
|
+
const routeBrief = (route) => ({
|
|
200
|
+
routeId: route.routeId,
|
|
201
|
+
method: route.method,
|
|
202
|
+
path: route.path,
|
|
203
|
+
...(route.name ? { name: route.name } : {}),
|
|
204
|
+
responseType: route.response?.type,
|
|
205
|
+
...(typeof route.response?.statusCode === "number" ? { statusCode: route.response.statusCode } : {}),
|
|
206
|
+
...(route.variants?.length ? { variants: route.variants.length } : {}),
|
|
207
|
+
});
|
|
208
|
+
const ROUTES_INLINE_LIMIT = 128 * 1024;
|
|
209
|
+
// Full routes while they fit, one line per route beyond that.
|
|
210
|
+
const routesView = (routes) => {
|
|
211
|
+
const full = routes.map(exports.routeView);
|
|
212
|
+
if ((0, truncate_js_1.safeStringify)(full).length <= ROUTES_INLINE_LIMIT)
|
|
213
|
+
return { routes: full };
|
|
214
|
+
return { routes: routes.map(routeBrief), routesTruncated: true, routesNote: "Routes are shown without their response bodies (too large); read one with get_mock { route }… or change it with update_mock_route." };
|
|
215
|
+
};
|
|
216
|
+
exports.routesView = routesView;
|
|
217
|
+
const mockDetailView = (mock, apiUrl, auth) => ({
|
|
218
|
+
...(0, exports.mockSummaryView)(mock),
|
|
219
|
+
...(0, exports.alternativeUrl)(apiUrl, mock),
|
|
220
|
+
auth,
|
|
221
|
+
limits: mock.limits,
|
|
222
|
+
...(0, exports.routesView)(mock.routes ?? []),
|
|
223
|
+
});
|
|
224
|
+
exports.mockDetailView = mockDetailView;
|
|
225
|
+
// ── curl ────────────────────────────────────────────────────────────────
|
|
226
|
+
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
227
|
+
const sampleValue = (schema) => {
|
|
228
|
+
const type = schema?.type;
|
|
229
|
+
switch (Array.isArray(type) ? type[0] : type) {
|
|
230
|
+
case "number":
|
|
231
|
+
case "integer":
|
|
232
|
+
return 1;
|
|
233
|
+
case "boolean":
|
|
234
|
+
return true;
|
|
235
|
+
case "array":
|
|
236
|
+
return [];
|
|
237
|
+
case "object":
|
|
238
|
+
return sampleObject(schema);
|
|
239
|
+
default:
|
|
240
|
+
return "example";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const sampleObject = (schema) => {
|
|
244
|
+
const properties = schema?.properties ?? {};
|
|
245
|
+
const required = schema?.required;
|
|
246
|
+
const keys = Array.isArray(required) ? required.filter((key) => typeof key === "string") : Object.keys(properties).slice(0, 5);
|
|
247
|
+
return Object.fromEntries(keys.map((key) => [key, sampleValue(properties[key])]));
|
|
248
|
+
};
|
|
249
|
+
// A request body that the route will accept: from its JSON Schema gate, or a
|
|
250
|
+
// create builder's required fields; `{}` otherwise.
|
|
251
|
+
const sampleBody = (route) => {
|
|
252
|
+
if (route.requestValidation?.mode === "schema")
|
|
253
|
+
return sampleObject(route.requestValidation.schema);
|
|
254
|
+
const config = route.response?.type === "builder" ? route.response.config : undefined;
|
|
255
|
+
if (config?.preset === "create" && Array.isArray(config.requiredFields))
|
|
256
|
+
return Object.fromEntries(config.requiredFields.map((field) => [String(field), "example"]));
|
|
257
|
+
return {};
|
|
258
|
+
};
|
|
259
|
+
const sampleRoutePath = (path) => path
|
|
260
|
+
.split("/")
|
|
261
|
+
.map((segment) => (segment.startsWith(":") ? "1" : segment === "*" || segment === "**" ? "example" : segment))
|
|
262
|
+
.join("/");
|
|
263
|
+
exports.sampleRoutePath = sampleRoutePath;
|
|
264
|
+
const curlLine = (baseUrl, route, token) => {
|
|
265
|
+
const method = route.method === "ANY" ? "GET" : route.method;
|
|
266
|
+
const parts = ["curl -sS", `-X ${method}`];
|
|
267
|
+
if (token !== undefined)
|
|
268
|
+
parts.push(`-H ${shellQuote(`X-Mock-Token: ${token ?? "<token>"}`)}`);
|
|
269
|
+
if (method === "POST" || method === "PUT" || method === "PATCH")
|
|
270
|
+
parts.push("-H 'Content-Type: application/json'", `-d ${shellQuote((0, truncate_js_1.safeStringify)(sampleBody(route)))}`);
|
|
271
|
+
parts.push(shellQuote(`${baseUrl.replace(/\/+$/, "")}${(0, exports.sampleRoutePath)(route.path)}`));
|
|
272
|
+
return parts.join(" ");
|
|
273
|
+
};
|
|
274
|
+
exports.curlLine = curlLine;
|
|
275
|
+
const mockRefArg = (what) => (0, shared_js_1.refArg)(what, "(its name, slug or id)");
|
|
276
|
+
exports.mockRefArg = mockRefArg;
|
|
277
|
+
exports.targetArg = zod_1.z
|
|
278
|
+
.enum(["server", "desktop"])
|
|
279
|
+
.optional()
|
|
280
|
+
.describe('"server" (default): a mock on the Callman server with a public URL. "desktop": a local mock inside the user\'s Callman desktop app, reachable at http://127.0.0.1:<port> on their computer only (needs the app open with "Let AI tools manage local mock APIs on this computer" on).');
|
|
281
|
+
// An argument that belongs to the other target is an error, not a silent no-op.
|
|
282
|
+
const requireTarget = (target, args, only, hint) => {
|
|
283
|
+
const given = Object.entries(args).filter(([, value]) => value !== undefined).map(([key]) => key);
|
|
284
|
+
if (given.length > 0 && target !== only) {
|
|
285
|
+
throw new errors_js_1.ToolInputError("ARG_TARGET_MISMATCH", `${given.join(", ")} ${given.length === 1 ? "applies" : "apply"} to ${only} mocks only — ${hint}`, { target });
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
exports.requireTarget = requireTarget;
|
|
289
|
+
const loadMocks = (ctx, ws) => ctx.cache.getOrSet(`mocks:${ws}`, () => ctx.api.listAllMocks(ws));
|
|
290
|
+
exports.loadMocks = loadMocks;
|
|
291
|
+
// `id` of a MockRef is the slug (mockId) — what every /api/mocks route takes.
|
|
292
|
+
const resolveMock = async (ctx, ws, ref, options = {}) => {
|
|
293
|
+
const mocks = await (0, exports.loadMocks)(ctx, ws);
|
|
294
|
+
const items = mocks.map((summary) => ({ id: summary.mockId, name: summary.name, summary }));
|
|
295
|
+
return (0, resolve_js_1.resolveRef)("mock", ref, items, { aliases: (item) => [item.summary.id], describe: (item) => ({ publicUrl: item.summary.publicUrl }), ...options });
|
|
296
|
+
};
|
|
297
|
+
exports.resolveMock = resolveMock;
|
|
298
|
+
const findMock = (ctx, workspace, ref, options = {}) => (0, lookups_js_1.findInWorkspaces)(ctx, workspace, "mock", ref, (ws) => (0, exports.resolveMock)(ctx, ws.id, ref, options), {
|
|
299
|
+
matchOf: (resolved) => resolved.matched,
|
|
300
|
+
describe: (resolved) => ({ mockId: resolved.item.id, name: resolved.item.name }),
|
|
301
|
+
});
|
|
302
|
+
exports.findMock = findMock;
|
|
303
|
+
// A route of a mock: routeId, "METHOD /path", the path alone (when unique) or its name.
|
|
304
|
+
const resolveRoute = (mock, ref) => {
|
|
305
|
+
const routes = mock.routes ?? [];
|
|
306
|
+
const reference = ref.trim();
|
|
307
|
+
const candidates = () => routes.slice(0, 20).map((route) => ({ routeId: route.routeId, method: route.method, path: route.path, ...(route.name ? { name: route.name } : {}) }));
|
|
308
|
+
const byId = routes.find((route) => route.routeId === reference);
|
|
309
|
+
if (byId)
|
|
310
|
+
return byId;
|
|
311
|
+
const combined = /^(GET|POST|PUT|DELETE|PATCH|ANY)\s+(\S+)$/i.exec(reference);
|
|
312
|
+
if (combined) {
|
|
313
|
+
const hit = routes.find((route) => route.method === combined[1].toUpperCase() && route.path === combined[2]);
|
|
314
|
+
if (hit)
|
|
315
|
+
return hit;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const byPath = routes.filter((route) => route.path === reference);
|
|
319
|
+
if (byPath.length === 1)
|
|
320
|
+
return byPath[0];
|
|
321
|
+
if (byPath.length > 1)
|
|
322
|
+
throw new errors_js_1.ToolInputError("REF_AMBIGUOUS", `Several routes use the path "${reference}" — say "METHOD ${reference}" or pass the routeId.`, { kind: "route", ref: reference, candidates: byPath.map((route) => ({ routeId: route.routeId, method: route.method, path: route.path })) });
|
|
323
|
+
const byName = routes.filter((route) => route.name && (0, ids_js_1.normalizeName)(route.name) === (0, ids_js_1.normalizeName)(reference));
|
|
324
|
+
if (byName.length === 1)
|
|
325
|
+
return byName[0];
|
|
326
|
+
}
|
|
327
|
+
throw new errors_js_1.ToolInputError("REF_NOT_FOUND", `No route of mock "${mock.name}" matches "${reference}".`, { kind: "route", ref: reference, candidates: candidates() });
|
|
328
|
+
};
|
|
329
|
+
exports.resolveRoute = resolveRoute;
|
|
330
|
+
// ── local mocks (the desktop app's own mock servers) ───────────────────
|
|
331
|
+
exports.LOCAL_BODY_MAX_BYTES = 256 * 1024;
|
|
332
|
+
exports.LOCAL_DELAY_MAX_MS = 30_000;
|
|
333
|
+
const localUnsupported = (where, what) => new errors_js_1.ToolInputError("LOCAL_MOCK_FEATURE_UNSUPPORTED", `${where}: local mocks on the desktop answer with static responses only — ${what} is not available.`, {
|
|
334
|
+
hint: 'Use target: "server" for templates, builders, variants, request validation and state, or describe the response as a static one.',
|
|
335
|
+
});
|
|
336
|
+
// A route as the desktop's local mock manager stores it.
|
|
337
|
+
const toLocalRoute = (input, where) => {
|
|
338
|
+
(0, exports.assertRoutePath)(input.path, where);
|
|
339
|
+
if (input.variants?.length)
|
|
340
|
+
throw localUnsupported(where, "variants");
|
|
341
|
+
if (input.requestValidation)
|
|
342
|
+
throw localUnsupported(where, "requestValidation");
|
|
343
|
+
if (input.response && hasShorthand(input)) {
|
|
344
|
+
throw new errors_js_1.ToolInputError("MOCK_ROUTE_RESPONSE_CONFLICT", `${where}: give either \`response\` or the shorthand statusCode / headers / body, not both.`);
|
|
345
|
+
}
|
|
346
|
+
if (input.response && input.response.type !== "static")
|
|
347
|
+
throw localUnsupported(where, `a ${input.response.type} response`);
|
|
348
|
+
const source = input.response ?? { statusCode: input.statusCode ?? 200, headers: input.headers, body: input.body };
|
|
349
|
+
const body = source.body === undefined || source.body === null ? "" : typeof source.body === "string" ? source.body : JSON.stringify(source.body, null, 2);
|
|
350
|
+
if (Buffer.byteLength(body, "utf8") > exports.LOCAL_BODY_MAX_BYTES) {
|
|
351
|
+
throw new errors_js_1.ToolInputError("LOCAL_MOCK_BODY_TOO_LARGE", `${where}: the response body is larger than ${exports.LOCAL_BODY_MAX_BYTES / 1024} KB, the limit of a local mock route.`);
|
|
352
|
+
}
|
|
353
|
+
const delay = input.delay ?? { mode: "none" };
|
|
354
|
+
if ((delay.mode === "fixed" && delay.ms > exports.LOCAL_DELAY_MAX_MS) || (delay.mode === "random" && (delay.maxMs > exports.LOCAL_DELAY_MAX_MS || delay.minMs > delay.maxMs))) {
|
|
355
|
+
throw new errors_js_1.ToolInputError("LOCAL_MOCK_DELAY_INVALID", `${where}: a local mock delay is at most ${exports.LOCAL_DELAY_MAX_MS / 1000} s (random: minMs ≤ maxMs).`);
|
|
356
|
+
}
|
|
357
|
+
return { method: input.method, path: input.path, ...(input.name ? { name: input.name } : {}), statusCode: source.statusCode, headers: source.headers ?? {}, body, delay };
|
|
358
|
+
};
|
|
359
|
+
exports.toLocalRoute = toLocalRoute;
|
|
360
|
+
const localBaseUrl = (mock) => `http://127.0.0.1:${mock.port}`;
|
|
361
|
+
exports.localBaseUrl = localBaseUrl;
|
|
362
|
+
const localRouteView = (route) => ({
|
|
363
|
+
...(route.routeId ? { routeId: route.routeId } : {}),
|
|
364
|
+
method: route.method,
|
|
365
|
+
path: route.path,
|
|
366
|
+
...(route.name ? { name: route.name } : {}),
|
|
367
|
+
statusCode: route.statusCode,
|
|
368
|
+
headers: route.headers,
|
|
369
|
+
body: route.body,
|
|
370
|
+
delay: route.delay,
|
|
371
|
+
});
|
|
372
|
+
exports.localRouteView = localRouteView;
|
|
373
|
+
const localMockView = (mock, options = { routes: true }) => {
|
|
374
|
+
const routes = (mock.routes ?? []).map(exports.localRouteView);
|
|
375
|
+
const inline = (0, truncate_js_1.safeStringify)(routes).length <= ROUTES_INLINE_LIMIT;
|
|
376
|
+
return {
|
|
377
|
+
id: mock.id,
|
|
378
|
+
name: mock.name,
|
|
379
|
+
port: mock.port,
|
|
380
|
+
running: mock.running,
|
|
381
|
+
baseUrl: (0, exports.localBaseUrl)(mock),
|
|
382
|
+
autoStart: mock.autoStart,
|
|
383
|
+
...(mock.source ? { source: mock.source } : {}),
|
|
384
|
+
...(mock.lastError ? { lastError: mock.lastError } : {}),
|
|
385
|
+
requestCount: mock.requestCount ?? 0,
|
|
386
|
+
routeCount: routes.length,
|
|
387
|
+
...(options.routes
|
|
388
|
+
? inline
|
|
389
|
+
? { routes }
|
|
390
|
+
: { routes: routes.map(({ body: _body, headers: _headers, ...rest }) => rest), routesTruncated: true }
|
|
391
|
+
: {}),
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
exports.localMockView = localMockView;
|
|
395
|
+
const localCalls = (mock) => (mock.routes ?? []).map((route) => ({ route: `${route.method} ${route.path}`, curl: (0, exports.curlLine)((0, exports.localBaseUrl)(mock), route, undefined) }));
|
|
396
|
+
exports.localCalls = localCalls;
|
|
397
|
+
const resolveLocalMock = (mocks, ref, options = {}) => (0, resolve_js_1.resolveRef)("local mock", ref, mocks.map((record) => ({ id: record.id, name: record.name, record })), { describe: (item) => ({ port: item.record.port, running: item.record.running }), ...options });
|
|
398
|
+
exports.resolveLocalMock = resolveLocalMock;
|
|
399
|
+
const logView = (entry) => ({
|
|
400
|
+
id: String(entry._id),
|
|
401
|
+
at: entry.createdAt,
|
|
402
|
+
method: entry.request?.method,
|
|
403
|
+
path: entry.request?.path,
|
|
404
|
+
...(entry.request?.query && Object.keys(entry.request.query).length > 0 ? { query: entry.request.query } : {}),
|
|
405
|
+
...(entry.request?.body !== undefined ? { requestBody: (0, truncate_js_1.jsonPreview)(entry.request.body, truncate_js_1.LIMITS.BODY_PREVIEW) } : {}),
|
|
406
|
+
statusCode: entry.response?.statusCode,
|
|
407
|
+
durationMs: entry.response?.durationMs,
|
|
408
|
+
...(entry.response?.body !== undefined ? { responseBody: (0, truncate_js_1.jsonPreview)(entry.response.body, truncate_js_1.LIMITS.BODY_PREVIEW) } : {}),
|
|
409
|
+
matched: entry.matchInfo?.matched ?? false,
|
|
410
|
+
...(entry.matchInfo?.matchedRouteId ? { routeId: entry.matchInfo.matchedRouteId } : {}),
|
|
411
|
+
...(entry.matchInfo?.matchedVariantId ? { variantId: entry.matchInfo.matchedVariantId } : {}),
|
|
412
|
+
...(entry.matchInfo?.failureReason ? { failureReason: entry.matchInfo.failureReason } : {}),
|
|
413
|
+
});
|
|
414
|
+
exports.logView = logView;
|