mockpit 1.1.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 -6
- package/bin/mockpit.js +139 -18
- package/dist/mcp/server.js +30 -10
- package/dist/server/apiViews.d.ts +129 -25
- package/dist/server/apiViews.js +86 -23
- package/dist/server/app.d.ts +2 -1
- package/dist/server/app.js +1258 -200
- package/dist/server/appIcon.d.ts +22 -0
- package/dist/server/appIcon.js +43 -0
- package/dist/server/appIconPng.d.ts +1 -0
- package/dist/server/appIconPng.js +6 -0
- package/dist/server/askNudges.d.ts +3 -0
- package/dist/server/askNudges.js +40 -0
- package/dist/server/base64.d.ts +1 -0
- package/dist/server/base64.js +8 -0
- package/dist/server/decisions.d.ts +23 -0
- package/dist/server/decisions.js +99 -0
- package/dist/server/designGuide.js +53 -16
- 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 +28 -5
- package/dist/server/feedbackBatch.js +78 -11
- package/dist/server/fills.d.ts +41 -0
- package/dist/server/fills.js +328 -0
- package/dist/server/handoff.d.ts +6 -0
- package/dist/server/handoff.js +38 -0
- package/dist/server/index.js +15 -1
- package/dist/server/knobs.d.ts +6 -2
- package/dist/server/knobs.js +13 -4
- package/dist/server/mcpHttp.d.ts +5 -2
- package/dist/server/mcpHttp.js +14 -5
- package/dist/server/mcpSpec.d.ts +21 -13
- package/dist/server/mcpSpec.js +99 -9
- package/dist/server/postSurfaces.js +3 -12
- package/dist/server/richRender.js +3 -1
- package/dist/server/run.js +6 -2
- package/dist/server/runApi.d.ts +1 -1
- package/dist/server/runApi.js +5 -4
- package/dist/server/sqlStore.d.ts +92 -5
- package/dist/server/sqlStore.js +188 -30
- 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 +95 -10
- package/dist/server/types.js +89 -6
- package/dist/server/viewerRequest.d.ts +2 -0
- package/dist/server/viewerRequest.js +14 -0
- package/extensions/mockpit.js +1 -0
- package/guide/topics/asks.md +69 -10
- package/guide/topics/feedback.md +104 -16
- package/guide/topics/html.md +5 -1
- package/guide/topics/http.md +2 -1
- package/guide/topics/knobs.md +4 -3
- package/guide/topics/surfaces.md +32 -16
- package/package.json +4 -13
- package/skills/launch-video/SKILL.md +1 -1
- package/skills/mockpit/SKILL.md +14 -4
- package/viewer/dist/index.html +9 -9
- package/dist/server/slots.d.ts +0 -14
- package/dist/server/slots.js +0 -71
package/README.md
CHANGED
|
@@ -51,11 +51,14 @@ mockpit turns that into a design-decision loop:
|
|
|
51
51
|
panel goes, which of two layouts. The question lives in the mock, not in
|
|
52
52
|
chat; the agent tells you where to look and ends its turn. Options bound to
|
|
53
53
|
a variant or to knob values render as pictures; hovering one previews it on
|
|
54
|
-
the stage, clicking picks it
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
the stage, clicking picks it, and the part it changed is outlined. A
|
|
55
|
+
region the agent drew several ways (a slot: one `data-part`, a `data-fill`
|
|
56
|
+
per alternative) is asked about once; a slot inside a fill is asked only
|
|
57
|
+
while that fill is chosen. Variants the agent forgot to ask about get a
|
|
58
|
+
built-in **Which one?**.
|
|
57
59
|
3. **Tune.** Parts the agent marked are selectable on the stage. Tune lists
|
|
58
|
-
them with the knobs it declared for each,
|
|
60
|
+
them with the knobs it declared for each, a **Borrow** line for a part
|
|
61
|
+
another look draws differently, plus a comment field. Presets
|
|
59
62
|
save a set of tuned values in your browser.
|
|
60
63
|
4. **Send.** Everything above is a draft (it survives a reload) until one
|
|
61
64
|
**Send**. A mock with a single variant and no questions gets **Accept /
|
|
@@ -70,8 +73,12 @@ mockpit turns that into a design-decision loop:
|
|
|
70
73
|
|
|
71
74
|
**Home** lists the project's mocks with a thumbnail, their states and how many
|
|
72
75
|
questions are open, with "Answer next ›" to jump to the first one. Dark and
|
|
73
|
-
light themes, toggled from the top bar. On a
|
|
74
|
-
|
|
76
|
+
light themes, toggled from the top bar. On a phone or an iPad in portrait the
|
|
77
|
+
design is a canvas you pan and pinch, at each width the agent names (or one you
|
|
78
|
+
pick: Phone 390, Tablet 744, Desktop 820, Wide 1280) side by side, and the
|
|
79
|
+
questions float over it in a card you can snap to a corner, a side or the
|
|
80
|
+
bottom, or tuck against an edge. On a desktop, Inspect opens the stage full
|
|
81
|
+
screen to pan and pinch-zoom it.
|
|
75
82
|
|
|
76
83
|
## Quick start
|
|
77
84
|
|
package/bin/mockpit.js
CHANGED
|
@@ -37,7 +37,9 @@ the loop (never blocks on the user):
|
|
|
37
37
|
1. mockpit init once per repo; read the brief (mockpit guide).
|
|
38
38
|
2. mockpit publish once per variant; mark parts with data-part.
|
|
39
39
|
3. A choice is several variants plus one ask that binds them: mockpit ask
|
|
40
|
-
with options bound to variants.
|
|
40
|
+
with options bound to variants. A variant decides everything it differs
|
|
41
|
+
on; don't re-ask it. Visual options carry a variant or knob set. One
|
|
42
|
+
render plus a control: --knobs.
|
|
41
43
|
4. Tell the user in one line where to look, then end your turn. The question
|
|
42
44
|
itself lives in the mock (ask), never in chat. The user answers in the
|
|
43
45
|
browser at their own pace. Never poll.
|
|
@@ -60,11 +62,17 @@ verbs:
|
|
|
60
62
|
alone keeps a surface, a missing id removes it
|
|
61
63
|
--parts <name=file|-> instead: replace just that data-part's element in
|
|
62
64
|
the latest version (repeatable; name#key targets one instance)
|
|
65
|
+
--fills <part#fill=file|-> instead: replace or add one fill of a slot in
|
|
66
|
+
the latest version (repeatable)
|
|
63
67
|
--title <t> mock title
|
|
64
68
|
--kind <k> component|page (default component)
|
|
65
69
|
--knobs <json|file> knobs in tunekit usePane shape, keyed by path
|
|
66
70
|
--from <N> branch from version N
|
|
67
71
|
--prompt <text> what prompted this version
|
|
72
|
+
--note <text> one line on what this variant is, shown beside it
|
|
73
|
+
--widths <list> device widths to show it at, e.g. 390,1280
|
|
74
|
+
--flow <name> [--step <N>] make this screen step N of a flow (0 takes
|
|
75
|
+
it out); with no html it only places the screen
|
|
68
76
|
--project <name> project (default: git remote, else directory name)
|
|
69
77
|
--kit <id> opt the html surface into a kit (repeatable; see "mockpit kits")
|
|
70
78
|
--md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
|
|
@@ -72,7 +80,8 @@ verbs:
|
|
|
72
80
|
--data is a JSON tree)
|
|
73
81
|
mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
|
|
74
82
|
ask the user; bind options to variants
|
|
75
|
-
--option <l[=v]> an option (repeatable); "=variant" binds it to a variant
|
|
83
|
+
--option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
|
|
84
|
+
"=variant@3" to its version 3, "=knob:path=value" sets one knob
|
|
76
85
|
--scope <s> mock|state|part (default mock)
|
|
77
86
|
--state <label> the state a state-scoped ask is about
|
|
78
87
|
--part <name> the part a part-scoped ask is about
|
|
@@ -153,6 +162,7 @@ mockpit icons remove <set> [<set>...]
|
|
|
153
162
|
publish: `mockpit publish --mock <slug> --html <file> [options]
|
|
154
163
|
mockpit publish --mock <slug> --surfaces <json|file> [options]
|
|
155
164
|
mockpit publish --mock <slug> --parts <name=file|-> ... [options]
|
|
165
|
+
mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
|
|
156
166
|
Creates the mock, state or variant, or the next version of an existing one.
|
|
157
167
|
--state <label> state in the user's words (omit for a single-state mock)
|
|
158
168
|
--variant <name> variant label (default "default")
|
|
@@ -160,30 +170,48 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
|
|
|
160
170
|
a missing id removes it, order is the list order
|
|
161
171
|
--parts <name=file|-> replace only the element carrying data-part="name"
|
|
162
172
|
(name#key for one instance) in the latest version; repeatable
|
|
173
|
+
--fills <part#fill=file|-> replace the element carrying data-part="part"
|
|
174
|
+
data-fill="fill" in the latest version, or add it after
|
|
175
|
+
that slot's other fills; the marks are added if missing
|
|
163
176
|
--title <t> mock title
|
|
164
177
|
--kind <k> component|page (default component)
|
|
165
178
|
--knobs <json|file> knobs in tunekit usePane shape
|
|
166
179
|
--from <N> branch from version N
|
|
180
|
+
--fork <v[@N]> start this new variant from sibling v (its version N, else
|
|
181
|
+
its latest); alone it copies v, with --parts/--fills it
|
|
182
|
+
edits the copy
|
|
167
183
|
--prompt <text> what prompted this version
|
|
184
|
+
--note <text> one line on what this variant is, shown beside it (never
|
|
185
|
+
inside the design)
|
|
186
|
+
--widths <list> device widths the mock is judged at, side by side, e.g.
|
|
187
|
+
390,1280 (the ones the user ships to)
|
|
188
|
+
--flow <name> a flow this screen is a step of: the screens the user moves
|
|
189
|
+
between, in order, across mocks; without html it only
|
|
190
|
+
places the existing screen
|
|
191
|
+
--step <N> its 1-based place in the flow (moves it if it is there);
|
|
192
|
+
0 takes it out; omitted, it joins at the end
|
|
168
193
|
--project <name> project (default: git remote, else directory name)
|
|
169
194
|
--md/--mermaid/--diff/--terminal/--data/--code/--image <file> more surfaces
|
|
170
195
|
--json / --quiet
|
|
171
196
|
Prints the parts found per state, parts that vanished or were renamed,
|
|
172
197
|
nudges, a ready-to-run "mockpit ask" when variants have no ask, and feedback.`,
|
|
173
198
|
ask: `mockpit ask --mock <slug> "<question>" --option <label[=variant]> ... [--scope mock|state|part]
|
|
174
|
-
[--state s] [--part p] [--multi] [--id id]
|
|
199
|
+
[--state s] [--part p] [--multi] [--id id] [--after <ask id>]
|
|
175
200
|
mockpit ask --mock <slug> --asks <json|file>
|
|
176
201
|
Ask the user. A choice is several variants plus one ask that binds them.
|
|
177
202
|
Then tell the user where to look and end your turn.`,
|
|
178
|
-
read: `mockpit read [slug] [--project <name>] [--body] [--history]
|
|
179
|
-
No slug: one line per mock (states, variants, open asks) plus pending, what
|
|
203
|
+
read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree]
|
|
204
|
+
No slug: one line per mock (states, variants, open asks), one per flow, plus pending, what
|
|
180
205
|
the user is doing now. A slug: the mock's states, variants, asks with
|
|
181
|
-
answers, parts, knobs and
|
|
182
|
-
--body includes surfaces, --history version rows.
|
|
206
|
+
answers, parts, knobs and settled values, plus its pending, as JSON.
|
|
207
|
+
--body includes surfaces, --history version rows. --tree (no slug): the
|
|
208
|
+
project as a tree, each mock's variants with status and forkedFrom, and the
|
|
209
|
+
mocks it uses and is used in, as JSON. Never consumes feedback.`,
|
|
183
210
|
feedback: `mockpit feedback [--project <name>] [--session <id>]
|
|
184
211
|
Returns at once with {feedback, pending} as JSON: one batch per mock the user
|
|
185
212
|
sent since you last heard ({mock, reply: {asks, mix, tuned, comments},
|
|
186
|
-
comments, accepted, archived}
|
|
213
|
+
comments, accepted, archived, dropped}, or {mock, resolved: true, dismissed} when the
|
|
214
|
+
user closed it unanswered), delivered once across feedback, watch and
|
|
187
215
|
every write; and pending ({mock, viewerOpen, draft}) so you can tell the
|
|
188
216
|
user "take your time" or "2 of 3 answered". Run it when the user says they
|
|
189
217
|
answered.`,
|
|
@@ -200,7 +228,8 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
|
|
|
200
228
|
Claude Code, arm it under Monitor after asking, then end the turn).
|
|
201
229
|
Delivery is shared with feedback and every write: each Send arrives once.`,
|
|
202
230
|
export: `mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
|
|
203
|
-
Write index.html + history.json per state to .mockpit/accepted/<mock>/<state
|
|
231
|
+
Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/;
|
|
232
|
+
with slots, index.html is the chosen screen, plus source.html and decisions.json.`,
|
|
204
233
|
upload: `mockpit upload <file> [--kind image|file] [--session <id>]
|
|
205
234
|
Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
|
|
206
235
|
guide: `mockpit guide [--topic <id>] [--project <name>]
|
|
@@ -807,6 +836,7 @@ function printPublished(result, flags) {
|
|
|
807
836
|
console.log(`parts${state ? ` (${state})` : ""}: ${parts.map((p) => p.name).join(", ")}`);
|
|
808
837
|
}
|
|
809
838
|
if (result.applied?.length) console.log(`applied: ${result.applied.join(", ")}`);
|
|
839
|
+
if (result.flow) console.log(flowLine(result.flow));
|
|
810
840
|
const changes = result.partChanges;
|
|
811
841
|
if (changes?.renamed?.length) {
|
|
812
842
|
console.log(`renamed: ${changes.renamed.map((r) => `${r.from} → ${r.to}`).join(", ")}`);
|
|
@@ -819,6 +849,11 @@ function printPublished(result, flags) {
|
|
|
819
849
|
printFeedback(result.feedback);
|
|
820
850
|
}
|
|
821
851
|
|
|
852
|
+
function flowLine(flow) {
|
|
853
|
+
const steps = flow.steps.map((x) => (x.state === null ? x.mock : `${x.mock}/${x.state}`));
|
|
854
|
+
return `flow ${flow.name}: ${steps.length ? steps.join(" → ") : "empty, removed"}`;
|
|
855
|
+
}
|
|
856
|
+
|
|
822
857
|
// Only speak when there IS feedback: an empty line per write is pure token
|
|
823
858
|
// cost, and the cursor guarantees anything pending arrives on some write.
|
|
824
859
|
function printFeedback(feedback) {
|
|
@@ -849,9 +884,15 @@ function parsePublishFlags() {
|
|
|
849
884
|
html: { type: "string" },
|
|
850
885
|
surfaces: { type: "string" },
|
|
851
886
|
parts: { type: "string", multiple: true },
|
|
887
|
+
fills: { type: "string", multiple: true },
|
|
852
888
|
knobs: { type: "string" },
|
|
853
889
|
from: { type: "string" },
|
|
890
|
+
fork: { type: "string" },
|
|
854
891
|
prompt: { type: "string" },
|
|
892
|
+
note: { type: "string" },
|
|
893
|
+
widths: { type: "string" },
|
|
894
|
+
flow: { type: "string" },
|
|
895
|
+
step: { type: "string" },
|
|
855
896
|
kit: { type: "string", multiple: true },
|
|
856
897
|
layout: { type: "string" },
|
|
857
898
|
md: { type: "string", multiple: true },
|
|
@@ -870,18 +911,27 @@ function parsePublishFlags() {
|
|
|
870
911
|
|
|
871
912
|
// `--parts name=file` (repeatable; `name#key` targets one instance): the new
|
|
872
913
|
// outer html of one part, spliced server-side into the latest version.
|
|
873
|
-
|
|
914
|
+
// `--fills part#fill=file` is the same for one alternative of a slot.
|
|
915
|
+
function readPartFlags(values, slug, flag = "parts") {
|
|
874
916
|
if (!values?.length) return undefined;
|
|
875
|
-
const example =
|
|
917
|
+
const example =
|
|
918
|
+
flag === "parts"
|
|
919
|
+
? `mockpit publish --mock ${slug} --parts body=body.html`
|
|
920
|
+
: `mockpit publish --mock ${slug} --fills entry#skeleton=skeleton.html`;
|
|
876
921
|
const parts = {};
|
|
877
922
|
let stdin = false;
|
|
878
923
|
for (const raw of values) {
|
|
879
924
|
const at = raw.indexOf("=");
|
|
880
|
-
if (at <= 0 || at === raw.length - 1)
|
|
925
|
+
if (at <= 0 || at === raw.length - 1) {
|
|
926
|
+
die(
|
|
927
|
+
`--${flag} needs ${flag === "parts" ? "name" : "part#fill"}=file (got "${raw}")`,
|
|
928
|
+
example,
|
|
929
|
+
);
|
|
930
|
+
}
|
|
881
931
|
const name = raw.slice(0, at);
|
|
882
932
|
const file = raw.slice(at + 1);
|
|
883
933
|
if (file === "-") {
|
|
884
|
-
if (stdin) die(
|
|
934
|
+
if (stdin) die(`only one --${flag} can read stdin`, example);
|
|
885
935
|
stdin = true;
|
|
886
936
|
} else if (!existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
|
|
887
937
|
parts[name] = readContent(file);
|
|
@@ -897,6 +947,13 @@ async function publishMock() {
|
|
|
897
947
|
if (from !== undefined && !Number.isInteger(from)) {
|
|
898
948
|
die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
|
|
899
949
|
}
|
|
950
|
+
const step = flags.step === undefined ? undefined : Number(flags.step);
|
|
951
|
+
if (step !== undefined && !(Number.isInteger(step) && step >= 0)) {
|
|
952
|
+
die(
|
|
953
|
+
`--step must be a place in the flow, 1 or more, or 0 (got "${flags.step}")`,
|
|
954
|
+
`mockpit read`,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
900
957
|
const file = flags.html ?? positionals[0];
|
|
901
958
|
const composed =
|
|
902
959
|
file !== undefined || tokens.some((t) => t.kind === "option" && SURFACE_FLAGS.has(t.name));
|
|
@@ -904,16 +961,18 @@ async function publishMock() {
|
|
|
904
961
|
composed && "--html",
|
|
905
962
|
flags.surfaces !== undefined && "--surfaces",
|
|
906
963
|
flags.parts?.length && "--parts",
|
|
964
|
+
flags.fills?.length && "--fills",
|
|
907
965
|
].filter(Boolean);
|
|
908
966
|
if (given.length > 1) {
|
|
909
967
|
die(
|
|
910
|
-
`pass one of --html, --surfaces or --
|
|
968
|
+
`pass one of --html, --surfaces, --parts or --fills, not ${given.join(" and ")}`,
|
|
911
969
|
`mockpit publish --help`,
|
|
912
970
|
);
|
|
913
971
|
}
|
|
914
972
|
// Checked before the session exists so a typo'd path leaves nothing behind on the server.
|
|
915
973
|
if (file && file !== "-" && !existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
|
|
916
974
|
const parts = readPartFlags(flags.parts, slug);
|
|
975
|
+
const fills = readPartFlags(flags.fills, slug, "fills");
|
|
917
976
|
let surfaces =
|
|
918
977
|
flags.surfaces === undefined ? undefined : readJsonFlag("surfaces", flags.surfaces);
|
|
919
978
|
if (surfaces !== undefined && !Array.isArray(surfaces)) {
|
|
@@ -933,7 +992,7 @@ async function publishMock() {
|
|
|
933
992
|
}
|
|
934
993
|
surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
|
|
935
994
|
}
|
|
936
|
-
if (!surfaces && !parts)
|
|
995
|
+
if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined)
|
|
937
996
|
die(`no html for ${slug}`, `mockpit publish --mock ${slug} --html <file>`);
|
|
938
997
|
const body = {
|
|
939
998
|
session,
|
|
@@ -945,9 +1004,20 @@ async function publishMock() {
|
|
|
945
1004
|
kind: flags.kind,
|
|
946
1005
|
...(flags.knobs !== undefined && { knobs: readJsonFlag("knobs", flags.knobs) }),
|
|
947
1006
|
...(from !== undefined && { from }),
|
|
1007
|
+
...(flags.fork !== undefined && { fork: flags.fork }),
|
|
948
1008
|
prompt: flags.prompt,
|
|
1009
|
+
note: flags.note,
|
|
1010
|
+
...(flags.widths !== undefined && {
|
|
1011
|
+
widths: flags.widths
|
|
1012
|
+
.split(",")
|
|
1013
|
+
.filter((w) => w.trim())
|
|
1014
|
+
.map((w) => Number(w)),
|
|
1015
|
+
}),
|
|
1016
|
+
...(flags.flow !== undefined && { flow: flags.flow }),
|
|
1017
|
+
...(step !== undefined && { step }),
|
|
949
1018
|
...(surfaces && { surfaces }),
|
|
950
1019
|
...(parts && { parts }),
|
|
1020
|
+
...(fills && { fills }),
|
|
951
1021
|
};
|
|
952
1022
|
const result = await api(
|
|
953
1023
|
"/api/mocks",
|
|
@@ -958,11 +1028,31 @@ async function publishMock() {
|
|
|
958
1028
|
return result;
|
|
959
1029
|
}
|
|
960
1030
|
|
|
961
|
-
// "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant
|
|
1031
|
+
// "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant;
|
|
1032
|
+
// "Before=quiet@2" to its version 2 ("=@2" names the state's only variant);
|
|
1033
|
+
// "Above=knob:trim.position=top" sets one knob. The value is JSON when it
|
|
1034
|
+
// parses (17, true), else a string, so slider and toggle values keep their type.
|
|
962
1035
|
function parseOption(raw) {
|
|
1036
|
+
const knob = raw.indexOf("=knob:");
|
|
1037
|
+
if (knob > 0) {
|
|
1038
|
+
const pair = raw.slice(knob + "=knob:".length);
|
|
1039
|
+
const eq = pair.indexOf("=");
|
|
1040
|
+
if (eq <= 0)
|
|
1041
|
+
die(`option "${raw}" needs knob:<path>=<value>`, "--option Above=knob:trim.position=top");
|
|
1042
|
+
const text = pair.slice(eq + 1);
|
|
1043
|
+
let value = text;
|
|
1044
|
+
try {
|
|
1045
|
+
value = JSON.parse(text);
|
|
1046
|
+
} catch {}
|
|
1047
|
+
return { label: raw.slice(0, knob), set: { [pair.slice(0, eq)]: value } };
|
|
1048
|
+
}
|
|
963
1049
|
const at = raw.lastIndexOf("=");
|
|
964
1050
|
if (at <= 0) return { label: raw };
|
|
965
|
-
|
|
1051
|
+
const label = raw.slice(0, at);
|
|
1052
|
+
const bound = raw.slice(at + 1);
|
|
1053
|
+
const pin = /^(.*)@(\d+)$/.exec(bound);
|
|
1054
|
+
if (!pin) return { label, variant: bound };
|
|
1055
|
+
return { label, ...(pin[1] ? { variant: pin[1] } : {}), version: Number(pin[2]) };
|
|
966
1056
|
}
|
|
967
1057
|
|
|
968
1058
|
// One feedback batch, one line — for a background monitor's notifications.
|
|
@@ -986,6 +1076,13 @@ function printRun(r) {
|
|
|
986
1076
|
function watchLines(batch) {
|
|
987
1077
|
const lines = [];
|
|
988
1078
|
const name = batch.mock ?? "a mock";
|
|
1079
|
+
if (batch.handoff) lines.push(`mockpit handoff on ${name}: ${batch.handoff}`);
|
|
1080
|
+
if (batch.resolved) {
|
|
1081
|
+
const n = batch.dismissed?.length ?? 0;
|
|
1082
|
+
lines.push(
|
|
1083
|
+
`mockpit resolved ${name}: ${n ? `${n} question${n === 1 ? "" : "s"} dismissed unanswered (${batch.dismissed.map((d) => d.text || d.ask).join("; ")}); don't ask again` : "closed by the user"}`,
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
989
1086
|
if (batch.reply) {
|
|
990
1087
|
const r = batch.reply;
|
|
991
1088
|
const bits = [
|
|
@@ -1004,6 +1101,7 @@ function watchLines(batch) {
|
|
|
1004
1101
|
.join(", ")}`
|
|
1005
1102
|
: "",
|
|
1006
1103
|
r.comments?.length ? `${r.comments.length} comment${r.comments.length === 1 ? "" : "s"}` : "",
|
|
1104
|
+
r.reactions?.length ? r.reactions.join(", ") : "",
|
|
1007
1105
|
r.decision ? r.decision.kind : "",
|
|
1008
1106
|
r.text ?? "",
|
|
1009
1107
|
].filter(Boolean);
|
|
@@ -1320,6 +1418,7 @@ const commands = {
|
|
|
1320
1418
|
part: { type: "string" },
|
|
1321
1419
|
multi: { type: "boolean" },
|
|
1322
1420
|
id: { type: "string" },
|
|
1421
|
+
after: { type: "string" },
|
|
1323
1422
|
asks: { type: "string" },
|
|
1324
1423
|
},
|
|
1325
1424
|
});
|
|
@@ -1357,6 +1456,7 @@ const commands = {
|
|
|
1357
1456
|
...(flags.state && { state: flags.state }),
|
|
1358
1457
|
...(flags.part && { part: flags.part }),
|
|
1359
1458
|
...(flags.multi && { multi: true }),
|
|
1459
|
+
...(flags.after && { after: flags.after }),
|
|
1360
1460
|
},
|
|
1361
1461
|
];
|
|
1362
1462
|
}
|
|
@@ -1380,6 +1480,7 @@ const commands = {
|
|
|
1380
1480
|
`asked on ${result.mock}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
|
|
1381
1481
|
);
|
|
1382
1482
|
}
|
|
1483
|
+
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
1383
1484
|
console.log(`tell the user to look at ${result.url}, then end your turn`);
|
|
1384
1485
|
printFeedback(result.feedback);
|
|
1385
1486
|
},
|
|
@@ -1393,10 +1494,20 @@ const commands = {
|
|
|
1393
1494
|
project: { type: "string" },
|
|
1394
1495
|
body: { type: "boolean" },
|
|
1395
1496
|
history: { type: "boolean" },
|
|
1497
|
+
tree: { type: "boolean" },
|
|
1396
1498
|
},
|
|
1397
1499
|
});
|
|
1398
1500
|
const project = resolveProject(flags).name;
|
|
1399
1501
|
if (positionals.length > 1) die("read takes one mock slug", "mockpit read writer");
|
|
1502
|
+
if (flags.tree) {
|
|
1503
|
+
return out(
|
|
1504
|
+
await api(
|
|
1505
|
+
`/api/projects/${encodeURIComponent(project)}/tree`,
|
|
1506
|
+
{},
|
|
1507
|
+
{ report: "die", fix: `mockpit read --project ${project}` },
|
|
1508
|
+
),
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1400
1511
|
if (positionals.length === 1) {
|
|
1401
1512
|
const slug = slugify(positionals[0]);
|
|
1402
1513
|
return out(
|
|
@@ -1424,9 +1535,10 @@ const commands = {
|
|
|
1424
1535
|
for (const m of mocks) {
|
|
1425
1536
|
const states = m.states.length ? m.states.join(" / ") : "single state";
|
|
1426
1537
|
console.log(
|
|
1427
|
-
` ${m.slug} · ${m.kind} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.open ? ` · ${m.open} open` : ""}`,
|
|
1538
|
+
` ${m.slug} · ${m.kind} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.resolvedAt ? " · resolved" : m.open ? ` · ${m.open} open` : ""}`,
|
|
1428
1539
|
);
|
|
1429
1540
|
}
|
|
1541
|
+
for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
|
|
1430
1542
|
for (const line of pendingLines(list.pending)) console.log(line);
|
|
1431
1543
|
},
|
|
1432
1544
|
async export() {
|
|
@@ -1452,6 +1564,15 @@ const commands = {
|
|
|
1452
1564
|
const dir = join(root, entry.state === null ? "default" : slugify(entry.state));
|
|
1453
1565
|
mkdirSync(dir, { recursive: true });
|
|
1454
1566
|
writeFileSync(join(dir, "index.html"), entry.html ?? "");
|
|
1567
|
+
// The screen with every fill, when index.html is the composed choice.
|
|
1568
|
+
if (entry.source) writeFileSync(join(dir, "source.html"), entry.source);
|
|
1569
|
+
const decided = data.decisions?.states?.[entry.state ?? ""];
|
|
1570
|
+
if (decided) {
|
|
1571
|
+
writeFileSync(
|
|
1572
|
+
join(dir, "decisions.json"),
|
|
1573
|
+
JSON.stringify({ ...decided, knobs: data.decisions.knobs }, null, 2) + "\n",
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1455
1576
|
writeFileSync(
|
|
1456
1577
|
join(dir, "history.json"),
|
|
1457
1578
|
JSON.stringify(
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, STDIO_MCP_CATALOG, STDIO_MCP_TOOLS, STDIO_RUN_CATALOG, STDIO_RUN_TOOLS, toolResult, } from "../server/mcpSpec.js";
|
|
9
|
+
import { MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, STDIO_MCP_CATALOG, STDIO_MCP_TOOLS, STDIO_RUN_CATALOG, STDIO_RUN_TOOLS, toolResult, withFeedbackImages, } from "../server/mcpSpec.js";
|
|
10
10
|
import { RUN_INSTRUCTIONS } from "../server/runApi.js";
|
|
11
11
|
// Point at a deployed instance later by setting MOCKPIT_URL.
|
|
12
12
|
const API = process.env.MOCKPIT_URL ?? "http://localhost:8228";
|
|
@@ -156,6 +156,7 @@ const handlers = {
|
|
|
156
156
|
...args,
|
|
157
157
|
html,
|
|
158
158
|
parts: readPartFiles(args.parts),
|
|
159
|
+
fills: readPartFiles(args.fills),
|
|
159
160
|
session,
|
|
160
161
|
agent: AGENT,
|
|
161
162
|
project: resolveProject(args.project),
|
|
@@ -169,13 +170,15 @@ const handlers = {
|
|
|
169
170
|
project: resolveProject(args.project),
|
|
170
171
|
});
|
|
171
172
|
},
|
|
172
|
-
read: (args) => json(args.mock === undefined
|
|
173
|
-
? `/api/
|
|
174
|
-
:
|
|
175
|
-
project: resolveProject(args.project)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
read: (args) => json(args.mock === undefined && args.tree === true
|
|
174
|
+
? `/api/projects/${enc(resolveProject(args.project))}/tree`
|
|
175
|
+
: args.mock === undefined
|
|
176
|
+
? `/api/mocks${query({ project: resolveProject(args.project) })}`
|
|
177
|
+
: `/api/mocks/${enc(args.mock)}${query({
|
|
178
|
+
project: resolveProject(args.project),
|
|
179
|
+
body: args.body,
|
|
180
|
+
history: args.history,
|
|
181
|
+
})}`),
|
|
179
182
|
// Never waits: the server reads from the session's cursor, shared with
|
|
180
183
|
// piggyback and `mockpit watch`, so each Send is delivered exactly once.
|
|
181
184
|
async feedback(args) {
|
|
@@ -230,18 +233,35 @@ async function run(args) {
|
|
|
230
233
|
body: JSON.stringify({ code, session, project: resolveProject(args.project), agent: AGENT }),
|
|
231
234
|
});
|
|
232
235
|
}
|
|
236
|
+
async function loadImage(ref) {
|
|
237
|
+
const res = await fetch(`${API}/a/${encodeURIComponent(ref.id)}`, {
|
|
238
|
+
headers: TOKEN ? { authorization: `Bearer ${TOKEN}` } : {},
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok)
|
|
241
|
+
return null;
|
|
242
|
+
return {
|
|
243
|
+
data: new Uint8Array(await res.arrayBuffer()),
|
|
244
|
+
mimeType: res.headers.get("content-type") ?? "image/png",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
233
247
|
const server = new McpServer(MCP_SERVER_INFO, {
|
|
234
248
|
instructions: CODE_MODE ? RUN_INSTRUCTIONS : MCP_INSTRUCTIONS,
|
|
235
249
|
});
|
|
236
250
|
if (CODE_MODE) {
|
|
237
251
|
const [{ name, ...config }] = STDIO_RUN_TOOLS;
|
|
238
|
-
server.registerTool(name, config, async (args) =>
|
|
252
|
+
server.registerTool(name, config, async (args) => {
|
|
253
|
+
const envelope = await run(args);
|
|
254
|
+
return withFeedbackImages(runToolResult(envelope), envelope, loadImage);
|
|
255
|
+
});
|
|
239
256
|
}
|
|
240
257
|
else {
|
|
241
258
|
for (const tool of STDIO_MCP_TOOLS) {
|
|
242
259
|
const handler = handlers[tool.name];
|
|
243
260
|
const { name, ...config } = tool;
|
|
244
|
-
server.registerTool(name, config, async (args) =>
|
|
261
|
+
server.registerTool(name, config, async (args) => {
|
|
262
|
+
const value = await handler(args);
|
|
263
|
+
return withFeedbackImages(toolResult(name, value), value, loadImage);
|
|
264
|
+
});
|
|
245
265
|
}
|
|
246
266
|
}
|
|
247
267
|
// Replaces the SDK's own listing (registered above), so stdio advertises the
|