mockpit 1.2.0 → 1.3.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 +13 -8
- package/bin/mockpit.js +116 -16
- package/dist/mcp/server.js +10 -7
- package/dist/server/apiViews.d.ts +118 -17
- package/dist/server/apiViews.js +66 -8
- package/dist/server/app.d.ts +2 -1
- package/dist/server/app.js +917 -144
- package/dist/server/decisions.d.ts +23 -0
- package/dist/server/decisions.js +99 -0
- package/dist/server/designGuide.js +45 -10
- package/dist/server/embeds.d.ts +25 -0
- package/dist/server/embeds.js +118 -0
- package/dist/server/eventLog.js +2 -1
- package/dist/server/feedbackBatch.d.ts +12 -1
- package/dist/server/feedbackBatch.js +20 -3
- package/dist/server/fills.d.ts +41 -0
- package/dist/server/fills.js +328 -0
- package/dist/server/index.js +15 -1
- package/dist/server/knobs.d.ts +6 -2
- package/dist/server/knobs.js +11 -1
- package/dist/server/mcpHttp.d.ts +3 -0
- package/dist/server/mcpHttp.js +3 -0
- package/dist/server/mcpSpec.d.ts +13 -13
- package/dist/server/mcpSpec.js +73 -8
- package/dist/server/postSurfaces.js +3 -12
- package/dist/server/richRender.js +3 -1
- package/dist/server/run.js +4 -0
- package/dist/server/runApi.d.ts +1 -1
- package/dist/server/runApi.js +4 -3
- package/dist/server/sqlStore.d.ts +89 -4
- package/dist/server/sqlStore.js +149 -23
- package/dist/server/surfacePage.d.ts +6 -2
- package/dist/server/surfacePage.js +142 -10
- package/dist/server/themes.d.ts +2 -0
- package/dist/server/themes.js +23 -3
- package/dist/server/types.d.ts +73 -11
- package/dist/server/types.js +42 -2
- package/dist/server/viewerRequest.d.ts +2 -0
- package/dist/server/viewerRequest.js +14 -0
- package/guide/topics/asks.md +50 -3
- package/guide/topics/feedback.md +89 -11
- package/guide/topics/html.md +5 -1
- package/guide/topics/http.md +2 -1
- package/guide/topics/knobs.md +2 -1
- package/guide/topics/surfaces.md +24 -8
- package/package.json +4 -13
- package/skills/launch-video/SKILL.md +1 -1
- package/skills/mockpit/SKILL.md +12 -4
- package/viewer/dist/index.html +8 -8
- package/dist/server/slots.d.ts +0 -14
- package/dist/server/slots.js +0 -71
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// Slots: a part with alternative fills, declared in the agent's own markup.
|
|
2
|
+
//
|
|
3
|
+
// <section data-part="entry" data-fill="dots">…</section>
|
|
4
|
+
// <section data-part="entry" data-fill="skeleton">…</section>
|
|
5
|
+
//
|
|
6
|
+
// Elements sharing a `data-part` name with two or more distinct `data-fill`
|
|
7
|
+
// values make one slot. The first fill in document order is the default unless
|
|
8
|
+
// one carries `data-fill-default`. A fill may hold further slots: the scan
|
|
9
|
+
// records, for each slot, the fill it sits in, so the viewer can ask about it
|
|
10
|
+
// only while that fill is chosen.
|
|
11
|
+
//
|
|
12
|
+
// String-only and runtime-agnostic, like parts.ts: identity is read from the
|
|
13
|
+
// markup, never from a DOM. The scan is a tolerant tag walker, not a parser:
|
|
14
|
+
// it tracks open elements to know containment and each fill's extent, skips
|
|
15
|
+
// comments and raw-text elements, and treats an unclosed element as ending at
|
|
16
|
+
// the next close of an outer one.
|
|
17
|
+
const VOID = new Set([
|
|
18
|
+
"area",
|
|
19
|
+
"base",
|
|
20
|
+
"br",
|
|
21
|
+
"col",
|
|
22
|
+
"embed",
|
|
23
|
+
"hr",
|
|
24
|
+
"img",
|
|
25
|
+
"input",
|
|
26
|
+
"link",
|
|
27
|
+
"meta",
|
|
28
|
+
"param",
|
|
29
|
+
"source",
|
|
30
|
+
"track",
|
|
31
|
+
"wbr",
|
|
32
|
+
]);
|
|
33
|
+
const RAW = new Set(["script", "style", "textarea", "title"]);
|
|
34
|
+
const TOKEN = /<!--[\s\S]*?-->|<\/([A-Za-z][\w:-]*)\s*>|<([A-Za-z][\w:-]*)((?:\s[^<>]*?)?)(\/?)>/g;
|
|
35
|
+
// Longest names first, and each must end at the name's end: `data-fill`
|
|
36
|
+
// must not match the start of `data-fill-label`.
|
|
37
|
+
const ATTR = /\s(data-part-label|data-part|data-fill-label|data-fill-default|data-fill)(?=[\s=/>]|$)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
38
|
+
// A slot's knob path is its part name, so the name must be a valid path segment.
|
|
39
|
+
const PATH_SEGMENT = /^[A-Za-z_][\w-]{0,63}$/;
|
|
40
|
+
const MAX_SLOTS = 100;
|
|
41
|
+
const decode = (s) => s
|
|
42
|
+
.replace(/"/g, '"')
|
|
43
|
+
.replace(/'|'/g, "'")
|
|
44
|
+
.replace(/</g, "<")
|
|
45
|
+
.replace(/>/g, ">")
|
|
46
|
+
.replace(/&/g, "&")
|
|
47
|
+
.trim()
|
|
48
|
+
.slice(0, 200);
|
|
49
|
+
function attributes(raw) {
|
|
50
|
+
const out = {};
|
|
51
|
+
ATTR.lastIndex = 0;
|
|
52
|
+
let m;
|
|
53
|
+
while ((m = ATTR.exec(raw)))
|
|
54
|
+
out[m[1]] = decode(m[2] ?? m[3] ?? m[4] ?? "");
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export function scanFills(html) {
|
|
58
|
+
const { slots, ranges } = scanAll(html);
|
|
59
|
+
const real = slots.filter((s) => s.fills.length >= 2);
|
|
60
|
+
const isSlot = new Set(real.map((s) => s.part));
|
|
61
|
+
return { slots: real, ranges: ranges.filter((r) => isSlot.has(r.part)) };
|
|
62
|
+
}
|
|
63
|
+
// Every part carrying `data-fill`, a lone fill included: a splice may add the
|
|
64
|
+
// second fill that makes it a slot.
|
|
65
|
+
function scanAll(html) {
|
|
66
|
+
const ranges = [];
|
|
67
|
+
// Per part, in document order: each fill element and the fill it sits in.
|
|
68
|
+
const seen = new Map();
|
|
69
|
+
const stack = [];
|
|
70
|
+
const close = (i, end) => {
|
|
71
|
+
for (let j = stack.length - 1; j >= i; j--) {
|
|
72
|
+
const f = stack[j].fill;
|
|
73
|
+
if (f)
|
|
74
|
+
f.end = end;
|
|
75
|
+
}
|
|
76
|
+
stack.length = i;
|
|
77
|
+
};
|
|
78
|
+
TOKEN.lastIndex = 0;
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = TOKEN.exec(html))) {
|
|
81
|
+
if (m[0].startsWith("<!--"))
|
|
82
|
+
continue;
|
|
83
|
+
if (m[1]) {
|
|
84
|
+
const tag = m[1].toLowerCase();
|
|
85
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
86
|
+
if (stack[i].tag === tag) {
|
|
87
|
+
close(i, TOKEN.lastIndex);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const tag = m[2].toLowerCase();
|
|
94
|
+
const attrs = attributes(m[3] ?? "");
|
|
95
|
+
let fill = null;
|
|
96
|
+
const part = attrs["data-part"];
|
|
97
|
+
const name = attrs["data-fill"];
|
|
98
|
+
if (part && name && PATH_SEGMENT.test(part)) {
|
|
99
|
+
fill = { part, fill: name, start: m.index, end: html.length, tagEnd: TOKEN.lastIndex };
|
|
100
|
+
ranges.push(fill);
|
|
101
|
+
let slot = seen.get(part);
|
|
102
|
+
if (!slot && seen.size < MAX_SLOTS) {
|
|
103
|
+
const outer = [...stack].reverse().find((o) => o.fill)?.fill ?? null;
|
|
104
|
+
slot = {
|
|
105
|
+
fills: [],
|
|
106
|
+
defaults: [],
|
|
107
|
+
parent: outer ? { part: outer.part, fill: outer.fill } : null,
|
|
108
|
+
};
|
|
109
|
+
seen.set(part, slot);
|
|
110
|
+
}
|
|
111
|
+
if (slot) {
|
|
112
|
+
if (attrs["data-part-label"] && !slot.label)
|
|
113
|
+
slot.label = attrs["data-part-label"];
|
|
114
|
+
if (!slot.fills.some((f) => f.name === name)) {
|
|
115
|
+
slot.fills.push({
|
|
116
|
+
name,
|
|
117
|
+
...(attrs["data-fill-label"] ? { label: attrs["data-fill-label"] } : {}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if ("data-fill-default" in attrs)
|
|
121
|
+
slot.defaults.push(name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (VOID.has(tag) || m[4] === "/") {
|
|
125
|
+
if (fill)
|
|
126
|
+
fill.end = TOKEN.lastIndex;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (RAW.has(tag)) {
|
|
130
|
+
// Skip to the matching close: markup inside script or style is text.
|
|
131
|
+
const endTag = new RegExp(`</${tag}\\s*>`, "gi");
|
|
132
|
+
endTag.lastIndex = TOKEN.lastIndex;
|
|
133
|
+
const e = endTag.exec(html);
|
|
134
|
+
TOKEN.lastIndex = e ? endTag.lastIndex : html.length;
|
|
135
|
+
if (fill)
|
|
136
|
+
fill.end = TOKEN.lastIndex;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
stack.push({ tag, fill });
|
|
140
|
+
}
|
|
141
|
+
const slots = [];
|
|
142
|
+
for (const [part, s] of seen) {
|
|
143
|
+
slots.push({
|
|
144
|
+
part,
|
|
145
|
+
...(s.label ? { label: s.label } : {}),
|
|
146
|
+
fills: s.fills,
|
|
147
|
+
default: s.defaults[0] ?? s.fills[0].name,
|
|
148
|
+
parent: s.parent,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { slots, ranges };
|
|
152
|
+
}
|
|
153
|
+
// Each slot is a select knob at its part's path: the fills are its options and
|
|
154
|
+
// the default fill is its value. A knob the agent declares at the same path
|
|
155
|
+
// wins (to relabel the options), so these are merged under declared knobs.
|
|
156
|
+
export function slotKnobs(slots) {
|
|
157
|
+
const out = {};
|
|
158
|
+
for (const s of slots) {
|
|
159
|
+
const config = {
|
|
160
|
+
type: "select",
|
|
161
|
+
options: s.fills.map((f) => (f.label ? { value: f.name, label: f.label } : f.name)),
|
|
162
|
+
value: s.default,
|
|
163
|
+
};
|
|
164
|
+
out[s.part] = config;
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
const cssString = (s) => `"${s.replace(/[\\"]/g, (c) => `\\${c}`).replace(/[\n\r<>]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `)}"`;
|
|
169
|
+
// Shows only the chosen fill of each slot, keyed on the `data-k-<part>`
|
|
170
|
+
// attribute the knob runtime keeps on <html>. The chosen fill is never styled:
|
|
171
|
+
// only the others are hidden, so a fill keeps whatever display the agent gave it.
|
|
172
|
+
export function slotCss(slots) {
|
|
173
|
+
const rules = [];
|
|
174
|
+
for (const s of slots) {
|
|
175
|
+
const attr = `data-k-${s.part}`;
|
|
176
|
+
for (const f of s.fills) {
|
|
177
|
+
const el = `[data-part=${cssString(s.part)}][data-fill=${cssString(f.name)}]`;
|
|
178
|
+
const chosen = `[${attr}=${cssString(f.name)}]`;
|
|
179
|
+
rules.push(f.name === s.default
|
|
180
|
+
? `html[${attr}]:not(${chosen}) ${el}{display:none!important}`
|
|
181
|
+
: `html:not(${chosen}) ${el}{display:none!important}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return rules.join("\n");
|
|
185
|
+
}
|
|
186
|
+
const FILL_ATTRS = /\s(data-fill-label|data-fill-default|data-fill)(?=[\s=/>]|$)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
|
|
187
|
+
// The screen as chosen: every slot reduced to its chosen fill, the others cut
|
|
188
|
+
// out with whatever they contained, and the fill attributes dropped from the
|
|
189
|
+
// fills that stay. `chosen` maps a slot's part to a fill; a slot it does not
|
|
190
|
+
// name keeps its default.
|
|
191
|
+
export function composeFills(html, chosen) {
|
|
192
|
+
const { slots, ranges } = scanFills(html);
|
|
193
|
+
const pick = new Map(slots.map((s) => [s.part, chosen[s.part] ?? s.default]));
|
|
194
|
+
const cut = ranges.filter((r) => pick.get(r.part) !== r.fill);
|
|
195
|
+
const kept = ranges.filter((r) => pick.get(r.part) === r.fill);
|
|
196
|
+
const edits = [];
|
|
197
|
+
for (const r of cut) {
|
|
198
|
+
if (cut.some((o) => o !== r && o.start <= r.start && r.end <= o.end))
|
|
199
|
+
continue;
|
|
200
|
+
edits.push({ start: r.start, end: r.end, text: "" });
|
|
201
|
+
}
|
|
202
|
+
for (const r of kept) {
|
|
203
|
+
if (cut.some((o) => o.start <= r.start && r.end <= o.end))
|
|
204
|
+
continue;
|
|
205
|
+
edits.push({
|
|
206
|
+
start: r.start,
|
|
207
|
+
end: r.tagEnd,
|
|
208
|
+
text: html.slice(r.start, r.tagEnd).replace(FILL_ATTRS, ""),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
edits.sort((a, b) => b.start - a.start);
|
|
212
|
+
let out = html;
|
|
213
|
+
for (const e of edits)
|
|
214
|
+
out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
// A post's slots across its html surfaces (the first surface that declares a
|
|
218
|
+
// part wins), cached per surfaces array: a stored post never mutates it.
|
|
219
|
+
const bySurfaces = new WeakMap();
|
|
220
|
+
export function slotsInSurfaces(surfaces) {
|
|
221
|
+
const hit = bySurfaces.get(surfaces);
|
|
222
|
+
if (hit)
|
|
223
|
+
return hit;
|
|
224
|
+
const out = [];
|
|
225
|
+
for (const sf of surfaces) {
|
|
226
|
+
if (sf.kind !== "html")
|
|
227
|
+
continue;
|
|
228
|
+
for (const slot of scanFills(sf.html).slots) {
|
|
229
|
+
if (!out.some((o) => o.part === slot.part))
|
|
230
|
+
out.push(slot);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
bySurfaces.set(surfaces, out);
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
const attrValue = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
237
|
+
const FIRST_TAG = /^(?:\s|<!--[\s\S]*?-->)*<([A-Za-z][\w:-]*)((?:\s[^<>]*?)?)(\/?)>/;
|
|
238
|
+
// The replacement must be one element marked as that fill: the marks are added
|
|
239
|
+
// when missing and a contradicting one is refused, so a splice cannot quietly
|
|
240
|
+
// land in another slot.
|
|
241
|
+
function markFill(html, part, fill) {
|
|
242
|
+
const m = FIRST_TAG.exec(html);
|
|
243
|
+
if (!m)
|
|
244
|
+
return { error: `fill "${part}#${fill}" must be html starting with its element` };
|
|
245
|
+
const attrs = attributes(m[2] ?? "");
|
|
246
|
+
if (attrs["data-part"] !== undefined && attrs["data-part"] !== part) {
|
|
247
|
+
return { error: `fill "${part}#${fill}" is marked data-part="${attrs["data-part"]}"` };
|
|
248
|
+
}
|
|
249
|
+
if (attrs["data-fill"] !== undefined && attrs["data-fill"] !== fill) {
|
|
250
|
+
return { error: `fill "${part}#${fill}" is marked data-fill="${attrs["data-fill"]}"` };
|
|
251
|
+
}
|
|
252
|
+
const add = (attrs["data-part"] === undefined ? ` data-part="${attrValue(part)}"` : "") +
|
|
253
|
+
(attrs["data-fill"] === undefined ? ` data-fill="${attrValue(fill)}"` : "");
|
|
254
|
+
const at = m.index + m[0].indexOf("<") + 1 + m[1].length;
|
|
255
|
+
return html.slice(0, at) + add + html.slice(at);
|
|
256
|
+
}
|
|
257
|
+
// Replace one fill of a slot, or add it after the slot's last fill, keyed
|
|
258
|
+
// "part#fill". Like spliceParts, every target is located in the original
|
|
259
|
+
// markup and overlapping edits are refused.
|
|
260
|
+
export function spliceFills(surfaces, edits) {
|
|
261
|
+
const docs = surfaces.flatMap((s, i) => (s.kind === "html" ? [{ i, surface: s }] : []));
|
|
262
|
+
if (docs.length === 0) {
|
|
263
|
+
return { ok: false, error: "fills needs an html surface; this variant has none" };
|
|
264
|
+
}
|
|
265
|
+
const scans = docs.map((d) => scanAll(d.surface.html).ranges);
|
|
266
|
+
const plan = new Map();
|
|
267
|
+
const targets = Object.keys(edits);
|
|
268
|
+
for (const [n, target] of targets.entries()) {
|
|
269
|
+
const at = target.indexOf("#");
|
|
270
|
+
const part = at > 0 ? target.slice(0, at) : "";
|
|
271
|
+
const fill = at > 0 ? target.slice(at + 1).trim() : "";
|
|
272
|
+
if (!PATH_SEGMENT.test(part) || !fill) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
error: `fills keys are "part#fill", e.g. "entry#skeleton" (got "${target}")`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const where = scans.flatMap((rs, d) => (rs.some((r) => r.part === part) ? [d] : []));
|
|
279
|
+
if (where.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
error: `no fills of "${part}"; mark its alternatives data-part="${part}" data-fill="<name>" with html or parts first`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (where.length > 1) {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: `fills of "${part}" are in several html surfaces; revise with surfaces`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const d = where[0];
|
|
292
|
+
const own = scans[d].filter((r) => r.part === part);
|
|
293
|
+
const same = own.filter((r) => r.fill === fill);
|
|
294
|
+
if (same.length > 1) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
error: `fill "${target}" appears ${same.length} times; revise with html or parts`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const text = markFill(edits[target], part, fill);
|
|
301
|
+
if (typeof text !== "string")
|
|
302
|
+
return { ok: false, error: text.error };
|
|
303
|
+
const edit = same.length
|
|
304
|
+
? { start: same[0].start, end: same[0].end, text, n }
|
|
305
|
+
: (() => {
|
|
306
|
+
const end = Math.max(...own.map((r) => r.end));
|
|
307
|
+
return { start: end, end, text: `\n${text}`, n };
|
|
308
|
+
})();
|
|
309
|
+
const list = plan.get(d) ?? [];
|
|
310
|
+
const clash = list.find((e) => e.start < edit.end && edit.start < e.end);
|
|
311
|
+
if (clash) {
|
|
312
|
+
return { ok: false, error: `fills "${targets[clash.n]}" and "${target}" overlap; send one` };
|
|
313
|
+
}
|
|
314
|
+
list.push(edit);
|
|
315
|
+
plan.set(d, list);
|
|
316
|
+
}
|
|
317
|
+
const next = [...surfaces];
|
|
318
|
+
for (const [d, list] of plan) {
|
|
319
|
+
const { i, surface } = docs[d];
|
|
320
|
+
let html = surface.html;
|
|
321
|
+
// From the end, and same-place additions last-listed first, so they land in listed order.
|
|
322
|
+
for (const e of list.toSorted((a, b) => b.start - a.start || b.n - a.n)) {
|
|
323
|
+
html = html.slice(0, e.start) + e.text + html.slice(e.end);
|
|
324
|
+
}
|
|
325
|
+
next[i] = { ...surface, html };
|
|
326
|
+
}
|
|
327
|
+
return { ok: true, surfaces: next, applied: targets };
|
|
328
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serve } from "@hono/node-server";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { basename, dirname, join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -25,6 +26,18 @@ const [viewerHtml, setupText, topicTexts, pkgJson] = await Promise.all([
|
|
|
25
26
|
readFile(join(root, "package.json"), "utf8"),
|
|
26
27
|
]);
|
|
27
28
|
const topics = Object.fromEntries(GUIDE_TOPICS.map((id, i) => [id, topicTexts[i]]));
|
|
29
|
+
// What renders surface documents: this server's own files and the locked
|
|
30
|
+
// library versions. Hashed whole rather than picked apart, so an edit in dev or
|
|
31
|
+
// an upgrade both reach browsers holding an old document; a change elsewhere
|
|
32
|
+
// in the server only costs one re-fetch per document.
|
|
33
|
+
const serverDir = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const renderHash = createHash("sha256");
|
|
35
|
+
for (const name of (await readdir(serverDir)).sort()) {
|
|
36
|
+
if (/\.(ts|js)$/.test(name))
|
|
37
|
+
renderHash.update(await readFile(join(serverDir, name)));
|
|
38
|
+
}
|
|
39
|
+
renderHash.update(await readFile(join(root, "package-lock.json")).catch(() => pkgJson));
|
|
40
|
+
const renderRev = renderHash.digest("hex").slice(0, 10);
|
|
28
41
|
const pr = process.env.MOCKPIT_PUBLIC_READ;
|
|
29
42
|
const publicRead = pr === "session" || pr === "full" ? pr : undefined;
|
|
30
43
|
// Storage: SQLite via node:sqlite, the same SqlStore the Cloudflare Durable
|
|
@@ -55,6 +68,7 @@ const app = createApp({
|
|
|
55
68
|
// notice); set it to the empty string to disable the update check
|
|
56
69
|
version: process.env.MOCKPIT_VERSION ?? JSON.parse(pkgJson).version,
|
|
57
70
|
upgradeCommand: "npm install -g mockpit",
|
|
71
|
+
renderRev,
|
|
58
72
|
// `run` scripts execute in QuickJS inside a worker thread per run.
|
|
59
73
|
executor: createNodeExecutor(),
|
|
60
74
|
log: logPath ? createFileLog(logPath) : undefined,
|
package/dist/server/knobs.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export declare function explicitKnob(config: KnobConfig): ExplicitKnob;
|
|
|
10
10
|
export declare function checkKnobConfig(path: string, raw: unknown): Checked<KnobConfig>;
|
|
11
11
|
export declare function checkKnobs(raw: unknown): Checked<Knobs>;
|
|
12
12
|
export declare function checkKnobValue(path: string, config: KnobConfig, value: unknown): Checked<KnobValue>;
|
|
13
|
-
export declare function knobFor(path: string, mock: Pick<Mock, "knobs">, posts: Pick<Post, "knobs">
|
|
14
|
-
|
|
13
|
+
export declare function knobFor(path: string, mock: Pick<Mock, "knobs">, posts: (Pick<Post, "knobs"> & {
|
|
14
|
+
surfaces?: Post["surfaces"];
|
|
15
|
+
})[]): KnobConfig | undefined;
|
|
16
|
+
export declare function checkKnobValues(raw: unknown, mock: Pick<Mock, "knobs">, posts: (Pick<Post, "knobs"> & {
|
|
17
|
+
surfaces?: Post["surfaces"];
|
|
18
|
+
})[], what: string): Checked<Record<string, KnobValue>>;
|
|
15
19
|
export declare function discreteChoices(config: KnobConfig): number | null;
|
package/dist/server/knobs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Knob schema and value validation (plan Q3: values are checked against the
|
|
2
2
|
// declared schema before they are stored or used). Runtime-agnostic. The config
|
|
3
3
|
// shape mirrors tunekit's usePane config; see the knob types in types.ts.
|
|
4
|
+
import { slotKnobs, slotsInSurfaces } from "./fills.js";
|
|
4
5
|
const MAX_KNOBS = 100;
|
|
5
6
|
const MAX_OPTIONS = 100;
|
|
6
7
|
const MAX_STRING = 500;
|
|
@@ -292,13 +293,22 @@ export function checkKnobValue(path, config, value) {
|
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
}
|
|
295
|
-
// The declared knob for a path: the mock's own, else a variant's override
|
|
296
|
+
// The declared knob for a path: the mock's own, else a variant's override,
|
|
297
|
+
// else the knob a variant's slot implies (fills.ts), so an option can `set` a
|
|
298
|
+
// slot's fill without the agent declaring anything.
|
|
296
299
|
export function knobFor(path, mock, posts) {
|
|
297
300
|
if (Object.hasOwn(mock.knobs, path))
|
|
298
301
|
return mock.knobs[path];
|
|
299
302
|
for (const p of posts)
|
|
300
303
|
if (p.knobs && Object.hasOwn(p.knobs, path))
|
|
301
304
|
return p.knobs[path];
|
|
305
|
+
for (const p of posts) {
|
|
306
|
+
if (!p.surfaces)
|
|
307
|
+
continue;
|
|
308
|
+
const implied = slotKnobs(slotsInSurfaces(p.surfaces));
|
|
309
|
+
if (Object.hasOwn(implied, path))
|
|
310
|
+
return implied[path];
|
|
311
|
+
}
|
|
302
312
|
return undefined;
|
|
303
313
|
}
|
|
304
314
|
export function checkKnobValues(raw, mock, posts, what) {
|
package/dist/server/mcpHttp.d.ts
CHANGED
package/dist/server/mcpHttp.js
CHANGED
|
@@ -35,6 +35,9 @@ export function registerMcp(app, deps) {
|
|
|
35
35
|
case "ask":
|
|
36
36
|
return unwrap(await flows.ask(args.mock, args, ctx));
|
|
37
37
|
case "read":
|
|
38
|
+
if (args.mock === undefined && args.tree === true) {
|
|
39
|
+
return unwrap(await flows.tree({ project: args.project }));
|
|
40
|
+
}
|
|
38
41
|
if (args.mock === undefined)
|
|
39
42
|
return unwrap(await flows.list({ project: args.project }));
|
|
40
43
|
return unwrap(await flows.get(args.mock, {
|
package/dist/server/mcpSpec.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ type Param = {
|
|
|
53
53
|
d?: string;
|
|
54
54
|
req?: boolean;
|
|
55
55
|
} | {
|
|
56
|
-
t: "surfaces" | "asks" | "object";
|
|
56
|
+
t: "surfaces" | "asks" | "object" | "integers";
|
|
57
57
|
d?: string;
|
|
58
58
|
req?: boolean;
|
|
59
59
|
};
|
|
@@ -131,9 +131,9 @@ export declare const STDIO_MCP_TOOLS: ({
|
|
|
131
131
|
_meta: {
|
|
132
132
|
"anthropic/alwaysLoad": boolean;
|
|
133
133
|
};
|
|
134
|
-
outputSchema?: z.ZodObject<
|
|
135
|
-
[
|
|
136
|
-
}
|
|
134
|
+
outputSchema?: z.ZodObject<{
|
|
135
|
+
[x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
136
|
+
}, z.core.$loose> | undefined;
|
|
137
137
|
name: string;
|
|
138
138
|
description: string;
|
|
139
139
|
inputSchema: Readonly<{
|
|
@@ -141,9 +141,9 @@ export declare const STDIO_MCP_TOOLS: ({
|
|
|
141
141
|
}>;
|
|
142
142
|
} | {
|
|
143
143
|
_meta?: undefined;
|
|
144
|
-
outputSchema?: z.ZodObject<
|
|
145
|
-
[
|
|
146
|
-
}
|
|
144
|
+
outputSchema?: z.ZodObject<{
|
|
145
|
+
[x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
146
|
+
}, z.core.$loose> | undefined;
|
|
147
147
|
name: string;
|
|
148
148
|
description: string;
|
|
149
149
|
inputSchema: Readonly<{
|
|
@@ -154,9 +154,9 @@ export declare const STDIO_RUN_TOOLS: ({
|
|
|
154
154
|
_meta: {
|
|
155
155
|
"anthropic/alwaysLoad": boolean;
|
|
156
156
|
};
|
|
157
|
-
outputSchema?: z.ZodObject<
|
|
158
|
-
[
|
|
159
|
-
}
|
|
157
|
+
outputSchema?: z.ZodObject<{
|
|
158
|
+
[x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
159
|
+
}, z.core.$loose> | undefined;
|
|
160
160
|
name: string;
|
|
161
161
|
description: string;
|
|
162
162
|
inputSchema: Readonly<{
|
|
@@ -164,9 +164,9 @@ export declare const STDIO_RUN_TOOLS: ({
|
|
|
164
164
|
}>;
|
|
165
165
|
} | {
|
|
166
166
|
_meta?: undefined;
|
|
167
|
-
outputSchema?: z.ZodObject<
|
|
168
|
-
[
|
|
169
|
-
}
|
|
167
|
+
outputSchema?: z.ZodObject<{
|
|
168
|
+
[x: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
169
|
+
}, z.core.$loose> | undefined;
|
|
170
170
|
name: string;
|
|
171
171
|
description: string;
|
|
172
172
|
inputSchema: Readonly<{
|