mockpit 0.0.0-stage → 1.1.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/LICENSE +22 -0
- package/README.md +239 -2
- package/bin/demoData.js +132 -0
- package/bin/initDesign.js +846 -0
- package/bin/mockpit.js +1835 -0
- package/bin/serveUrl.d.ts +1 -0
- package/bin/serveUrl.js +7 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +252 -0
- package/dist/server/apiViews.d.ts +329 -0
- package/dist/server/apiViews.js +135 -0
- package/dist/server/app.d.ts +55 -0
- package/dist/server/app.js +3134 -0
- package/dist/server/base64.d.ts +1 -0
- package/dist/server/base64.js +16 -0
- package/dist/server/cdn.d.ts +6 -0
- package/dist/server/cdn.js +33 -0
- package/dist/server/codeRunner.d.ts +5 -0
- package/dist/server/codeRunner.js +89 -0
- package/dist/server/codeWorker.d.ts +1 -0
- package/dist/server/codeWorker.js +196 -0
- package/dist/server/designGuide.d.ts +16 -0
- package/dist/server/designGuide.js +311 -0
- package/dist/server/eventLog.d.ts +27 -0
- package/dist/server/eventLog.js +187 -0
- package/dist/server/events.d.ts +53 -0
- package/dist/server/events.js +14 -0
- package/dist/server/feedbackBatch.d.ts +43 -0
- package/dist/server/feedbackBatch.js +98 -0
- package/dist/server/icons.d.ts +50 -0
- package/dist/server/icons.js +225 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +76 -0
- package/dist/server/kits.d.ts +36 -0
- package/dist/server/kits.js +279 -0
- package/dist/server/knobs.d.ts +15 -0
- package/dist/server/knobs.js +333 -0
- package/dist/server/logFile.d.ts +2 -0
- package/dist/server/logFile.js +27 -0
- package/dist/server/mcpHttp.d.ts +53 -0
- package/dist/server/mcpHttp.js +143 -0
- package/dist/server/mcpSpec.d.ts +191 -0
- package/dist/server/mcpSpec.js +402 -0
- package/dist/server/parts.d.ts +31 -0
- package/dist/server/parts.js +338 -0
- package/dist/server/postMarkdown.d.ts +10 -0
- package/dist/server/postMarkdown.js +271 -0
- package/dist/server/postSurfaces.d.ts +40 -0
- package/dist/server/postSurfaces.js +407 -0
- package/dist/server/public.d.ts +5 -0
- package/dist/server/public.js +5 -0
- package/dist/server/push.d.ts +45 -0
- package/dist/server/push.js +234 -0
- package/dist/server/richRender.d.ts +15 -0
- package/dist/server/richRender.js +392 -0
- package/dist/server/run.d.ts +82 -0
- package/dist/server/run.js +290 -0
- package/dist/server/runApi.d.ts +6 -0
- package/dist/server/runApi.js +82 -0
- package/dist/server/slots.d.ts +14 -0
- package/dist/server/slots.js +71 -0
- package/dist/server/sqlStore.d.ts +119 -0
- package/dist/server/sqlStore.js +954 -0
- package/dist/server/sqliteStorage.d.ts +2 -0
- package/dist/server/sqliteStorage.js +90 -0
- package/dist/server/surfacePage.d.ts +48 -0
- package/dist/server/surfacePage.js +1099 -0
- package/dist/server/themes.d.ts +48 -0
- package/dist/server/themes.js +305 -0
- package/dist/server/types.d.ts +599 -0
- package/dist/server/types.js +299 -0
- package/dist/workers/public.d.ts +1 -0
- package/dist/workers/public.js +3 -0
- package/extensions/mockpit.js +717 -0
- package/guide/AGENT_SETUP.md +23 -0
- package/guide/topics/asks.md +87 -0
- package/guide/topics/feedback.md +127 -0
- package/guide/topics/html.md +175 -0
- package/guide/topics/http.md +76 -0
- package/guide/topics/knobs.md +72 -0
- package/guide/topics/scripts.md +129 -0
- package/guide/topics/surfaces.md +100 -0
- package/package.json +165 -4
- package/skills/launch-video/SKILL.md +159 -0
- package/skills/mockpit/SKILL.md +40 -0
- 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";
|