mockpit 0.0.0-stage → 1.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.
Files changed (82) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +232 -2
  3. package/bin/demoData.js +132 -0
  4. package/bin/initDesign.js +846 -0
  5. package/bin/mockpit.js +1830 -0
  6. package/bin/serveUrl.d.ts +1 -0
  7. package/bin/serveUrl.js +7 -0
  8. package/dist/mcp/server.d.ts +2 -0
  9. package/dist/mcp/server.js +230 -0
  10. package/dist/server/apiViews.d.ts +329 -0
  11. package/dist/server/apiViews.js +135 -0
  12. package/dist/server/app.d.ts +53 -0
  13. package/dist/server/app.js +3112 -0
  14. package/dist/server/base64.d.ts +1 -0
  15. package/dist/server/base64.js +16 -0
  16. package/dist/server/cdn.d.ts +6 -0
  17. package/dist/server/cdn.js +33 -0
  18. package/dist/server/codeRunner.d.ts +5 -0
  19. package/dist/server/codeRunner.js +89 -0
  20. package/dist/server/codeWorker.d.ts +1 -0
  21. package/dist/server/codeWorker.js +196 -0
  22. package/dist/server/designGuide.d.ts +16 -0
  23. package/dist/server/designGuide.js +311 -0
  24. package/dist/server/events.d.ts +53 -0
  25. package/dist/server/events.js +14 -0
  26. package/dist/server/feedbackBatch.d.ts +43 -0
  27. package/dist/server/feedbackBatch.js +98 -0
  28. package/dist/server/icons.d.ts +50 -0
  29. package/dist/server/icons.js +225 -0
  30. package/dist/server/index.d.ts +1 -0
  31. package/dist/server/index.js +64 -0
  32. package/dist/server/kits.d.ts +36 -0
  33. package/dist/server/kits.js +279 -0
  34. package/dist/server/knobs.d.ts +15 -0
  35. package/dist/server/knobs.js +333 -0
  36. package/dist/server/mcpHttp.d.ts +53 -0
  37. package/dist/server/mcpHttp.js +143 -0
  38. package/dist/server/mcpSpec.d.ts +191 -0
  39. package/dist/server/mcpSpec.js +402 -0
  40. package/dist/server/parts.d.ts +31 -0
  41. package/dist/server/parts.js +338 -0
  42. package/dist/server/postMarkdown.d.ts +10 -0
  43. package/dist/server/postMarkdown.js +271 -0
  44. package/dist/server/postSurfaces.d.ts +40 -0
  45. package/dist/server/postSurfaces.js +407 -0
  46. package/dist/server/public.d.ts +5 -0
  47. package/dist/server/public.js +5 -0
  48. package/dist/server/push.d.ts +45 -0
  49. package/dist/server/push.js +234 -0
  50. package/dist/server/richRender.d.ts +15 -0
  51. package/dist/server/richRender.js +392 -0
  52. package/dist/server/run.d.ts +82 -0
  53. package/dist/server/run.js +290 -0
  54. package/dist/server/runApi.d.ts +6 -0
  55. package/dist/server/runApi.js +82 -0
  56. package/dist/server/slots.d.ts +14 -0
  57. package/dist/server/slots.js +71 -0
  58. package/dist/server/sqlStore.d.ts +119 -0
  59. package/dist/server/sqlStore.js +954 -0
  60. package/dist/server/sqliteStorage.d.ts +2 -0
  61. package/dist/server/sqliteStorage.js +90 -0
  62. package/dist/server/surfacePage.d.ts +48 -0
  63. package/dist/server/surfacePage.js +1099 -0
  64. package/dist/server/themes.d.ts +48 -0
  65. package/dist/server/themes.js +305 -0
  66. package/dist/server/types.d.ts +599 -0
  67. package/dist/server/types.js +299 -0
  68. package/dist/workers/public.d.ts +1 -0
  69. package/dist/workers/public.js +3 -0
  70. package/extensions/mockpit.js +703 -0
  71. package/guide/AGENT_SETUP.md +23 -0
  72. package/guide/topics/asks.md +87 -0
  73. package/guide/topics/feedback.md +127 -0
  74. package/guide/topics/html.md +175 -0
  75. package/guide/topics/http.md +76 -0
  76. package/guide/topics/knobs.md +72 -0
  77. package/guide/topics/scripts.md +129 -0
  78. package/guide/topics/surfaces.md +100 -0
  79. package/package.json +165 -4
  80. package/skills/launch-video/SKILL.md +159 -0
  81. package/skills/mockpit/SKILL.md +40 -0
  82. package/viewer/dist/index.html +1922 -0
@@ -0,0 +1,299 @@
1
+ // Shared data model — no runtime imports, safe for any platform.
2
+ // A post is an ordered list of surfaces. Each surface declares its own kind;
3
+ // the post itself is kind-agnostic. An `html` surface is arbitrary agent
4
+ // markup rendered in an opaque-origin iframe. Rich text/code kinds are structured
5
+ // data rendered into sandboxed documents; image/json stay as data rendered
6
+ // natively by the trusted viewer. A diagram-with-its-diff is `[html, diff]`.
7
+ // The canonical, ordered list of every surface kind — the single source of
8
+ // truth. `SurfaceKind` derives from it, and the MCP tool schemas (mcpSpec.ts)
9
+ // build their `kind` enums from it, so a kind can't be added to the model
10
+ // without the MCP tier advertising it too (the gap that left `json`/`code`
11
+ // publishable over CLI/REST but invisible to MCP). The per-kind FIELD schemas
12
+ // in postSurfaces.ts and mcpSpec.ts are still hand-written; test/mcpSpec.test.ts
13
+ // guards that every kind here round-trips through both the MCP schema and the
14
+ // validator with its fields, so neither half can silently fall behind.
15
+ export const SURFACE_KINDS = [
16
+ "html",
17
+ "diff",
18
+ "image",
19
+ "markdown",
20
+ "terminal",
21
+ "mermaid",
22
+ "json",
23
+ "code",
24
+ ];
25
+ export const SURFACE_KIND_METADATA = {
26
+ html: { contentField: "html", sandboxed: true },
27
+ diff: { contentField: "patch", sandboxed: true, frameClass: "diffframe" },
28
+ image: { sandboxed: false },
29
+ markdown: { contentField: "markdown", sandboxed: true, frameClass: "mdframe" },
30
+ terminal: { contentField: "text", sandboxed: true, frameClass: "termframe" },
31
+ mermaid: { contentField: "mermaid", sandboxed: true, frameClass: "mermaidframe" },
32
+ json: { contentField: "data", sandboxed: false },
33
+ code: { contentField: "code", sandboxed: true, frameClass: "codeframe" },
34
+ };
35
+ export const SURFACE_KIND_LIST = SURFACE_KINDS.join(", ");
36
+ export const SANDBOXED_SURFACE_KINDS = SURFACE_KINDS.filter((kind) => SURFACE_KIND_METADATA[kind].sandboxed);
37
+ export const NATIVE_SURFACE_KINDS = SURFACE_KINDS.filter((kind) => !SURFACE_KIND_METADATA[kind].sandboxed);
38
+ export const SURFACE_CONTENT_FIELDS = Object.fromEntries(SURFACE_KINDS.flatMap((kind) => {
39
+ const meta = SURFACE_KIND_METADATA[kind];
40
+ const field = "contentField" in meta ? meta.contentField : undefined;
41
+ return field ? [[kind, field]] : [];
42
+ }));
43
+ export const SURFACE_FRAME_CLASSES = Object.fromEntries(SURFACE_KINDS.flatMap((kind) => {
44
+ const meta = SURFACE_KIND_METADATA[kind];
45
+ const frameClass = "frameClass" in meta ? meta.frameClass : undefined;
46
+ return frameClass ? [[kind, frameClass]] : [];
47
+ }));
48
+ export function isSurfaceKind(kind) {
49
+ return typeof kind === "string" && Object.hasOwn(SURFACE_KIND_METADATA, kind);
50
+ }
51
+ export function isSandboxedSurfaceKind(kind) {
52
+ return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
53
+ }
54
+ // The viewer's write-in option on every ask. Reserved, so a delivered
55
+ // `{id: "other", other: true}` choice never shadows an option the agent declared.
56
+ export const OTHER_ID = "other";
57
+ export const HISTORY_LIMIT = 20;
58
+ // "user" is the reserved trust label for genuine human comments. A session
59
+ // agent with that name could otherwise have its programmatic comments delivered
60
+ // as user feedback, so the store normalizes it when creating sessions.
61
+ export function reservedAgent(name) {
62
+ return name === "user" ? "agent" : name;
63
+ }
64
+ // SQLite terminates a TEXT value at the first embedded NUL byte. A NUL has no
65
+ // place in a title/comment/label anyway, so stored text is stripped of it
66
+ // (removing the byte, not truncating). Returns the input untouched when there's
67
+ // nothing to strip, so the common path is free.
68
+ const NUL_CHAR = String.fromCharCode(0);
69
+ export function stripNul(s) {
70
+ return (typeof s === "string" && s.includes(NUL_CHAR) ? s.replaceAll(NUL_CHAR, "") : s);
71
+ }
72
+ // Per-asset upload cap (enforced at the HTTP/MCP edge → 413) and the workspace-wide
73
+ // budget the store evicts down to. One Durable Object holds the whole workspace, so
74
+ // the budget sits well under its ~10 GB SQLite ceiling.
75
+ export const MAX_ASSET_BYTES = 5 * 1024 * 1024;
76
+ export const MAX_WORKSPACE_ASSET_BYTES = 2 * 1024 * 1024 * 1024;
77
+ // Short, unguessable id: 8 random bytes (64 bits) as 11 url-safe base64 chars.
78
+ // These double as bearer capabilities: in publicRead mode `/s/:id` and
79
+ // `/api/mocks/:id` are reachable without the workspace token, so the id IS the
80
+ // share secret and must resist enumeration. btoa is a global in both Node and
81
+ // Workers.
82
+ export const newId = () => {
83
+ let id = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(8))))
84
+ .replace(/\+/g, "-")
85
+ .replace(/\//g, "_")
86
+ .replace(/=+$/, "");
87
+ // Ids are used as CLI positional args and path segments. A leading "-" or
88
+ // "_" makes node:util parseArgs treat them as options, so swap a leading
89
+ // separator for an alphanumeric.
90
+ if (id[0] === "-" || id[0] === "_")
91
+ id = "0" + id.slice(1);
92
+ return id;
93
+ };
94
+ // Content-addressed asset id: the lowercase hex SHA-256 of the bytes. Because
95
+ // it depends only on the content, an agent can derive `/a/:id` from the bytes
96
+ // alone — no upload round-trip — and write the URL into a surface before (or
97
+ // while) the upload lands. Identical uploads collapse to one stored blob.
98
+ export async function hashAssetId(data) {
99
+ // Copy into a fresh ArrayBuffer-backed view: digest wants a definite
100
+ // ArrayBuffer, and this also avoids the SharedArrayBuffer-backed lib type.
101
+ const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(data));
102
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
103
+ }
104
+ // Assign a stable id to every surface that lacks one, preserving existing ids.
105
+ export function normalizeSurfaceIds(surfaces) {
106
+ return surfaces.map((s) => (s.id ? s : { ...s, id: newId() }));
107
+ }
108
+ // Fallback project for sessions that never declared one (bare `curl` publishes).
109
+ export const DEFAULT_PROJECT = "workspace";
110
+ export const DEFAULT_VARIANT = "default";
111
+ // Stable, url-safe slug derived from a title. Empty input yields "mock" so a
112
+ // slug is never the empty string.
113
+ export function slugify(input) {
114
+ const slug = input
115
+ .toLowerCase()
116
+ .normalize("NFKD")
117
+ .replace(/[̀-ͯ]/g, "")
118
+ .replace(/[^a-z0-9]+/g, "-")
119
+ .replace(/^-+|-+$/g, "")
120
+ .slice(0, 60)
121
+ .replace(/-+$/g, "");
122
+ return slug || "mock";
123
+ }
124
+ // Last path segment of a working directory, for sessions that never declared a
125
+ // project. Handles both separators so a Windows cwd resolves the same way.
126
+ export function projectFromCwd(cwd) {
127
+ if (!cwd)
128
+ return null;
129
+ const parts = cwd.split(/[/\\]+/).filter(Boolean);
130
+ return parts.length > 0 ? parts[parts.length - 1] : null;
131
+ }
132
+ export const htmlSurface = (html, kits) => ({
133
+ kind: "html",
134
+ html,
135
+ ...(Array.isArray(kits) && kits.length > 0
136
+ ? { kits: kits.filter((k) => typeof k === "string") }
137
+ : {}),
138
+ });
139
+ // An open ask is one no reply has answered yet: a write-in or a note alone
140
+ // answers it too ("Neither, because…").
141
+ export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
142
+ export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a));
143
+ // How many built-in "Which one?" asks the viewer shows: one mock-wide when every
144
+ // state needs one and the live names line up, else one per needing state. Same
145
+ // rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
146
+ // the question the user actually sees. Answering it archives the losers, which
147
+ // is what makes it stop counting.
148
+ export function builtinAskCount(mock, posts, mockId) {
149
+ const live = (state) => posts
150
+ .filter((p) => p.mock === mockId && p.state === state && p.status !== "archived")
151
+ .map((p) => p.variant);
152
+ const needs = (state) => {
153
+ const names = live(state);
154
+ if (names.length < 2)
155
+ return false;
156
+ return !mock.asks.some((a) => (a.scope === "mock" || (a.scope === "state" && a.state === state)) &&
157
+ names.every((n) => a.options.some((o) => o.variant === n)));
158
+ };
159
+ const states = mock.states.length ? mock.states : [null];
160
+ const needing = states.filter(needs);
161
+ if (!needing.length)
162
+ return 0;
163
+ const key = (state) => [...live(state)].sort().join("\n");
164
+ const first = key(needing[0]);
165
+ return needing.length === states.length && states.every((st) => key(st) === first)
166
+ ? 1
167
+ : needing.length;
168
+ }
169
+ export const openCount = (mock, posts) => openAsks(mock).length + builtinAskCount(mock, posts, mock.id);
170
+ // The viewer's built-in "Which one?" for variants no agent ask binds: `variant`
171
+ // mock-wide, `variant:<state>` per state. Never stored; synthesized from the
172
+ // variants so a reply answering it validates, flips and reads like any ask. An
173
+ // option's id is the variant name, so the viewer needs no id mapping.
174
+ export const BUILTIN_ASK_ID = "variant";
175
+ export function builtinAsk(mock, posts, id) {
176
+ let state = null;
177
+ if (id !== BUILTIN_ASK_ID) {
178
+ if (!id.startsWith(`${BUILTIN_ASK_ID}:`))
179
+ return undefined;
180
+ state = id.slice(BUILTIN_ASK_ID.length + 1);
181
+ if (!mock.states.includes(state))
182
+ return undefined;
183
+ }
184
+ const names = [
185
+ ...new Set(posts.filter((p) => state === null || p.state === state).map((p) => p.variant)),
186
+ ];
187
+ if (names.length === 0)
188
+ return undefined;
189
+ return {
190
+ id,
191
+ text: "Which one?",
192
+ scope: state === null ? "mock" : "state",
193
+ ...(state === null ? {} : { state }),
194
+ options: names.map((v) => ({ id: v, label: v, variant: v })),
195
+ at: "",
196
+ };
197
+ }
198
+ // Per-project rollup for the projects list. Pure, so every store agrees on
199
+ // what "open" and "lastActiveAt" mean.
200
+ export function summarizeProjects(mocks, sessions, posts = []) {
201
+ const byProject = new Map();
202
+ for (const m of mocks) {
203
+ const list = byProject.get(m.project);
204
+ if (list)
205
+ list.push(m);
206
+ else
207
+ byProject.set(m.project, [m]);
208
+ }
209
+ const sessionCounts = new Map();
210
+ const sessionActive = new Map();
211
+ for (const s of sessions) {
212
+ const name = s.project ?? projectFromCwd(s.cwd) ?? DEFAULT_PROJECT;
213
+ sessionCounts.set(name, (sessionCounts.get(name) ?? 0) + 1);
214
+ const prev = sessionActive.get(name);
215
+ if (!prev || s.lastActiveAt > prev)
216
+ sessionActive.set(name, s.lastActiveAt);
217
+ }
218
+ const names = new Set([...byProject.keys(), ...sessionCounts.keys()]);
219
+ return [...names]
220
+ .map((name) => {
221
+ const list = byProject.get(name) ?? [];
222
+ const lastMock = list.reduce((max, m) => (m.updatedAt > max ? m.updatedAt : max), "");
223
+ return {
224
+ name,
225
+ mocks: list.length,
226
+ open: list.reduce((n, m) => n + openCount(m, posts), 0),
227
+ lastActiveAt: [lastMock, sessionActive.get(name) ?? ""].sort().pop() ?? "",
228
+ sessions: sessionCounts.get(name) ?? 0,
229
+ };
230
+ })
231
+ .sort((a, b) => b.lastActiveAt.localeCompare(a.lastActiveAt));
232
+ }
233
+ // The combined byte weight of a post's surfaces, for size limits. image
234
+ // surfaces are tiny refs — the asset bytes they point at are bounded separately
235
+ // by MAX_ASSET_BYTES, not this post cap.
236
+ export function surfacesByteLength(surfaces) {
237
+ let n = 0;
238
+ for (const p of surfaces) {
239
+ if (p.kind === "html")
240
+ n += p.html.length;
241
+ else if (p.kind === "diff") {
242
+ n += p.patch?.length ?? 0;
243
+ for (const f of p.files ?? [])
244
+ n += f.before.length + f.after.length;
245
+ }
246
+ else if (p.kind === "image") {
247
+ n += p.assetId.length + (p.alt?.length ?? 0) + (p.caption?.length ?? 0);
248
+ }
249
+ else if (p.kind === "markdown") {
250
+ n += p.markdown.length;
251
+ }
252
+ else if (p.kind === "terminal") {
253
+ n += p.text.length + (p.title?.length ?? 0);
254
+ }
255
+ else if (p.kind === "mermaid") {
256
+ n += p.mermaid.length;
257
+ }
258
+ else if (p.kind === "json") {
259
+ n += JSON.stringify(p.data).length;
260
+ }
261
+ else {
262
+ n +=
263
+ p.code.length + (p.language?.length ?? 0) + (p.title?.length ?? 0) + (p.lineStart ? 4 : 0);
264
+ }
265
+ }
266
+ return n;
267
+ }
268
+ // Collect the asset ids an ordered surfaces list references (image surfaces).
269
+ // Used to keep referenced assets out of eviction's first wave. Assets embedded
270
+ // by raw URL inside html markup are invisible here — touch-on-serve keeps those
271
+ // warm instead.
272
+ export function collectAssetIds(surfaces, out) {
273
+ for (const p of surfaces) {
274
+ if (p.kind === "image")
275
+ out.add(p.assetId);
276
+ }
277
+ }
278
+ // Pick the assets to evict so `incomingBytes` fits under `budget`. Oldest
279
+ // (lastAccessedAt) first, but unreferenced assets go before referenced ones —
280
+ // a live embed is only evicted as a last resort, once unreferenced candidates
281
+ // are exhausted. Returns the ids to remove (possibly empty).
282
+ export function selectEvictions(candidates, incomingBytes, budget) {
283
+ let total = candidates.reduce((sum, c) => sum + c.byteLength, 0);
284
+ if (total + incomingBytes <= budget)
285
+ return [];
286
+ const order = [...candidates].sort((a, b) => {
287
+ if (a.referenced !== b.referenced)
288
+ return a.referenced ? 1 : -1;
289
+ return a.lastAccessedAt.localeCompare(b.lastAccessedAt);
290
+ });
291
+ const evict = [];
292
+ for (const c of order) {
293
+ if (total + incomingBytes <= budget)
294
+ break;
295
+ evict.push(c.id);
296
+ total -= c.byteLength;
297
+ }
298
+ return evict;
299
+ }
@@ -0,0 +1 @@
1
+ export { SqlStore } from "../server/sqlStore.js";
@@ -0,0 +1,3 @@
1
+ // Stable Workers entrypoint for integrations that embed mockpit inside a
2
+ // Cloudflare Durable Object and want the SQLite-backed Store implementation.
3
+ export { SqlStore } from "../server/sqlStore.js";