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,82 @@
|
|
|
1
|
+
import type { FlowContext, FlowResult } from "./app.ts";
|
|
2
|
+
import type { FeedbackBatch } from "./feedbackBatch.ts";
|
|
3
|
+
import { type GuideTopic } from "./designGuide.ts";
|
|
4
|
+
import { type McpFlows } from "./mcpHttp.ts";
|
|
5
|
+
import type { Asset, AssetKind, Store } from "./types.ts";
|
|
6
|
+
export interface RunLimits {
|
|
7
|
+
deadlineMs: number;
|
|
8
|
+
cpuMs: number;
|
|
9
|
+
memoryBytes: number;
|
|
10
|
+
stackBytes: number;
|
|
11
|
+
maxCodeBytes: number;
|
|
12
|
+
maxOutputChars: number;
|
|
13
|
+
maxCalls: number;
|
|
14
|
+
maxInflight: number;
|
|
15
|
+
maxRuns: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_RUN_LIMITS: RunLimits;
|
|
18
|
+
export type RunErrorKind = "script" | "timeout" | "limit" | "aborted";
|
|
19
|
+
export interface RunError {
|
|
20
|
+
kind: RunErrorKind;
|
|
21
|
+
message: string;
|
|
22
|
+
line?: number;
|
|
23
|
+
column?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ExecRequest {
|
|
26
|
+
code: string;
|
|
27
|
+
limits: RunLimits;
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
call(name: string, argsJson: string): Promise<string>;
|
|
30
|
+
print(line: string): void;
|
|
31
|
+
}
|
|
32
|
+
export type ExecResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
json?: string;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
error: RunError;
|
|
38
|
+
};
|
|
39
|
+
export interface Executor {
|
|
40
|
+
run(request: ExecRequest): Promise<ExecResult>;
|
|
41
|
+
}
|
|
42
|
+
export interface RunCall {
|
|
43
|
+
fn: string;
|
|
44
|
+
ok: boolean;
|
|
45
|
+
summary: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RunEnvelope {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
value?: unknown;
|
|
50
|
+
prints: string[];
|
|
51
|
+
calls: RunCall[];
|
|
52
|
+
error?: RunError;
|
|
53
|
+
feedback: FeedbackBatch[];
|
|
54
|
+
session: string | null;
|
|
55
|
+
truncated?: true;
|
|
56
|
+
}
|
|
57
|
+
export interface RunDeps {
|
|
58
|
+
store: Store;
|
|
59
|
+
executor?: Executor;
|
|
60
|
+
limits: RunLimits;
|
|
61
|
+
flows: McpFlows;
|
|
62
|
+
guide(project: string, topic?: GuideTopic): string | Promise<string>;
|
|
63
|
+
createSession(input: {
|
|
64
|
+
agent: string;
|
|
65
|
+
project?: string;
|
|
66
|
+
}): Promise<string>;
|
|
67
|
+
uploadAsset(input: {
|
|
68
|
+
data: Uint8Array;
|
|
69
|
+
contentType: string;
|
|
70
|
+
filename?: string;
|
|
71
|
+
kind?: AssetKind;
|
|
72
|
+
session?: string;
|
|
73
|
+
}): Promise<{
|
|
74
|
+
asset: Omit<Asset, "data">;
|
|
75
|
+
} | {
|
|
76
|
+
error: string;
|
|
77
|
+
status: number;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export declare const RUN_UNAVAILABLE = "run is not available on this deployment: it has no script sandbox. Use the mock tools (or REST) instead";
|
|
81
|
+
export declare function cut(s: string, max: number): string;
|
|
82
|
+
export declare function createRunFlow(deps: RunDeps): (body: any, ctx: FlowContext) => Promise<FlowResult>;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Server-side codemode: one `run` executes an agent's script against the same
|
|
2
|
+
// flows REST and MCP call, with the caller's identity. Runtime-agnostic — the
|
|
3
|
+
// sandbox itself is an `Executor` the host passes in (server/codeRunner.ts on
|
|
4
|
+
// Node); a deployment without one answers that `run` is unavailable.
|
|
5
|
+
//
|
|
6
|
+
// The script is a new untrusted channel into the trusted origin, gated like the
|
|
7
|
+
// bridge: it never sees a credential, binds only agent verbs (never Send,
|
|
8
|
+
// drafts, accept/drop, settings), and every way out goes through a host call
|
|
9
|
+
// that is counted, bounded and logged.
|
|
10
|
+
import { decodeBase64 } from "./base64.js";
|
|
11
|
+
import { isGuideTopic, unknownTopicMessage } from "./designGuide.js";
|
|
12
|
+
import { unwrap } from "./mcpHttp.js";
|
|
13
|
+
import { RUN_FUNCTIONS } from "./runApi.js";
|
|
14
|
+
// A script never waits for the user, so the wall clock is only a sandbox guard:
|
|
15
|
+
// 10 s clears a run of publishes with room to spare. 32 MiB and a 256 KiB stack are what the spike showed safe (512 KiB overflows the host
|
|
16
|
+
// stack on workerd). The code cap clears a few full-page html variants written
|
|
17
|
+
// as template literals.
|
|
18
|
+
export const DEFAULT_RUN_LIMITS = {
|
|
19
|
+
deadlineMs: 10_000,
|
|
20
|
+
cpuMs: 10_000,
|
|
21
|
+
memoryBytes: 32 * 1024 * 1024,
|
|
22
|
+
stackBytes: 256 * 1024,
|
|
23
|
+
maxCodeBytes: 1024 * 1024,
|
|
24
|
+
maxOutputChars: 24_000,
|
|
25
|
+
maxCalls: 100,
|
|
26
|
+
maxInflight: 4,
|
|
27
|
+
maxRuns: 4,
|
|
28
|
+
};
|
|
29
|
+
export const RUN_UNAVAILABLE = "run is not available on this deployment: it has no script sandbox. Use the mock tools (or REST) instead";
|
|
30
|
+
const text = (v, max) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : undefined;
|
|
31
|
+
const plain = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
32
|
+
// Keep the head and the tail: the start says what a value is, the end is where
|
|
33
|
+
// a failing log usually explains itself.
|
|
34
|
+
export function cut(s, max) {
|
|
35
|
+
if (s.length <= max)
|
|
36
|
+
return s;
|
|
37
|
+
const keep = Math.max(0, max - 40);
|
|
38
|
+
const head = Math.ceil(keep / 2);
|
|
39
|
+
return `${s.slice(0, head)}\n…[${s.length - keep} chars cut]…\n${s.slice(s.length - (keep - head))}`;
|
|
40
|
+
}
|
|
41
|
+
function writeSummary(body) {
|
|
42
|
+
const post = body?.post;
|
|
43
|
+
const slug = body?.mock?.slug ?? "?";
|
|
44
|
+
if (!post)
|
|
45
|
+
return slug;
|
|
46
|
+
return `${slug}/${post.state ? `${post.state}/` : ""}${post.variant} v${post.version}`;
|
|
47
|
+
}
|
|
48
|
+
// Host calls past `size` in flight queue rather than fail, so Promise.all over
|
|
49
|
+
// a handful of publishes just works.
|
|
50
|
+
function semaphore(size) {
|
|
51
|
+
const queue = [];
|
|
52
|
+
let used = 0;
|
|
53
|
+
return {
|
|
54
|
+
async take() {
|
|
55
|
+
if (used < size) {
|
|
56
|
+
used++;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
60
|
+
},
|
|
61
|
+
give() {
|
|
62
|
+
const next = queue.shift();
|
|
63
|
+
if (next)
|
|
64
|
+
next();
|
|
65
|
+
else
|
|
66
|
+
used--;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function createRunFlow(deps) {
|
|
71
|
+
const { store, flows, limits } = deps;
|
|
72
|
+
let active = 0;
|
|
73
|
+
return async function runFlow(body, ctx) {
|
|
74
|
+
const executor = deps.executor;
|
|
75
|
+
if (!executor)
|
|
76
|
+
return { status: 501, body: { error: RUN_UNAVAILABLE } };
|
|
77
|
+
if (!body || typeof body !== "object")
|
|
78
|
+
return { status: 400, body: { error: "invalid JSON body" } };
|
|
79
|
+
const code = body.code;
|
|
80
|
+
if (typeof code !== "string" || !code.trim()) {
|
|
81
|
+
return { status: 400, body: { error: 'provide "code": the body of an async function' } };
|
|
82
|
+
}
|
|
83
|
+
let sessionId = null;
|
|
84
|
+
let project = text(body.project, 500);
|
|
85
|
+
if (typeof body.session === "string" && body.session) {
|
|
86
|
+
const session = await store.getSession(body.session);
|
|
87
|
+
if (!session)
|
|
88
|
+
return { status: 404, body: { error: `session "${body.session}" not found` } };
|
|
89
|
+
sessionId = session.id;
|
|
90
|
+
project ??= session.project ?? undefined;
|
|
91
|
+
}
|
|
92
|
+
const agent = text(body.agent, 500) ?? "agent";
|
|
93
|
+
const prints = [];
|
|
94
|
+
const calls = [];
|
|
95
|
+
const feedback = [];
|
|
96
|
+
let printed = 0;
|
|
97
|
+
let truncated = false;
|
|
98
|
+
const envelope = (ok, extra) => ({
|
|
99
|
+
status: 200,
|
|
100
|
+
body: {
|
|
101
|
+
ok,
|
|
102
|
+
...extra,
|
|
103
|
+
prints,
|
|
104
|
+
calls,
|
|
105
|
+
feedback,
|
|
106
|
+
session: sessionId,
|
|
107
|
+
...(truncated ? { truncated: true } : {}),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
if (new TextEncoder().encode(code).byteLength > limits.maxCodeBytes) {
|
|
111
|
+
return envelope(false, {
|
|
112
|
+
error: { kind: "limit", message: `code exceeds ${limits.maxCodeBytes} bytes` },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (active >= limits.maxRuns) {
|
|
116
|
+
return { status: 503, body: { error: "too many concurrent runs; retry shortly" } };
|
|
117
|
+
}
|
|
118
|
+
active++;
|
|
119
|
+
const run = new AbortController();
|
|
120
|
+
const onAbort = () => run.abort();
|
|
121
|
+
ctx.signal?.addEventListener("abort", onAbort, { once: true });
|
|
122
|
+
if (ctx.signal?.aborted)
|
|
123
|
+
run.abort();
|
|
124
|
+
// A script is agent code: it never acts as the user, whatever origin sent it.
|
|
125
|
+
const flowCtx = { ...ctx, viewer: false, strict: false, signal: run.signal };
|
|
126
|
+
const gate = semaphore(limits.maxInflight);
|
|
127
|
+
const inflight = new Set();
|
|
128
|
+
const session = async () => (sessionId ??= await deps.createSession({ agent, project }));
|
|
129
|
+
const mine = async (v) => ({
|
|
130
|
+
...v,
|
|
131
|
+
session: await session(),
|
|
132
|
+
...(project ? { project } : {}),
|
|
133
|
+
});
|
|
134
|
+
const take = async (result) => {
|
|
135
|
+
const value = unwrap(await result);
|
|
136
|
+
if (Array.isArray(value?.feedback))
|
|
137
|
+
feedback.push(...value.feedback);
|
|
138
|
+
return value;
|
|
139
|
+
};
|
|
140
|
+
const projectOf = async () => {
|
|
141
|
+
if (project)
|
|
142
|
+
return project;
|
|
143
|
+
const projects = await store.listProjects();
|
|
144
|
+
return projects[0]?.name ?? "workspace";
|
|
145
|
+
};
|
|
146
|
+
// Each host function returns [value the script sees, one-line summary].
|
|
147
|
+
const host = {
|
|
148
|
+
async guide([topic]) {
|
|
149
|
+
if (topic === undefined || topic === null)
|
|
150
|
+
return [await deps.guide(await projectOf()), "brief"];
|
|
151
|
+
if (!isGuideTopic(topic))
|
|
152
|
+
throw new Error(unknownTopicMessage(String(topic)));
|
|
153
|
+
return [await deps.guide("", topic), String(topic)];
|
|
154
|
+
},
|
|
155
|
+
async read([mock, opts]) {
|
|
156
|
+
if (mock === undefined || mock === null) {
|
|
157
|
+
const value = await take(flows.list({ project: await projectOf() }));
|
|
158
|
+
return [value, `${value.mocks.length} mocks`];
|
|
159
|
+
}
|
|
160
|
+
const o = plain(opts);
|
|
161
|
+
const value = await take(flows.get(mock, { project, body: o.body === true, history: o.history === true }));
|
|
162
|
+
return [value, String(value?.slug ?? mock)];
|
|
163
|
+
},
|
|
164
|
+
async publish([v]) {
|
|
165
|
+
const value = await take(flows.publish({ ...(await mine(plain(v))), agent }, flowCtx));
|
|
166
|
+
return [value, writeSummary(value)];
|
|
167
|
+
},
|
|
168
|
+
async ask([mock, asks]) {
|
|
169
|
+
const value = await take(flows.ask(mock, await mine({ asks }), flowCtx));
|
|
170
|
+
return [value, `${value.mock} ${value.asks.length} ask(s)`];
|
|
171
|
+
},
|
|
172
|
+
async feedback() {
|
|
173
|
+
const value = await take(flows.feedback(await mine({})));
|
|
174
|
+
const n = value.feedback.length;
|
|
175
|
+
return [value, n ? `${n} batch(es)` : "no feedback"];
|
|
176
|
+
},
|
|
177
|
+
async say([v, message]) {
|
|
178
|
+
const r = plain(v);
|
|
179
|
+
await take(flows.say(r.mock, { ...(await mine(r)), message }));
|
|
180
|
+
return [null, String(r.mock ?? "?")];
|
|
181
|
+
},
|
|
182
|
+
async upload([data, opts]) {
|
|
183
|
+
if (typeof data !== "string" || !data)
|
|
184
|
+
throw new Error("upload needs base64 data");
|
|
185
|
+
const o = plain(opts);
|
|
186
|
+
const result = await deps.uploadAsset({
|
|
187
|
+
data: decodeBase64(data),
|
|
188
|
+
contentType: typeof o.contentType === "string" ? o.contentType : "",
|
|
189
|
+
filename: typeof o.filename === "string" ? o.filename : undefined,
|
|
190
|
+
kind: o.kind === "image" || o.kind === "file" ? o.kind : undefined,
|
|
191
|
+
session: await session(),
|
|
192
|
+
});
|
|
193
|
+
if ("error" in result)
|
|
194
|
+
throw new Error(result.error);
|
|
195
|
+
const { id, contentType, byteLength } = result.asset;
|
|
196
|
+
return [{ id, url: `${ctx.base}/a/${id}`, contentType, byteLength }, id];
|
|
197
|
+
},
|
|
198
|
+
async export([v]) {
|
|
199
|
+
const r = plain(v);
|
|
200
|
+
const value = await take(flows.exportMock(r.mock, { ...r, project }, flowCtx));
|
|
201
|
+
return [value, `${value.mock} ${value.states.length} state(s)`];
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const call = (name, argsJson) => {
|
|
205
|
+
const entry = { fn: name, ok: false, summary: "cancelled" };
|
|
206
|
+
calls.push(entry);
|
|
207
|
+
const fail = (message) => {
|
|
208
|
+
entry.summary = message;
|
|
209
|
+
return Promise.reject(new Error(message));
|
|
210
|
+
};
|
|
211
|
+
if (run.signal.aborted)
|
|
212
|
+
return fail("run ended");
|
|
213
|
+
if (calls.length > limits.maxCalls)
|
|
214
|
+
return fail(`call limit of ${limits.maxCalls} reached`);
|
|
215
|
+
const fn = RUN_FUNCTIONS.includes(name)
|
|
216
|
+
? host[name]
|
|
217
|
+
: undefined;
|
|
218
|
+
if (!fn)
|
|
219
|
+
return fail(`mockpit.${name} is not a function`);
|
|
220
|
+
const work = (async () => {
|
|
221
|
+
await gate.take();
|
|
222
|
+
try {
|
|
223
|
+
if (run.signal.aborted)
|
|
224
|
+
throw new Error("run ended");
|
|
225
|
+
const args = JSON.parse(argsJson);
|
|
226
|
+
const [value, summary] = await fn(Array.isArray(args) ? args : []);
|
|
227
|
+
entry.ok = true;
|
|
228
|
+
entry.summary = summary;
|
|
229
|
+
return JSON.stringify(value ?? null);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
entry.summary = err instanceof Error ? err.message : String(err);
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
gate.give();
|
|
237
|
+
}
|
|
238
|
+
})();
|
|
239
|
+
inflight.add(work);
|
|
240
|
+
work.then(() => inflight.delete(work), () => inflight.delete(work));
|
|
241
|
+
return work;
|
|
242
|
+
};
|
|
243
|
+
const print = (line) => {
|
|
244
|
+
if (truncated)
|
|
245
|
+
return;
|
|
246
|
+
if (printed + line.length > limits.maxOutputChars) {
|
|
247
|
+
truncated = true;
|
|
248
|
+
prints.push(cut(line, Math.max(0, limits.maxOutputChars - printed)));
|
|
249
|
+
prints.push("…[further prints cut]");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
printed += line.length;
|
|
253
|
+
prints.push(line);
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
let result;
|
|
257
|
+
try {
|
|
258
|
+
result = await executor.run({ code, limits, signal: run.signal, call, print });
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
result = {
|
|
262
|
+
ok: false,
|
|
263
|
+
error: {
|
|
264
|
+
kind: "limit",
|
|
265
|
+
message: `sandbox failed: ${err instanceof Error ? err.message : err}`,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// Writes already sent finish and land in `calls`.
|
|
270
|
+
run.abort();
|
|
271
|
+
await Promise.allSettled(inflight);
|
|
272
|
+
if (ctx.signal?.aborted && (result.ok || result.error.kind !== "aborted")) {
|
|
273
|
+
result = { ok: false, error: { kind: "aborted", message: "the caller went away" } };
|
|
274
|
+
}
|
|
275
|
+
if (!result.ok)
|
|
276
|
+
return envelope(false, { error: result.error });
|
|
277
|
+
if (result.json === undefined)
|
|
278
|
+
return envelope(true, {});
|
|
279
|
+
const room = Math.max(1000, limits.maxOutputChars - printed);
|
|
280
|
+
if (result.json.length <= room)
|
|
281
|
+
return envelope(true, { value: JSON.parse(result.json) });
|
|
282
|
+
truncated = true;
|
|
283
|
+
return envelope(true, { value: cut(result.json, room) });
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
ctx.signal?.removeEventListener("abort", onAbort);
|
|
287
|
+
active--;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const RUN_API = "// The body of an async function, in plain JavaScript (no TypeScript syntax).\n// `await mockpit.*`, `return` a JSON value, print() adds an output line.\n// No network, timers, imports or process. Session, project and agent are fixed\n// for the run; pass the returned session to the next run.\ntype Ref = { mock: string; state?: string; variant?: string };\ntype Knobs = Record<string, unknown>; // path \u2192 [default,min,max,step] | boolean | {type:\"select\",options} | \"#hex\"\ntype Surface =\n | { kind: \"html\"; html: string; kits?: string[] }\n | { kind: \"markdown\"; markdown: string }\n | { kind: \"diff\"; patch?: string; files?: { filename: string; before: string; after: string }[]; layout?: \"unified\" | \"split\" }\n | { kind: \"image\"; assetId: string; alt?: string; caption?: string }\n | { kind: \"terminal\"; text: string; cols?: number; title?: string }\n | { kind: \"mermaid\"; mermaid: string }\n | { kind: \"json\"; data: unknown; title?: string }\n | { kind: \"code\"; code: string; language?: string; lineStart?: number };\ntype Ask = {\n id?: string; text: string; scope?: \"mock\" | \"state\" | \"part\"; state?: string; part?: string; multi?: boolean;\n options: { id?: string; label: string; variant?: string; set?: Knobs }[];\n};\ntype Written = {\n post: { state: string | null; variant: string; version: number }; url: string;\n parts: { state: string | null; parts: { name: string }[] }[];\n partChanges?: { vanished: string[]; renamed: { from: string; to: string }[] };\n nudges?: string[]; suggestedAsk?: Ask; // send it with ask when nudged\n};\ntype Feedback = {\n mock: string | null;\n reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; note?: string }[]; mix: Knobs; tuned: Knobs; comments: { part: string | null; text: string }[] };\n comments: { text: string; state: string | null; variant: string | null }[];\n accepted: { state: string | null; variant: string }[]; archived: { state: string | null; variant: string }[];\n};\ntype Pending = { mock: string; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };\ndeclare const mockpit: {\n guide(topic?: \"knobs\" | \"asks\" | \"surfaces\" | \"html\" | \"feedback\" | \"http\" | \"scripts\"): Promise<string>;\n /** No mock: every mock plus pending. With one: its states, variants, asks, parts, knobs. */\n read(mock?: string, opts?: { body?: boolean; history?: boolean }): Promise<unknown>;\n /** Creates or versions (mock, state, variant). One of: html; surfaces, the full ordered list\n * ({id} alone keeps that surface, a missing id is removed); parts {\"name\"|\"name#key\": outer html}. */\n publish(v: Ref & { title?: string; kind?: \"component\" | \"page\"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number }): Promise<Written>;\n ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string }>;\n /** Never waits: what the user sent since you last heard, and what they are doing now. */\n feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;\n say(v: Ref, message: string): Promise<void>;\n export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string }[]; reply: unknown }>;\n /** data: base64 bytes. Use id as an image surface's assetId. */\n upload(data: string, opts: { contentType: string; filename?: string; kind?: \"image\" | \"file\" }): Promise<{ id: string; url: string }>;\n};\ndeclare function print(...values: unknown[]): void;";
|
|
2
|
+
export declare const RUN_FUNCTIONS: readonly ["guide", "read", "publish", "ask", "feedback", "say", "export", "upload"];
|
|
3
|
+
export type RunFunction = (typeof RUN_FUNCTIONS)[number];
|
|
4
|
+
export declare const RUN_EXAMPLE = "const html = (tone) => `<section data-part=\"hero\" class=\"${tone}\"><h1>Writer</h1></section>`;\nawait mockpit.publish({ mock: \"writer\", variant: \"calm\", html: html(\"calm\") });\nawait mockpit.publish({ mock: \"writer\", variant: \"bold\", html: html(\"bold\") });\nconst { url } = await mockpit.ask(\"writer\", [{ id: \"look\", text: \"Which look?\",\n options: [{ label: \"Calm\", variant: \"calm\" }, { label: \"Bold\", variant: \"bold\" }] }]);\n// Tell the user where to look and end the turn; a later run reads mockpit.feedback().\nreturn url;";
|
|
5
|
+
export declare const RUN_DESCRIPTION: string;
|
|
6
|
+
export declare const RUN_INSTRUCTIONS: string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The typed surface a `run` script sees, as text. One source for the MCP `run`
|
|
2
|
+
// tool description, the scripts guide topic and `mockpit run --help`, so the
|
|
3
|
+
// tiers cannot describe different APIs. Hand-shaped rather than generated from
|
|
4
|
+
// mcpSpec: a generator repeats Surface per tool and the run-level params on
|
|
5
|
+
// every function, roughly tripling the size. test/run.test.ts checks that every
|
|
6
|
+
// host function named here exists and vice versa.
|
|
7
|
+
export const RUN_API = `// The body of an async function, in plain JavaScript (no TypeScript syntax).
|
|
8
|
+
// \`await mockpit.*\`, \`return\` a JSON value, print() adds an output line.
|
|
9
|
+
// No network, timers, imports or process. Session, project and agent are fixed
|
|
10
|
+
// for the run; pass the returned session to the next run.
|
|
11
|
+
type Ref = { mock: string; state?: string; variant?: string };
|
|
12
|
+
type Knobs = Record<string, unknown>; // path → [default,min,max,step] | boolean | {type:"select",options} | "#hex"
|
|
13
|
+
type Surface =
|
|
14
|
+
| { kind: "html"; html: string; kits?: string[] }
|
|
15
|
+
| { kind: "markdown"; markdown: string }
|
|
16
|
+
| { kind: "diff"; patch?: string; files?: { filename: string; before: string; after: string }[]; layout?: "unified" | "split" }
|
|
17
|
+
| { kind: "image"; assetId: string; alt?: string; caption?: string }
|
|
18
|
+
| { kind: "terminal"; text: string; cols?: number; title?: string }
|
|
19
|
+
| { kind: "mermaid"; mermaid: string }
|
|
20
|
+
| { kind: "json"; data: unknown; title?: string }
|
|
21
|
+
| { kind: "code"; code: string; language?: string; lineStart?: number };
|
|
22
|
+
type Ask = {
|
|
23
|
+
id?: string; text: string; scope?: "mock" | "state" | "part"; state?: string; part?: string; multi?: boolean;
|
|
24
|
+
options: { id?: string; label: string; variant?: string; set?: Knobs }[];
|
|
25
|
+
};
|
|
26
|
+
type Written = {
|
|
27
|
+
post: { state: string | null; variant: string; version: number }; url: string;
|
|
28
|
+
parts: { state: string | null; parts: { name: string }[] }[];
|
|
29
|
+
partChanges?: { vanished: string[]; renamed: { from: string; to: string }[] };
|
|
30
|
+
nudges?: string[]; suggestedAsk?: Ask; // send it with ask when nudged
|
|
31
|
+
};
|
|
32
|
+
type Feedback = {
|
|
33
|
+
mock: string | null;
|
|
34
|
+
reply: null | { asks: { ask: string; chosen: { id: string; label: string; other?: true }[]; note?: string }[]; mix: Knobs; tuned: Knobs; comments: { part: string | null; text: string }[] };
|
|
35
|
+
comments: { text: string; state: string | null; variant: string | null }[];
|
|
36
|
+
accepted: { state: string | null; variant: string }[]; archived: { state: string | null; variant: string }[];
|
|
37
|
+
};
|
|
38
|
+
type Pending = { mock: string; viewerOpen: boolean; draft: null | { answered: number; of: number; comments: number; touchedAt: string } };
|
|
39
|
+
declare const mockpit: {
|
|
40
|
+
guide(topic?: "knobs" | "asks" | "surfaces" | "html" | "feedback" | "http" | "scripts"): Promise<string>;
|
|
41
|
+
/** No mock: every mock plus pending. With one: its states, variants, asks, parts, knobs. */
|
|
42
|
+
read(mock?: string, opts?: { body?: boolean; history?: boolean }): Promise<unknown>;
|
|
43
|
+
/** Creates or versions (mock, state, variant). One of: html; surfaces, the full ordered list
|
|
44
|
+
* ({id} alone keeps that surface, a missing id is removed); parts {"name"|"name#key": outer html}. */
|
|
45
|
+
publish(v: Ref & { title?: string; kind?: "component" | "page"; html?: string; surfaces?: (Surface | { id: string })[]; parts?: Record<string, string>; knobs?: Knobs; variantKnobs?: Knobs; from?: number }): Promise<Written>;
|
|
46
|
+
ask(mock: string, asks: Ask[]): Promise<{ asks: { id: string; text: string }[]; url: string }>;
|
|
47
|
+
/** Never waits: what the user sent since you last heard, and what they are doing now. */
|
|
48
|
+
feedback(): Promise<{ feedback: Feedback[]; pending: Pending[] }>;
|
|
49
|
+
say(v: Ref, message: string): Promise<void>;
|
|
50
|
+
export(v: Ref): Promise<{ states: { state: string | null; variant: string; version: number; html: string }[]; reply: unknown }>;
|
|
51
|
+
/** data: base64 bytes. Use id as an image surface's assetId. */
|
|
52
|
+
upload(data: string, opts: { contentType: string; filename?: string; kind?: "image" | "file" }): Promise<{ id: string; url: string }>;
|
|
53
|
+
};
|
|
54
|
+
declare function print(...values: unknown[]): void;`;
|
|
55
|
+
// The host functions a script can reach, by the dotted name the calls log uses.
|
|
56
|
+
export const RUN_FUNCTIONS = [
|
|
57
|
+
"guide",
|
|
58
|
+
"read",
|
|
59
|
+
"publish",
|
|
60
|
+
"ask",
|
|
61
|
+
"feedback",
|
|
62
|
+
"say",
|
|
63
|
+
"export",
|
|
64
|
+
"upload",
|
|
65
|
+
];
|
|
66
|
+
export const RUN_EXAMPLE = `const html = (tone) => \`<section data-part="hero" class="\${tone}"><h1>Writer</h1></section>\`;
|
|
67
|
+
await mockpit.publish({ mock: "writer", variant: "calm", html: html("calm") });
|
|
68
|
+
await mockpit.publish({ mock: "writer", variant: "bold", html: html("bold") });
|
|
69
|
+
const { url } = await mockpit.ask("writer", [{ id: "look", text: "Which look?",
|
|
70
|
+
options: [{ label: "Calm", variant: "calm" }, { label: "Bold", variant: "bold" }] }]);
|
|
71
|
+
// Tell the user where to look and end the turn; a later run reads mockpit.feedback().
|
|
72
|
+
return url;`;
|
|
73
|
+
export const RUN_DESCRIPTION = "Run a script against mockpit, which shows design work to the user (project > mock > state > " +
|
|
74
|
+
"variant > version). Publish variants and ask in one call; nothing waits for the user. Returns " +
|
|
75
|
+
"value, prints, calls (every host call in order: which writes landed), error {kind, message, " +
|
|
76
|
+
"line} and feedback (every batch received, even if the script failed). Read mockpit.guide() " +
|
|
77
|
+
"before the first html.\n\n" +
|
|
78
|
+
`\`\`\`ts\n${RUN_API}\n\`\`\`\n\nExample:\n\`\`\`js\n${RUN_EXAMPLE}\n\`\`\``;
|
|
79
|
+
export const RUN_INSTRUCTIONS = "Mockpit shows design work to the user. Call run with a JavaScript body that uses the " +
|
|
80
|
+
"mockpit API in its description: publish variants, ask, then end your turn. When the user " +
|
|
81
|
+
"says they answered, run mockpit.feedback(). Feedback is delivered once; read the run's " +
|
|
82
|
+
"feedback field.";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Slot } from "./types.ts";
|
|
2
|
+
export interface SlotTag {
|
|
3
|
+
raw: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
variant: string;
|
|
6
|
+
version: number | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseSlotTags(html: string): SlotTag[];
|
|
9
|
+
export declare function resolveSlots(html: string, currentVersion: (slug: string, variant: string) => number | null): Slot[];
|
|
10
|
+
export declare function expandSlots(html: string, body: (slot: {
|
|
11
|
+
slug: string;
|
|
12
|
+
variant: string;
|
|
13
|
+
version: number | null;
|
|
14
|
+
}) => string | null): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Page composition: `<mockpit-slot slug="…" variant="…" version="…">` tags in a
|
|
2
|
+
// page item's html are expanded server-side by inlining the referenced variant
|
|
3
|
+
// version's first html surface body. Snapshot semantics — publish resolves a
|
|
4
|
+
// missing `version` to the component's current one and stores the resolved list
|
|
5
|
+
// on the post, so a page keeps rendering what it was composed from.
|
|
6
|
+
//
|
|
7
|
+
// Runtime-agnostic (no node imports) and string-only: the result is handed to
|
|
8
|
+
// renderHtmlPage, which serves it from /s/:id under the sandbox CSP header, so
|
|
9
|
+
// an inlined component body is exactly as contained as the page's own markup.
|
|
10
|
+
const SLOT_TAG = /<mockpit-slot\b([^>]*?)(?:\/>|>\s*<\/mockpit-slot\s*>|>)/gi;
|
|
11
|
+
const ATTR = /([a-z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/gi;
|
|
12
|
+
function attributes(raw) {
|
|
13
|
+
const out = {};
|
|
14
|
+
ATTR.lastIndex = 0;
|
|
15
|
+
let m;
|
|
16
|
+
while ((m = ATTR.exec(raw)))
|
|
17
|
+
out[m[1].toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
export function parseSlotTags(html) {
|
|
21
|
+
const tags = [];
|
|
22
|
+
SLOT_TAG.lastIndex = 0;
|
|
23
|
+
let m;
|
|
24
|
+
while ((m = SLOT_TAG.exec(html))) {
|
|
25
|
+
const attrs = attributes(m[1] ?? "");
|
|
26
|
+
const slug = attrs.slug ?? "";
|
|
27
|
+
if (!slug)
|
|
28
|
+
continue;
|
|
29
|
+
const version = Number(attrs.version);
|
|
30
|
+
tags.push({
|
|
31
|
+
raw: m[0],
|
|
32
|
+
slug,
|
|
33
|
+
variant: attrs.variant || "default",
|
|
34
|
+
version: Number.isInteger(version) && version > 0 ? version : null,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return tags;
|
|
38
|
+
}
|
|
39
|
+
// The slot list to store on a page post: every tag with its version resolved
|
|
40
|
+
// (a missing one pins to the component's current version at publish time).
|
|
41
|
+
export function resolveSlots(html, currentVersion) {
|
|
42
|
+
const slots = [];
|
|
43
|
+
for (const tag of parseSlotTags(html)) {
|
|
44
|
+
const version = tag.version ?? currentVersion(tag.slug, tag.variant);
|
|
45
|
+
if (version == null)
|
|
46
|
+
continue;
|
|
47
|
+
slots.push({ slug: tag.slug, variant: tag.variant, version });
|
|
48
|
+
}
|
|
49
|
+
return slots;
|
|
50
|
+
}
|
|
51
|
+
const escapeAttr = (s) => s.replace(/&/g, "&").replace(/"/g, """);
|
|
52
|
+
// Replace each tag with the resolved body. An unresolvable slot renders as an
|
|
53
|
+
// empty marked-up placeholder rather than vanishing, so a broken reference is
|
|
54
|
+
// visible in review instead of silently changing the layout.
|
|
55
|
+
export function expandSlots(html, body) {
|
|
56
|
+
SLOT_TAG.lastIndex = 0;
|
|
57
|
+
return html.replace(SLOT_TAG, (raw, attrRaw) => {
|
|
58
|
+
const attrs = attributes(attrRaw ?? "");
|
|
59
|
+
const slug = attrs.slug ?? "";
|
|
60
|
+
if (!slug)
|
|
61
|
+
return raw;
|
|
62
|
+
const variant = attrs.variant || "default";
|
|
63
|
+
const parsed = Number(attrs.version);
|
|
64
|
+
const version = Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
65
|
+
const resolved = body({ slug, variant, version });
|
|
66
|
+
const label = `data-mockpit-slot="${escapeAttr(slug)}" data-mockpit-variant="${escapeAttr(variant)}"${version == null ? "" : ` data-mockpit-version="${version}"`}`;
|
|
67
|
+
return resolved == null
|
|
68
|
+
? `<div ${label} data-mockpit-missing="1"></div>`
|
|
69
|
+
: `<div ${label}>${resolved}</div>`;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type Ask, type Asset, type Comment, type CommentQuery, type CommitReplyInput, type CreateAssetInput, type CreateCommentInput, type CreateMockInput, type CreatePostInput, type CreateSessionInput, type Draft, type Knobs, type Mock, type MockKind, type Post, type PostQuery, type PostStatus, type PostVersion, type Session, type Slot, type SqlStorage, type Store, type Surface, type UpdateMockInput, type UpdatePostInput } from "./types.ts";
|
|
2
|
+
export declare class SqlStore implements Store {
|
|
3
|
+
private sql;
|
|
4
|
+
private assetRefCache;
|
|
5
|
+
constructor(sql: SqlStorage);
|
|
6
|
+
private atomic;
|
|
7
|
+
private tables;
|
|
8
|
+
private columns;
|
|
9
|
+
private addMissing;
|
|
10
|
+
private migrate;
|
|
11
|
+
private createIndexes;
|
|
12
|
+
private migrateLegacy;
|
|
13
|
+
private migrateToSurfaces;
|
|
14
|
+
private migrateToPosts;
|
|
15
|
+
private migrateSurfaceIds;
|
|
16
|
+
private migrateToItems;
|
|
17
|
+
private migrateToVersions;
|
|
18
|
+
private migrateToMocks;
|
|
19
|
+
private settingSync;
|
|
20
|
+
private insertVersion;
|
|
21
|
+
private rowToVersion;
|
|
22
|
+
private historiesFor;
|
|
23
|
+
private rowsToPosts;
|
|
24
|
+
private rowToSession;
|
|
25
|
+
private rowToMock;
|
|
26
|
+
private rowToPost;
|
|
27
|
+
private rowToAsset;
|
|
28
|
+
private rowToComment;
|
|
29
|
+
listSessions(): Promise<Session[]>;
|
|
30
|
+
getSession(id: string): Promise<Session | null>;
|
|
31
|
+
getSessionByKey(key: string): Promise<Session | null>;
|
|
32
|
+
createSession(input: CreateSessionInput): Promise<Session>;
|
|
33
|
+
renameSession(id: string, title: string): Promise<Session | null>;
|
|
34
|
+
removeSession(id: string): Promise<boolean>;
|
|
35
|
+
private touch;
|
|
36
|
+
markAgentSeen(sessionId: string, seq: number): Promise<void>;
|
|
37
|
+
getSetting(key: string): Promise<string | null>;
|
|
38
|
+
setSetting(key: string, value: string): Promise<void>;
|
|
39
|
+
listProjects(): Promise<import("./types.ts").ProjectSummary[]>;
|
|
40
|
+
listMocks(project?: string): Promise<Mock[]>;
|
|
41
|
+
getMock(id: string): Promise<Mock | null>;
|
|
42
|
+
findMock(project: string, slug: string): Promise<Mock | null>;
|
|
43
|
+
createMock(input: CreateMockInput): Promise<Mock>;
|
|
44
|
+
updateMock(id: string, patch: UpdateMockInput): Promise<Mock | null>;
|
|
45
|
+
removeMock(id: string): Promise<boolean>;
|
|
46
|
+
putDraft(mockId: string, draft: Draft | null): Promise<{
|
|
47
|
+
draft: Draft | null;
|
|
48
|
+
id: string;
|
|
49
|
+
project: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
title: string;
|
|
52
|
+
kind: MockKind;
|
|
53
|
+
states: string[];
|
|
54
|
+
asks: Ask[];
|
|
55
|
+
knobs: Knobs;
|
|
56
|
+
sessionId: string | null;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
} | null>;
|
|
60
|
+
private touchMock;
|
|
61
|
+
listPosts(query?: PostQuery): Promise<Post[]>;
|
|
62
|
+
countPostsBySession(): Promise<Map<string, number>>;
|
|
63
|
+
getPost(id: string): Promise<Post | null>;
|
|
64
|
+
findPost(mockId: string, state: string | null, variant: string): Promise<Post | null>;
|
|
65
|
+
createPost(input: CreatePostInput): Promise<Post | null>;
|
|
66
|
+
updatePost(id: string, patch: UpdatePostInput): Promise<{
|
|
67
|
+
author?: string | undefined;
|
|
68
|
+
slots: Slot[];
|
|
69
|
+
state: string | null;
|
|
70
|
+
version: number;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
history: PostVersion[];
|
|
73
|
+
from: number;
|
|
74
|
+
prompt: string;
|
|
75
|
+
knobs?: Knobs | undefined;
|
|
76
|
+
title: string;
|
|
77
|
+
surfaces: Surface[];
|
|
78
|
+
id: string;
|
|
79
|
+
sessionId: string;
|
|
80
|
+
mock: string;
|
|
81
|
+
variant: string;
|
|
82
|
+
status: PostStatus;
|
|
83
|
+
createdAt: string;
|
|
84
|
+
} | null>;
|
|
85
|
+
removePost(id: string): Promise<boolean>;
|
|
86
|
+
setPostStatus(id: string, status: PostStatus): Promise<{
|
|
87
|
+
status: PostStatus;
|
|
88
|
+
id: string;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
mock: string;
|
|
91
|
+
state: string | null;
|
|
92
|
+
variant: string;
|
|
93
|
+
title: string;
|
|
94
|
+
surfaces: Surface[];
|
|
95
|
+
createdAt: string;
|
|
96
|
+
updatedAt: string;
|
|
97
|
+
version: number;
|
|
98
|
+
history: PostVersion[];
|
|
99
|
+
knobs?: Knobs;
|
|
100
|
+
slots: Slot[];
|
|
101
|
+
from?: number;
|
|
102
|
+
prompt?: string;
|
|
103
|
+
author?: string;
|
|
104
|
+
} | null>;
|
|
105
|
+
listComments(query: CommentQuery): Promise<Comment[]>;
|
|
106
|
+
private insertComment;
|
|
107
|
+
createComment(input: CreateCommentInput): Promise<Comment | null>;
|
|
108
|
+
removeComment(id: string): Promise<Comment | null>;
|
|
109
|
+
commitReply(input: CommitReplyInput): Promise<Comment | null>;
|
|
110
|
+
private referencedAssetIds;
|
|
111
|
+
private addAssetRefs;
|
|
112
|
+
private invalidateAssetRefs;
|
|
113
|
+
putAsset(input: CreateAssetInput): Promise<Asset | null>;
|
|
114
|
+
getAsset(id: string): Promise<Asset | null>;
|
|
115
|
+
touchAsset(id: string): Promise<void>;
|
|
116
|
+
listAssets(sessionId: string): Promise<Asset[]>;
|
|
117
|
+
removeAsset(id: string): Promise<boolean>;
|
|
118
|
+
isAssetReferenced(id: string): Promise<boolean>;
|
|
119
|
+
}
|