mockpit 2.0.0 → 3.0.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 +60 -212
- package/bin/demoData.js +2 -2
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +407 -187
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +80 -24
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +289 -52
- package/dist/server/apiViews.js +189 -81
- package/dist/server/app.d.ts +7 -2
- package/dist/server/app.js +2845 -832
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +638 -0
- package/dist/server/codeWorker.js +1 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +4 -4
- package/dist/server/decisions.js +9 -9
- package/dist/server/designGuide.d.ts +5 -5
- package/dist/server/designGuide.js +128 -67
- package/dist/server/embeds.d.ts +3 -1
- package/dist/server/embeds.js +31 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +9 -6
- package/dist/server/feedbackBatch.d.ts +35 -13
- package/dist/server/feedbackBatch.js +111 -47
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +39 -10
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +5 -1
- package/dist/server/mcpHttp.js +16 -11
- package/dist/server/mcpSpec.js +118 -46
- package/dist/server/parts.js +9 -1
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +25 -16
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +25 -19
- package/dist/server/sqlStore.d.ts +112 -96
- package/dist/server/sqlStore.js +906 -172
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +6 -9
- package/dist/server/surfacePage.js +361 -86
- package/dist/server/types.d.ts +226 -72
- package/dist/server/types.js +73 -78
- package/extensions/mockpit.js +89 -72
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +2 -3
- package/guide/topics/asks.md +106 -62
- package/guide/topics/feedback.md +126 -119
- package/guide/topics/html.md +17 -24
- package/guide/topics/http.md +41 -41
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +22 -33
- package/guide/topics/surfaces.md +39 -48
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -3
- package/skills/launch-video/SKILL.md +11 -11
- package/skills/mockpit/SKILL.md +26 -22
- package/viewer/dist/index.html +9 -9
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Search to jump: find a design by anything the user might remember it by, its
|
|
2
|
+
// title, a state, a variant's name or note, a question, or its area. It reads
|
|
3
|
+
// what `listJumpRows` hands it, which never includes surfaces, so a big
|
|
4
|
+
// workspace costs its metadata and not its pages. This project comes first, then
|
|
5
|
+
// the others. Runtime-agnostic (no node imports).
|
|
6
|
+
import { isLive, openAsks } from "./types.js";
|
|
7
|
+
const MAX_HERE = 20;
|
|
8
|
+
const MAX_ELSEWHERE = 12;
|
|
9
|
+
// A design whose variants all carry the word shouldn't push every other design off the list.
|
|
10
|
+
const MAX_PER_DESIGN = 3;
|
|
11
|
+
const SNIPPET = 90;
|
|
12
|
+
// Precision before recall: what a design is called beats what was once said about it.
|
|
13
|
+
const RANK = {
|
|
14
|
+
exact: 0,
|
|
15
|
+
prefix: 1,
|
|
16
|
+
word: 2,
|
|
17
|
+
title: 3,
|
|
18
|
+
state: 4,
|
|
19
|
+
variant: 5,
|
|
20
|
+
area: 6,
|
|
21
|
+
question: 7,
|
|
22
|
+
description: 8,
|
|
23
|
+
};
|
|
24
|
+
// "Cafe" finds "Café", and "food" finds "Food".
|
|
25
|
+
const fold = (s) => s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
26
|
+
function clip(text, term) {
|
|
27
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
28
|
+
if (flat.length <= SNIPPET)
|
|
29
|
+
return flat;
|
|
30
|
+
const at = Math.max(0, fold(flat).indexOf(term));
|
|
31
|
+
const start = at < SNIPPET / 3 ? 0 : at - Math.floor(SNIPPET / 3);
|
|
32
|
+
const cut = flat.slice(start, start + SNIPPET).trim();
|
|
33
|
+
return `${start > 0 ? "…" : ""}${cut}${start + SNIPPET < flat.length ? "…" : ""}`;
|
|
34
|
+
}
|
|
35
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
function allHits(rows, terms, whole) {
|
|
37
|
+
const byDesign = new Map();
|
|
38
|
+
for (const v of rows.variants) {
|
|
39
|
+
const list = byDesign.get(v.designId);
|
|
40
|
+
if (list)
|
|
41
|
+
list.push(v);
|
|
42
|
+
else
|
|
43
|
+
byDesign.set(v.designId, [v]);
|
|
44
|
+
}
|
|
45
|
+
const has = (text) => {
|
|
46
|
+
const f = fold(text);
|
|
47
|
+
return terms.every((t) => f.includes(t));
|
|
48
|
+
};
|
|
49
|
+
const out = [];
|
|
50
|
+
const areasSeen = new Set();
|
|
51
|
+
for (const m of rows.designs) {
|
|
52
|
+
const variants = byDesign.get(m.id) ?? [];
|
|
53
|
+
const base = {
|
|
54
|
+
project: m.project,
|
|
55
|
+
id: m.id,
|
|
56
|
+
slug: m.slug,
|
|
57
|
+
title: m.title,
|
|
58
|
+
kind: m.kind,
|
|
59
|
+
width: m.widths[0] ?? null,
|
|
60
|
+
area: m.area,
|
|
61
|
+
open: openAsks(m).length,
|
|
62
|
+
designArchived: !!m.archivedAt,
|
|
63
|
+
inUse: variants.some((v) => v.status === "accepted"),
|
|
64
|
+
text: null,
|
|
65
|
+
state: null,
|
|
66
|
+
variant: null,
|
|
67
|
+
archived: false,
|
|
68
|
+
};
|
|
69
|
+
const mine = [];
|
|
70
|
+
const add = (rank, match, extra = {}) => mine.push({ hit: { ...base, match, ...extra }, rank, updatedAt: m.updatedAt });
|
|
71
|
+
const title = fold(m.title);
|
|
72
|
+
if (title === whole)
|
|
73
|
+
add(RANK.exact, "title");
|
|
74
|
+
else if (title.startsWith(whole))
|
|
75
|
+
add(RANK.prefix, "title");
|
|
76
|
+
else if (terms.every((t) => new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRe(t)}`, "u").test(title)))
|
|
77
|
+
add(RANK.word, "title");
|
|
78
|
+
else if (has(m.title) || has(m.slug))
|
|
79
|
+
add(RANK.title, "title");
|
|
80
|
+
for (const state of m.states) {
|
|
81
|
+
if (has(state))
|
|
82
|
+
add(RANK.state, "state", { text: state, state });
|
|
83
|
+
}
|
|
84
|
+
const namesSeen = new Set();
|
|
85
|
+
for (const v of variants) {
|
|
86
|
+
const gone = !isLive(v.status);
|
|
87
|
+
const tail = gone ? 0.5 : 0;
|
|
88
|
+
// A variant named "default" says nothing the design's title doesn't.
|
|
89
|
+
if (v.variant !== "default" && !namesSeen.has(v.variant) && has(v.variant)) {
|
|
90
|
+
namesSeen.add(v.variant);
|
|
91
|
+
add(RANK.variant + tail, "variant", {
|
|
92
|
+
text: v.variant,
|
|
93
|
+
state: v.state,
|
|
94
|
+
variant: v.variant,
|
|
95
|
+
archived: gone,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (v.description && has(v.description)) {
|
|
99
|
+
add(RANK.description + tail, "description", {
|
|
100
|
+
text: clip(v.description, terms[0]),
|
|
101
|
+
state: v.state,
|
|
102
|
+
variant: v.variant,
|
|
103
|
+
archived: gone,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const ask of m.asks) {
|
|
108
|
+
if (has(ask.text)) {
|
|
109
|
+
add(RANK.question, "question", {
|
|
110
|
+
text: clip(ask.text, terms[0]),
|
|
111
|
+
state: ask.state ?? null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
mine.sort((a, b) => a.rank - b.rank);
|
|
116
|
+
out.push(...mine.slice(0, MAX_PER_DESIGN));
|
|
117
|
+
if (m.area && has(m.area) && !areasSeen.has(`${m.project}\n${m.area}`)) {
|
|
118
|
+
areasSeen.add(`${m.project}\n${m.area}`);
|
|
119
|
+
out.push({
|
|
120
|
+
hit: {
|
|
121
|
+
...base,
|
|
122
|
+
id: null,
|
|
123
|
+
slug: null,
|
|
124
|
+
title: m.area,
|
|
125
|
+
kind: null,
|
|
126
|
+
width: null,
|
|
127
|
+
match: "area",
|
|
128
|
+
open: 0,
|
|
129
|
+
designArchived: false,
|
|
130
|
+
inUse: false,
|
|
131
|
+
},
|
|
132
|
+
rank: RANK.area,
|
|
133
|
+
updatedAt: m.updatedAt,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
export function jumpHits(rows, query, project) {
|
|
140
|
+
const whole = fold(query).trim().replace(/\s+/g, " ");
|
|
141
|
+
const terms = whole.split(" ").filter(Boolean);
|
|
142
|
+
if (!terms.length)
|
|
143
|
+
return [];
|
|
144
|
+
const order = (a, b) => a.rank - b.rank || b.updatedAt.localeCompare(a.updatedAt);
|
|
145
|
+
const all = allHits(rows, terms, whole);
|
|
146
|
+
const here = all.filter((h) => h.hit.project === project).sort(order);
|
|
147
|
+
const elsewhere = all.filter((h) => h.hit.project !== project).sort(order);
|
|
148
|
+
return [...here.slice(0, MAX_HERE), ...elsewhere.slice(0, MAX_ELSEWHERE)].map((h) => h.hit);
|
|
149
|
+
}
|
|
150
|
+
// The designs this browser opened last, in that order, as they are called now: a
|
|
151
|
+
// renamed one follows, and a deleted one drops out.
|
|
152
|
+
export function recentHits(rows, ids) {
|
|
153
|
+
const wanted = new Map(ids.map((id, i) => [id, i]));
|
|
154
|
+
const inUse = new Set(rows.variants.filter((v) => v.status === "accepted").map((v) => v.designId));
|
|
155
|
+
return rows.designs
|
|
156
|
+
.filter((m) => wanted.has(m.id))
|
|
157
|
+
.sort((a, b) => wanted.get(a.id) - wanted.get(b.id))
|
|
158
|
+
.map((m) => ({
|
|
159
|
+
project: m.project,
|
|
160
|
+
id: m.id,
|
|
161
|
+
slug: m.slug,
|
|
162
|
+
title: m.title,
|
|
163
|
+
kind: m.kind,
|
|
164
|
+
width: m.widths[0] ?? null,
|
|
165
|
+
area: m.area,
|
|
166
|
+
match: "title",
|
|
167
|
+
text: null,
|
|
168
|
+
state: null,
|
|
169
|
+
variant: null,
|
|
170
|
+
archived: false,
|
|
171
|
+
open: openAsks(m).length,
|
|
172
|
+
designArchived: !!m.archivedAt,
|
|
173
|
+
inUse: inUse.has(m.id),
|
|
174
|
+
}));
|
|
175
|
+
}
|
package/dist/server/knobs.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExplicitKnob, KnobConfig, Knobs, KnobValue,
|
|
1
|
+
import type { ExplicitKnob, KnobConfig, Knobs, KnobValue, Design, Post } from "./types.ts";
|
|
2
2
|
export type Checked<T> = {
|
|
3
3
|
ok: true;
|
|
4
4
|
value: T;
|
|
@@ -10,10 +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,
|
|
13
|
+
export declare function knobFor(path: string, design: Pick<Design, "knobs">, posts: (Pick<Post, "knobs"> & {
|
|
14
14
|
surfaces?: Post["surfaces"];
|
|
15
15
|
})[]): KnobConfig | undefined;
|
|
16
|
-
export declare function checkKnobValues(raw: unknown,
|
|
16
|
+
export declare function checkKnobValues(raw: unknown, design: Pick<Design, "knobs">, posts: (Pick<Post, "knobs"> & {
|
|
17
17
|
surfaces?: Post["surfaces"];
|
|
18
18
|
})[], what: string): Checked<Record<string, KnobValue>>;
|
|
19
19
|
export declare function discreteChoices(config: KnobConfig): number | null;
|
package/dist/server/knobs.js
CHANGED
|
@@ -293,12 +293,12 @@ export function checkKnobValue(path, config, value) {
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
-
// The declared knob for a path: the
|
|
296
|
+
// The declared knob for a path: the design's own, else a variant's override,
|
|
297
297
|
// else the knob a variant's slot implies (fills.ts), so an option can `set` a
|
|
298
298
|
// slot's fill without the agent declaring anything.
|
|
299
|
-
export function knobFor(path,
|
|
300
|
-
if (Object.hasOwn(
|
|
301
|
-
return
|
|
299
|
+
export function knobFor(path, design, posts) {
|
|
300
|
+
if (Object.hasOwn(design.knobs, path))
|
|
301
|
+
return design.knobs[path];
|
|
302
302
|
for (const p of posts)
|
|
303
303
|
if (p.knobs && Object.hasOwn(p.knobs, path))
|
|
304
304
|
return p.knobs[path];
|
|
@@ -311,7 +311,7 @@ export function knobFor(path, mock, posts) {
|
|
|
311
311
|
}
|
|
312
312
|
return undefined;
|
|
313
313
|
}
|
|
314
|
-
export function checkKnobValues(raw,
|
|
314
|
+
export function checkKnobValues(raw, design, posts, what) {
|
|
315
315
|
if (raw === undefined || raw === null)
|
|
316
316
|
return { ok: true, value: {} };
|
|
317
317
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -319,7 +319,7 @@ export function checkKnobValues(raw, mock, posts, what) {
|
|
|
319
319
|
}
|
|
320
320
|
const out = {};
|
|
321
321
|
for (const [path, value] of Object.entries(raw)) {
|
|
322
|
-
const config = knobFor(path,
|
|
322
|
+
const config = knobFor(path, design, posts);
|
|
323
323
|
if (config === undefined)
|
|
324
324
|
return { ok: false, error: `${what}: no knob "${path}" is declared` };
|
|
325
325
|
const checked = checkKnobValue(path, config, value);
|
package/dist/server/mcpHttp.d.ts
CHANGED
|
@@ -11,13 +11,16 @@ export interface McpFlows {
|
|
|
11
11
|
tree: Flow<[query: {
|
|
12
12
|
project?: string;
|
|
13
13
|
}]>;
|
|
14
|
+
decisions: Flow<[query: {
|
|
15
|
+
project?: string;
|
|
16
|
+
}]>;
|
|
14
17
|
get: Flow<[ref: unknown, query: {
|
|
15
18
|
project?: unknown;
|
|
16
19
|
body?: boolean;
|
|
17
20
|
history?: boolean;
|
|
18
21
|
}]>;
|
|
19
22
|
ask: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
20
|
-
|
|
23
|
+
exportDesign: Flow<[
|
|
21
24
|
ref: unknown,
|
|
22
25
|
query: {
|
|
23
26
|
project?: unknown;
|
|
@@ -27,6 +30,7 @@ export interface McpFlows {
|
|
|
27
30
|
ctx: FlowContext
|
|
28
31
|
]>;
|
|
29
32
|
say: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
33
|
+
move: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
|
|
30
34
|
feedback: Flow<[query: {
|
|
31
35
|
session?: unknown;
|
|
32
36
|
project?: unknown;
|
package/dist/server/mcpHttp.js
CHANGED
|
@@ -12,16 +12,16 @@ export function unwrap(result) {
|
|
|
12
12
|
return result.body;
|
|
13
13
|
}
|
|
14
14
|
// `edit` is one verb with two shapes: `rename` or `areaOrder` edits the
|
|
15
|
-
// project, anything else edits the
|
|
15
|
+
// project, anything else edits the design. Shared by both MCP transports' HTTP
|
|
16
16
|
// side and `run`.
|
|
17
17
|
export function editCall(flows, args, ctx, project) {
|
|
18
18
|
if (args.rename === undefined && args.areaOrder === undefined) {
|
|
19
|
-
return flows.edit(args.
|
|
19
|
+
return flows.edit(args.design, args, ctx);
|
|
20
20
|
}
|
|
21
|
-
if (args.
|
|
21
|
+
if (args.design !== undefined) {
|
|
22
22
|
return Promise.resolve({
|
|
23
23
|
status: 400,
|
|
24
|
-
body: { error: '"rename" and "areaOrder" edit the project; edit the
|
|
24
|
+
body: { error: '"rename" and "areaOrder" edit the project; edit the design without them' },
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
if (!project) {
|
|
@@ -63,14 +63,17 @@ export function registerMcp(app, deps) {
|
|
|
63
63
|
case "publish":
|
|
64
64
|
return unwrap(await flows.publish(args, ctx));
|
|
65
65
|
case "ask":
|
|
66
|
-
return unwrap(await flows.ask(args.
|
|
66
|
+
return unwrap(await flows.ask(args.design, args, ctx));
|
|
67
67
|
case "read":
|
|
68
|
-
if (args.
|
|
68
|
+
if (args.decisions === true) {
|
|
69
|
+
return unwrap(await flows.decisions({ project: args.project }));
|
|
70
|
+
}
|
|
71
|
+
if (args.design === undefined && args.tree === true) {
|
|
69
72
|
return unwrap(await flows.tree({ project: args.project }));
|
|
70
73
|
}
|
|
71
|
-
if (args.
|
|
74
|
+
if (args.design === undefined)
|
|
72
75
|
return unwrap(await flows.list({ project: args.project }));
|
|
73
|
-
return unwrap(await flows.get(args.
|
|
76
|
+
return unwrap(await flows.get(args.design, {
|
|
74
77
|
project: args.project,
|
|
75
78
|
body: args.body === true,
|
|
76
79
|
history: args.history === true,
|
|
@@ -78,9 +81,11 @@ export function registerMcp(app, deps) {
|
|
|
78
81
|
case "feedback":
|
|
79
82
|
return unwrap(await flows.feedback({ session: args.session, project: args.project }, ctx));
|
|
80
83
|
case "say":
|
|
81
|
-
return unwrap(await flows.say(args.
|
|
84
|
+
return unwrap(await flows.say(args.design, args, ctx));
|
|
85
|
+
case "move":
|
|
86
|
+
return unwrap(await flows.move(args.design, args, ctx));
|
|
82
87
|
case "export":
|
|
83
|
-
return unwrap(await flows.
|
|
88
|
+
return unwrap(await flows.exportDesign(args.design, args, ctx));
|
|
84
89
|
case "edit":
|
|
85
90
|
return unwrap(await editCall(flows, args, ctx, await namedProject(args)));
|
|
86
91
|
case "upload": {
|
|
@@ -119,7 +124,7 @@ export function registerMcp(app, deps) {
|
|
|
119
124
|
app.post("/mcp", async (c) => {
|
|
120
125
|
const rpc = (id, result) => c.json({ jsonrpc: "2.0", id, result });
|
|
121
126
|
// `?mode=code` is the opt-in codemode catalog for clients without a
|
|
122
|
-
// harness of their own: one `run` tool instead of the
|
|
127
|
+
// harness of their own: one `run` tool instead of the design tools.
|
|
123
128
|
const code = c.req.query("mode") === "code";
|
|
124
129
|
const rpcError = (id, code, message, status = 200) => c.json({ jsonrpc: "2.0", id, error: { code, message } }, status);
|
|
125
130
|
let msg;
|