@ikenga/pkg-studio 0.2.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 +201 -0
- package/README.md +48 -0
- package/dist/assets/index-BhGjiVVg.js +222 -0
- package/dist/assets/index-BhGjiVVg.js.map +1 -0
- package/dist/assets/index-CU3hFKgx.css +1 -0
- package/dist/index.html +19 -0
- package/manifest.json +219 -0
- package/mcp/dist/index.js +1535 -0
- package/package.json +64 -0
- package/sidecars/project/dist/sidecar.js +235319 -0
|
@@ -0,0 +1,1535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
McpError
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
|
|
12
|
+
// src/sidecar-client.ts
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { createInterface } from "node:readline";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
18
|
+
function defaultSidecarPath() {
|
|
19
|
+
if (process.env.STUDIO_SIDECAR_PATH)
|
|
20
|
+
return process.env.STUDIO_SIDECAR_PATH;
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
return resolvePath(here, "../../sidecars/project/dist/sidecar.js");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class SidecarUnavailableError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "SidecarUnavailableError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class SidecarRpcError extends Error {
|
|
33
|
+
code;
|
|
34
|
+
data;
|
|
35
|
+
constructor(code, message, data) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "SidecarRpcError";
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.data = data;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class SidecarClient {
|
|
44
|
+
child = null;
|
|
45
|
+
rl = null;
|
|
46
|
+
nextId = 1;
|
|
47
|
+
pending = new Map;
|
|
48
|
+
listeners = new Set;
|
|
49
|
+
respawnAttempts = 0;
|
|
50
|
+
respawning = false;
|
|
51
|
+
sidecarPath;
|
|
52
|
+
env;
|
|
53
|
+
maxRespawnAttempts;
|
|
54
|
+
deadReason = null;
|
|
55
|
+
shuttingDown = false;
|
|
56
|
+
constructor(opts = {}) {
|
|
57
|
+
this.sidecarPath = opts.sidecarPath ?? defaultSidecarPath();
|
|
58
|
+
this.env = { ...process.env, ...opts.env ?? {} };
|
|
59
|
+
this.maxRespawnAttempts = opts.maxRespawnAttempts ?? 3;
|
|
60
|
+
}
|
|
61
|
+
start() {
|
|
62
|
+
if (this.child)
|
|
63
|
+
return;
|
|
64
|
+
this.spawnChild();
|
|
65
|
+
}
|
|
66
|
+
onEvent(fn) {
|
|
67
|
+
this.listeners.add(fn);
|
|
68
|
+
return () => this.listeners.delete(fn);
|
|
69
|
+
}
|
|
70
|
+
async call(method, params, opts = {}) {
|
|
71
|
+
if (this.deadReason && !this.child) {
|
|
72
|
+
throw new SidecarUnavailableError(this.deadReason);
|
|
73
|
+
}
|
|
74
|
+
if (!this.child)
|
|
75
|
+
this.spawnChild();
|
|
76
|
+
const child = this.child;
|
|
77
|
+
if (!child || !child.stdin || child.stdin.destroyed) {
|
|
78
|
+
throw new SidecarUnavailableError("sidecar stdin not writable");
|
|
79
|
+
}
|
|
80
|
+
const id = this.nextId++;
|
|
81
|
+
const req = { jsonrpc: "2.0", id, method, params };
|
|
82
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
this.pending.delete(id);
|
|
86
|
+
reject(new Error(`sidecar call '${method}' timed out after ${timeoutMs}ms`));
|
|
87
|
+
}, timeoutMs);
|
|
88
|
+
this.pending.set(id, {
|
|
89
|
+
resolve: (v) => resolve(v),
|
|
90
|
+
reject,
|
|
91
|
+
timer
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
child.stdin.write(JSON.stringify(req) + `
|
|
95
|
+
`);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
this.pending.delete(id);
|
|
99
|
+
reject(new SidecarUnavailableError(`sidecar write failed: ${e.message}`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async stop() {
|
|
104
|
+
this.shuttingDown = true;
|
|
105
|
+
if (!this.child)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
this.child.stdin?.end();
|
|
109
|
+
} catch {}
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
const t = setTimeout(() => {
|
|
112
|
+
try {
|
|
113
|
+
this.child?.kill("SIGTERM");
|
|
114
|
+
} catch {}
|
|
115
|
+
resolve();
|
|
116
|
+
}, 1500);
|
|
117
|
+
this.child?.once("exit", () => {
|
|
118
|
+
clearTimeout(t);
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
spawnChild() {
|
|
124
|
+
if (!existsSync(this.sidecarPath)) {
|
|
125
|
+
this.deadReason = `sidecar binary not found at ${this.sidecarPath}`;
|
|
126
|
+
process.stderr.write(`[studio-mcp] ${this.deadReason}
|
|
127
|
+
`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
process.stderr.write(`[studio-mcp] spawning sidecar: node ${this.sidecarPath}
|
|
131
|
+
`);
|
|
132
|
+
const child = spawn(process.execPath, [this.sidecarPath], {
|
|
133
|
+
env: this.env,
|
|
134
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
135
|
+
});
|
|
136
|
+
this.child = child;
|
|
137
|
+
this.deadReason = null;
|
|
138
|
+
child.stderr?.on("data", (b) => {
|
|
139
|
+
process.stderr.write(b);
|
|
140
|
+
});
|
|
141
|
+
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
142
|
+
this.rl = rl;
|
|
143
|
+
rl.on("line", (line) => {
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
if (!trimmed)
|
|
146
|
+
return;
|
|
147
|
+
let msg;
|
|
148
|
+
try {
|
|
149
|
+
msg = JSON.parse(trimmed);
|
|
150
|
+
} catch {
|
|
151
|
+
process.stderr.write(`[studio-mcp] sidecar emitted non-JSON line: ${trimmed.slice(0, 200)}
|
|
152
|
+
`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.routeMessage(msg);
|
|
156
|
+
});
|
|
157
|
+
child.on("error", (err) => {
|
|
158
|
+
process.stderr.write(`[studio-mcp] sidecar spawn error: ${err.message}
|
|
159
|
+
`);
|
|
160
|
+
});
|
|
161
|
+
child.on("exit", (code, signal) => {
|
|
162
|
+
process.stderr.write(`[studio-mcp] sidecar exited code=${code} signal=${signal}
|
|
163
|
+
`);
|
|
164
|
+
const reason = `sidecar exited (code=${code} signal=${signal})`;
|
|
165
|
+
for (const [, p] of this.pending) {
|
|
166
|
+
clearTimeout(p.timer);
|
|
167
|
+
p.reject(new SidecarUnavailableError(reason));
|
|
168
|
+
}
|
|
169
|
+
this.pending.clear();
|
|
170
|
+
this.child = null;
|
|
171
|
+
this.rl?.close();
|
|
172
|
+
this.rl = null;
|
|
173
|
+
if (this.shuttingDown)
|
|
174
|
+
return;
|
|
175
|
+
this.attemptRespawn();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
routeMessage(msg) {
|
|
179
|
+
if ("method" in msg && msg.method === "event" && "params" in msg) {
|
|
180
|
+
const params = msg.params;
|
|
181
|
+
for (const l of this.listeners) {
|
|
182
|
+
try {
|
|
183
|
+
l(params);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
process.stderr.write(`[studio-mcp] event listener threw: ${err.message}
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const resp = msg;
|
|
192
|
+
if (resp.id == null || typeof resp.id !== "number")
|
|
193
|
+
return;
|
|
194
|
+
const slot = this.pending.get(resp.id);
|
|
195
|
+
if (!slot)
|
|
196
|
+
return;
|
|
197
|
+
this.pending.delete(resp.id);
|
|
198
|
+
clearTimeout(slot.timer);
|
|
199
|
+
if (resp.error) {
|
|
200
|
+
slot.reject(new SidecarRpcError(resp.error.code, resp.error.message, resp.error.data));
|
|
201
|
+
} else {
|
|
202
|
+
slot.resolve(resp.result);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async attemptRespawn() {
|
|
206
|
+
if (this.respawning)
|
|
207
|
+
return;
|
|
208
|
+
this.respawning = true;
|
|
209
|
+
try {
|
|
210
|
+
while (this.respawnAttempts < this.maxRespawnAttempts) {
|
|
211
|
+
this.respawnAttempts++;
|
|
212
|
+
const backoff = Math.min(2000 * 2 ** (this.respawnAttempts - 1), 8000);
|
|
213
|
+
process.stderr.write(`[studio-mcp] sidecar respawn attempt ${this.respawnAttempts}/${this.maxRespawnAttempts} in ${backoff}ms
|
|
214
|
+
`);
|
|
215
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
216
|
+
if (this.shuttingDown)
|
|
217
|
+
return;
|
|
218
|
+
this.spawnChild();
|
|
219
|
+
if (this.child) {
|
|
220
|
+
this.respawnAttempts = 0;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.deadReason = `sidecar respawn gave up after ${this.maxRespawnAttempts} attempts`;
|
|
225
|
+
process.stderr.write(`[studio-mcp] ${this.deadReason}
|
|
226
|
+
`);
|
|
227
|
+
} finally {
|
|
228
|
+
this.respawning = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/catalog.ts
|
|
234
|
+
import { existsSync as existsSync2, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
235
|
+
import { dirname as dirname2, join, resolve as resolvePath2 } from "node:path";
|
|
236
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
237
|
+
import { createRequire } from "node:module";
|
|
238
|
+
function studioPkgRoot() {
|
|
239
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
240
|
+
return resolvePath2(here, "..", "..");
|
|
241
|
+
}
|
|
242
|
+
function resolveArchetypesPkgSkills() {
|
|
243
|
+
try {
|
|
244
|
+
const req = createRequire(import.meta.url);
|
|
245
|
+
const manifest = req.resolve("@ikenga/studio-archetypes/manifest.json");
|
|
246
|
+
return join(dirname2(manifest), "skills");
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function archetypeSkillRoots(pkgRoot = studioPkgRoot()) {
|
|
252
|
+
const roots = [];
|
|
253
|
+
const push = (dir) => {
|
|
254
|
+
if (dir && existsSync2(dir) && !roots.includes(dir))
|
|
255
|
+
roots.push(dir);
|
|
256
|
+
};
|
|
257
|
+
push(join(pkgRoot, "skills"));
|
|
258
|
+
push(resolveArchetypesPkgSkills());
|
|
259
|
+
push(resolvePath2(pkgRoot, "..", "..", "skills", "studio-archetypes", "skills"));
|
|
260
|
+
return roots;
|
|
261
|
+
}
|
|
262
|
+
function walkForFiles(root, filename, maxDepth = 6) {
|
|
263
|
+
const hits = [];
|
|
264
|
+
if (!existsSync2(root))
|
|
265
|
+
return hits;
|
|
266
|
+
function recurse(dir, depth) {
|
|
267
|
+
if (depth > maxDepth)
|
|
268
|
+
return;
|
|
269
|
+
let entries;
|
|
270
|
+
try {
|
|
271
|
+
entries = readdirSync(dir);
|
|
272
|
+
} catch {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
for (const e of entries) {
|
|
276
|
+
const full = join(dir, e);
|
|
277
|
+
let s;
|
|
278
|
+
try {
|
|
279
|
+
s = statSync(full);
|
|
280
|
+
} catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (s.isDirectory()) {
|
|
284
|
+
recurse(full, depth + 1);
|
|
285
|
+
} else if (s.isFile() && e === filename) {
|
|
286
|
+
hits.push(full);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
recurse(root, 0);
|
|
291
|
+
return hits;
|
|
292
|
+
}
|
|
293
|
+
function safeParseJson(path) {
|
|
294
|
+
try {
|
|
295
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
296
|
+
} catch (e) {
|
|
297
|
+
process.stderr.write(`[studio-mcp][catalog] failed to parse ${path}: ${e.message}
|
|
298
|
+
`);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function loadBuiltinBlocks(pkgRoot = studioPkgRoot()) {
|
|
303
|
+
const entries = [];
|
|
304
|
+
const seen = new Set;
|
|
305
|
+
for (const skillsDir of archetypeSkillRoots(pkgRoot)) {
|
|
306
|
+
let skillNames;
|
|
307
|
+
try {
|
|
308
|
+
skillNames = readdirSync(skillsDir);
|
|
309
|
+
} catch {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
for (const skill of skillNames) {
|
|
313
|
+
const blocksDir = join(skillsDir, skill, "blocks");
|
|
314
|
+
if (!existsSync2(blocksDir))
|
|
315
|
+
continue;
|
|
316
|
+
for (const file of walkForFiles(blocksDir, "block.json")) {
|
|
317
|
+
const body = safeParseJson(file);
|
|
318
|
+
if (!body)
|
|
319
|
+
continue;
|
|
320
|
+
const block_id = body.block_id ?? body.id;
|
|
321
|
+
if (!block_id || seen.has(block_id))
|
|
322
|
+
continue;
|
|
323
|
+
seen.add(block_id);
|
|
324
|
+
entries.push({
|
|
325
|
+
block_id,
|
|
326
|
+
kind: body.kind,
|
|
327
|
+
name: body.name,
|
|
328
|
+
tags: Array.isArray(body.tags) ? body.tags : undefined,
|
|
329
|
+
source: "builtin",
|
|
330
|
+
path: file,
|
|
331
|
+
body
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return entries;
|
|
337
|
+
}
|
|
338
|
+
function loadBuiltinArchetypes(pkgRoot = studioPkgRoot()) {
|
|
339
|
+
const entries = [];
|
|
340
|
+
const seen = new Set;
|
|
341
|
+
for (const skillsDir of archetypeSkillRoots(pkgRoot)) {
|
|
342
|
+
let skillNames;
|
|
343
|
+
try {
|
|
344
|
+
skillNames = readdirSync(skillsDir);
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
for (const skill of skillNames) {
|
|
349
|
+
if (!skill.startsWith("archetype-"))
|
|
350
|
+
continue;
|
|
351
|
+
const file = join(skillsDir, skill, "archetype.json");
|
|
352
|
+
if (!existsSync2(file))
|
|
353
|
+
continue;
|
|
354
|
+
const body = safeParseJson(file);
|
|
355
|
+
if (!body)
|
|
356
|
+
continue;
|
|
357
|
+
const archetype_id = body.archetype_id ?? body.id;
|
|
358
|
+
if (!archetype_id || seen.has(archetype_id))
|
|
359
|
+
continue;
|
|
360
|
+
seen.add(archetype_id);
|
|
361
|
+
entries.push({
|
|
362
|
+
archetype_id,
|
|
363
|
+
name: body.name,
|
|
364
|
+
source: "builtin",
|
|
365
|
+
path: file,
|
|
366
|
+
body
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return entries;
|
|
371
|
+
}
|
|
372
|
+
function loadCustomBlocks(projectRoot) {
|
|
373
|
+
const customDir = join(projectRoot, "blocks", "custom");
|
|
374
|
+
const entries = [];
|
|
375
|
+
for (const file of walkForFiles(customDir, "block.json")) {
|
|
376
|
+
const body = safeParseJson(file);
|
|
377
|
+
if (!body)
|
|
378
|
+
continue;
|
|
379
|
+
const block_id = body.block_id ?? body.id;
|
|
380
|
+
if (!block_id)
|
|
381
|
+
continue;
|
|
382
|
+
entries.push({
|
|
383
|
+
block_id,
|
|
384
|
+
kind: body.kind,
|
|
385
|
+
name: body.name,
|
|
386
|
+
tags: Array.isArray(body.tags) ? body.tags : undefined,
|
|
387
|
+
source: "custom",
|
|
388
|
+
path: file,
|
|
389
|
+
body
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return entries;
|
|
393
|
+
}
|
|
394
|
+
function loadCustomArchetypes(projectRoot) {
|
|
395
|
+
const customDir = join(projectRoot, "archetypes");
|
|
396
|
+
const entries = [];
|
|
397
|
+
if (!existsSync2(customDir))
|
|
398
|
+
return entries;
|
|
399
|
+
for (const file of walkForFiles(customDir, "archetype.json")) {
|
|
400
|
+
const body = safeParseJson(file);
|
|
401
|
+
if (!body)
|
|
402
|
+
continue;
|
|
403
|
+
const archetype_id = body.archetype_id ?? body.id;
|
|
404
|
+
if (!archetype_id)
|
|
405
|
+
continue;
|
|
406
|
+
entries.push({
|
|
407
|
+
archetype_id,
|
|
408
|
+
name: body.name,
|
|
409
|
+
source: "custom",
|
|
410
|
+
path: file,
|
|
411
|
+
body
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return entries;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
class Catalog {
|
|
418
|
+
builtinBlocks;
|
|
419
|
+
builtinArchetypes;
|
|
420
|
+
customBlocksByProject = new Map;
|
|
421
|
+
customArchetypesByProject = new Map;
|
|
422
|
+
constructor() {
|
|
423
|
+
this.builtinBlocks = loadBuiltinBlocks();
|
|
424
|
+
this.builtinArchetypes = loadBuiltinArchetypes();
|
|
425
|
+
}
|
|
426
|
+
refreshForProject(projectId, projectRoot) {
|
|
427
|
+
this.customBlocksByProject.set(projectId, loadCustomBlocks(projectRoot));
|
|
428
|
+
this.customArchetypesByProject.set(projectId, loadCustomArchetypes(projectRoot));
|
|
429
|
+
}
|
|
430
|
+
dropProject(projectId) {
|
|
431
|
+
this.customBlocksByProject.delete(projectId);
|
|
432
|
+
this.customArchetypesByProject.delete(projectId);
|
|
433
|
+
}
|
|
434
|
+
listBlocks(opts = {}) {
|
|
435
|
+
const merged = [
|
|
436
|
+
...this.builtinBlocks,
|
|
437
|
+
...opts.projectId ? this.customBlocksByProject.get(opts.projectId) ?? [] : []
|
|
438
|
+
];
|
|
439
|
+
return merged.filter((b) => {
|
|
440
|
+
if (opts.kind && b.kind !== opts.kind)
|
|
441
|
+
return false;
|
|
442
|
+
if (opts.tags && opts.tags.length > 0) {
|
|
443
|
+
const have = new Set(b.tags ?? []);
|
|
444
|
+
if (!opts.tags.some((t) => have.has(t)))
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
getBlock(block_id, projectId) {
|
|
451
|
+
return this.listBlocks({ projectId }).find((b) => b.block_id === block_id);
|
|
452
|
+
}
|
|
453
|
+
listArchetypes(opts = {}) {
|
|
454
|
+
return [
|
|
455
|
+
...this.builtinArchetypes,
|
|
456
|
+
...opts.projectId ? this.customArchetypesByProject.get(opts.projectId) ?? [] : []
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
getArchetype(archetype_id, projectId) {
|
|
460
|
+
return this.listArchetypes({ projectId }).find((a) => a.archetype_id === archetype_id);
|
|
461
|
+
}
|
|
462
|
+
writeCustomBlock(projectRoot, projectId, block) {
|
|
463
|
+
const block_id = block.block_id ?? block.id;
|
|
464
|
+
if (!block_id)
|
|
465
|
+
throw new Error("block.block_id is required");
|
|
466
|
+
const dir = join(projectRoot, "blocks", "custom", block_id);
|
|
467
|
+
mkdirSync(dir, { recursive: true });
|
|
468
|
+
const file = join(dir, "block.json");
|
|
469
|
+
writeFileSync(file, JSON.stringify(block, null, 2) + `
|
|
470
|
+
`, "utf8");
|
|
471
|
+
this.refreshForProject(projectId, projectRoot);
|
|
472
|
+
const found = this.getBlock(block_id, projectId);
|
|
473
|
+
if (!found)
|
|
474
|
+
throw new Error(`failed to register block ${block_id} after write`);
|
|
475
|
+
return found;
|
|
476
|
+
}
|
|
477
|
+
deleteCustomBlock(projectRoot, projectId, block_id) {
|
|
478
|
+
const entry = this.getBlock(block_id, projectId);
|
|
479
|
+
if (!entry)
|
|
480
|
+
return { ok: false, reason: "not-found" };
|
|
481
|
+
if (entry.source === "builtin")
|
|
482
|
+
return { ok: false, reason: "cannot-delete-builtin" };
|
|
483
|
+
try {
|
|
484
|
+
const dir = dirname2(entry.path);
|
|
485
|
+
rmSync(dir, { recursive: true, force: true });
|
|
486
|
+
} catch (e) {
|
|
487
|
+
return { ok: false, reason: e.message };
|
|
488
|
+
}
|
|
489
|
+
this.refreshForProject(projectId, projectRoot);
|
|
490
|
+
return { ok: true };
|
|
491
|
+
}
|
|
492
|
+
writeCustomArchetype(projectRoot, projectId, archetype) {
|
|
493
|
+
const archetype_id = archetype.archetype_id ?? archetype.id;
|
|
494
|
+
if (!archetype_id)
|
|
495
|
+
throw new Error("archetype.archetype_id is required");
|
|
496
|
+
const dir = join(projectRoot, "archetypes", archetype_id);
|
|
497
|
+
mkdirSync(dir, { recursive: true });
|
|
498
|
+
const file = join(dir, "archetype.json");
|
|
499
|
+
writeFileSync(file, JSON.stringify(archetype, null, 2) + `
|
|
500
|
+
`, "utf8");
|
|
501
|
+
this.refreshForProject(projectId, projectRoot);
|
|
502
|
+
const found = this.getArchetype(archetype_id, projectId);
|
|
503
|
+
if (!found)
|
|
504
|
+
throw new Error(`failed to register archetype ${archetype_id} after write`);
|
|
505
|
+
return found;
|
|
506
|
+
}
|
|
507
|
+
deleteCustomArchetype(projectRoot, projectId, archetype_id) {
|
|
508
|
+
const entry = this.getArchetype(archetype_id, projectId);
|
|
509
|
+
if (!entry)
|
|
510
|
+
return { ok: false, reason: "not-found" };
|
|
511
|
+
if (entry.source === "builtin")
|
|
512
|
+
return { ok: false, reason: "cannot-delete-builtin" };
|
|
513
|
+
try {
|
|
514
|
+
const dir = dirname2(entry.path);
|
|
515
|
+
rmSync(dir, { recursive: true, force: true });
|
|
516
|
+
} catch (e) {
|
|
517
|
+
return { ok: false, reason: e.message };
|
|
518
|
+
}
|
|
519
|
+
this.refreshForProject(projectId, projectRoot);
|
|
520
|
+
return { ok: true };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/tools/project.ts
|
|
525
|
+
async function callSidecar(sidecar, method, params) {
|
|
526
|
+
try {
|
|
527
|
+
const r = await sidecar.call(method, params);
|
|
528
|
+
return r;
|
|
529
|
+
} catch (e) {
|
|
530
|
+
if (e instanceof SidecarUnavailableError) {
|
|
531
|
+
return { ok: false, error: "sidecar-unavailable", message: e.message };
|
|
532
|
+
}
|
|
533
|
+
if (e instanceof SidecarRpcError) {
|
|
534
|
+
return { ok: false, error: "sidecar-rpc-error", message: `${e.code}: ${e.message}` };
|
|
535
|
+
}
|
|
536
|
+
return { ok: false, error: "internal-error", message: e.message };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function projectTools(sidecar, registry) {
|
|
540
|
+
return [
|
|
541
|
+
{
|
|
542
|
+
name: "project.open",
|
|
543
|
+
description: "Open a project on disk. Returns projectId + parsed Project.",
|
|
544
|
+
inputSchema: {
|
|
545
|
+
type: "object",
|
|
546
|
+
properties: {
|
|
547
|
+
path: { type: "string", description: "Absolute or cwd-relative project root path." }
|
|
548
|
+
},
|
|
549
|
+
required: ["path"],
|
|
550
|
+
additionalProperties: false
|
|
551
|
+
},
|
|
552
|
+
async handler(args) {
|
|
553
|
+
const r = await callSidecar(sidecar, "project.open", { path: args.path });
|
|
554
|
+
if (r.ok) {
|
|
555
|
+
const rec = r;
|
|
556
|
+
if (typeof rec.projectId === "string") {
|
|
557
|
+
registry.set(rec.projectId, {
|
|
558
|
+
path: args.path,
|
|
559
|
+
project: rec.project
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return r;
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "project.close",
|
|
568
|
+
description: "Close an open project; releases its FS watcher + LRU entries.",
|
|
569
|
+
inputSchema: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: { projectId: { type: "string" } },
|
|
572
|
+
required: ["projectId"],
|
|
573
|
+
additionalProperties: false
|
|
574
|
+
},
|
|
575
|
+
async handler(args) {
|
|
576
|
+
const r = await callSidecar(sidecar, "project.close", { projectId: args.projectId });
|
|
577
|
+
if (r.ok)
|
|
578
|
+
registry.delete(args.projectId);
|
|
579
|
+
return r;
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "project.list",
|
|
584
|
+
description: "List previously-opened projects (most recent first).",
|
|
585
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
586
|
+
async handler() {
|
|
587
|
+
return callSidecar(sidecar, "project.list", undefined);
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "project.create",
|
|
592
|
+
description: "Scaffold a new project on disk from an archetype id, then open it.",
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: "object",
|
|
595
|
+
properties: {
|
|
596
|
+
archetype_id: { type: "string" },
|
|
597
|
+
path: { type: "string", description: "Project root directory to create." },
|
|
598
|
+
name: { type: "string", description: "Human-readable project title." }
|
|
599
|
+
},
|
|
600
|
+
required: ["archetype_id", "path", "name"],
|
|
601
|
+
additionalProperties: false
|
|
602
|
+
},
|
|
603
|
+
async handler(args) {
|
|
604
|
+
const r = await callSidecar(sidecar, "project.create", {
|
|
605
|
+
archetype_id: args.archetype_id,
|
|
606
|
+
path: args.path,
|
|
607
|
+
name: args.name
|
|
608
|
+
});
|
|
609
|
+
if (r.ok) {
|
|
610
|
+
const rec = r;
|
|
611
|
+
if (typeof rec.projectId === "string") {
|
|
612
|
+
registry.set(rec.projectId, {
|
|
613
|
+
path: args.path,
|
|
614
|
+
project: rec.project
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return r;
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: "project.info",
|
|
623
|
+
description: "Fetch live project info: parsed Project + LRU openCells + queueDepth.",
|
|
624
|
+
inputSchema: {
|
|
625
|
+
type: "object",
|
|
626
|
+
properties: { projectId: { type: "string" } },
|
|
627
|
+
required: ["projectId"],
|
|
628
|
+
additionalProperties: false
|
|
629
|
+
},
|
|
630
|
+
async handler(args) {
|
|
631
|
+
return callSidecar(sidecar, "project.info", { projectId: args.projectId });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/tools/storyboard.ts
|
|
638
|
+
function storyboardTools(sidecar) {
|
|
639
|
+
return [
|
|
640
|
+
{
|
|
641
|
+
name: "storyboard.read",
|
|
642
|
+
description: "Read the full storyboard for an open project. Returns the parsed Project + cell index.",
|
|
643
|
+
inputSchema: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: { projectId: { type: "string" } },
|
|
646
|
+
required: ["projectId"],
|
|
647
|
+
additionalProperties: false
|
|
648
|
+
},
|
|
649
|
+
handler: (args) => callSidecar(sidecar, "storyboard.read", { projectId: args.projectId })
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "storyboard.read_cell",
|
|
653
|
+
description: "Read a single cell by id.",
|
|
654
|
+
inputSchema: {
|
|
655
|
+
type: "object",
|
|
656
|
+
properties: {
|
|
657
|
+
projectId: { type: "string" },
|
|
658
|
+
cellId: { type: "string" }
|
|
659
|
+
},
|
|
660
|
+
required: ["projectId", "cellId"],
|
|
661
|
+
additionalProperties: false
|
|
662
|
+
},
|
|
663
|
+
handler: (args) => callSidecar(sidecar, "storyboard.read_cell", { projectId: args.projectId, cellId: args.cellId })
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "storyboard.read_fountain",
|
|
667
|
+
description: "Read the project's Fountain screenplay source at <root>/script.fountain. Returns { exists, text }. `exists:false` (empty text) means the project has no script.fountain on disk yet — not an error.",
|
|
668
|
+
inputSchema: {
|
|
669
|
+
type: "object",
|
|
670
|
+
properties: { projectId: { type: "string" } },
|
|
671
|
+
required: ["projectId"],
|
|
672
|
+
additionalProperties: false
|
|
673
|
+
},
|
|
674
|
+
handler: (args) => callSidecar(sidecar, "storyboard.read_fountain", { projectId: args.projectId })
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: "storyboard.read_cell_content",
|
|
678
|
+
description: "Read a cell's authored source file (the markup at its content_path). Returns { html, content_path, exists }. `exists:false` (empty html) means the cell has no source written yet.",
|
|
679
|
+
inputSchema: {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties: {
|
|
682
|
+
projectId: { type: "string" },
|
|
683
|
+
cellId: { type: "string" }
|
|
684
|
+
},
|
|
685
|
+
required: ["projectId", "cellId"],
|
|
686
|
+
additionalProperties: false
|
|
687
|
+
},
|
|
688
|
+
handler: (args) => callSidecar(sidecar, "storyboard.read_cell_content", {
|
|
689
|
+
projectId: args.projectId,
|
|
690
|
+
cellId: args.cellId
|
|
691
|
+
})
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: "storyboard.write_cell_content",
|
|
695
|
+
description: "Persist a cell's authored source file (the FULL edited html) to its content_path and bump last_edited (the project FS watcher surfaces the change). This is the durable save seam for the cell editor.",
|
|
696
|
+
inputSchema: {
|
|
697
|
+
type: "object",
|
|
698
|
+
properties: {
|
|
699
|
+
projectId: { type: "string" },
|
|
700
|
+
cellId: { type: "string" },
|
|
701
|
+
html: { type: "string" }
|
|
702
|
+
},
|
|
703
|
+
required: ["projectId", "cellId", "html"],
|
|
704
|
+
additionalProperties: false
|
|
705
|
+
},
|
|
706
|
+
handler: (args) => callSidecar(sidecar, "storyboard.write_cell_content", {
|
|
707
|
+
projectId: args.projectId,
|
|
708
|
+
cellId: args.cellId,
|
|
709
|
+
html: args.html
|
|
710
|
+
})
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: "storyboard.write_cell",
|
|
714
|
+
description: "Overwrite a cell. Emits cells/changed on success.",
|
|
715
|
+
inputSchema: {
|
|
716
|
+
type: "object",
|
|
717
|
+
properties: {
|
|
718
|
+
projectId: { type: "string" },
|
|
719
|
+
cell: { type: "object", additionalProperties: true }
|
|
720
|
+
},
|
|
721
|
+
required: ["projectId", "cell"],
|
|
722
|
+
additionalProperties: false
|
|
723
|
+
},
|
|
724
|
+
handler: (args) => callSidecar(sidecar, "storyboard.write_cell", { projectId: args.projectId, cell: args.cell })
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: "storyboard.create_cell",
|
|
728
|
+
description: "Create a new cell. Emits cells/changed on success.",
|
|
729
|
+
inputSchema: {
|
|
730
|
+
type: "object",
|
|
731
|
+
properties: {
|
|
732
|
+
projectId: { type: "string" },
|
|
733
|
+
cell: { type: "object", additionalProperties: true }
|
|
734
|
+
},
|
|
735
|
+
required: ["projectId", "cell"],
|
|
736
|
+
additionalProperties: false
|
|
737
|
+
},
|
|
738
|
+
handler: (args) => callSidecar(sidecar, "storyboard.create_cell", { projectId: args.projectId, cell: args.cell })
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: "storyboard.delete_cell",
|
|
742
|
+
description: "Delete a cell by id.",
|
|
743
|
+
inputSchema: {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties: {
|
|
746
|
+
projectId: { type: "string" },
|
|
747
|
+
cellId: { type: "string" }
|
|
748
|
+
},
|
|
749
|
+
required: ["projectId", "cellId"],
|
|
750
|
+
additionalProperties: false
|
|
751
|
+
},
|
|
752
|
+
handler: (args) => callSidecar(sidecar, "storyboard.delete_cell", { projectId: args.projectId, cellId: args.cellId })
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
name: "storyboard.list_cells",
|
|
756
|
+
description: "List cells in an open project. Optional beat_id + rung filters.",
|
|
757
|
+
inputSchema: {
|
|
758
|
+
type: "object",
|
|
759
|
+
properties: {
|
|
760
|
+
projectId: { type: "string" },
|
|
761
|
+
beat_id: { type: "string" },
|
|
762
|
+
rung: { type: "string", enum: ["0_beat_sheet", "1_lofi", "2_hifi"] }
|
|
763
|
+
},
|
|
764
|
+
required: ["projectId"],
|
|
765
|
+
additionalProperties: false
|
|
766
|
+
},
|
|
767
|
+
handler: (args) => callSidecar(sidecar, "storyboard.list_cells", {
|
|
768
|
+
projectId: args.projectId,
|
|
769
|
+
beat_id: args.beat_id,
|
|
770
|
+
rung: args.rung
|
|
771
|
+
})
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "storyboard.upsert_beat",
|
|
775
|
+
description: "Insert or update a beat (script-level) on the project.",
|
|
776
|
+
inputSchema: {
|
|
777
|
+
type: "object",
|
|
778
|
+
properties: {
|
|
779
|
+
projectId: { type: "string" },
|
|
780
|
+
beat: { type: "object", additionalProperties: true }
|
|
781
|
+
},
|
|
782
|
+
required: ["projectId", "beat"],
|
|
783
|
+
additionalProperties: false
|
|
784
|
+
},
|
|
785
|
+
handler: (args) => callSidecar(sidecar, "storyboard.upsert_beat", { projectId: args.projectId, beat: args.beat })
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
name: "storyboard.upsert_rung",
|
|
789
|
+
description: "Insert or update a per-rung block (beatsheet/lofi/hifi) on a cell. Defaults to the cell's own rung when rungKey is omitted.",
|
|
790
|
+
inputSchema: {
|
|
791
|
+
type: "object",
|
|
792
|
+
properties: {
|
|
793
|
+
projectId: { type: "string" },
|
|
794
|
+
cellId: { type: "string" },
|
|
795
|
+
rung: { type: "object", additionalProperties: true },
|
|
796
|
+
rungKey: { type: "string", enum: ["0_beat_sheet", "1_lofi", "2_hifi"] }
|
|
797
|
+
},
|
|
798
|
+
required: ["projectId", "cellId", "rung"],
|
|
799
|
+
additionalProperties: false
|
|
800
|
+
},
|
|
801
|
+
handler: (args) => callSidecar(sidecar, "storyboard.upsert_rung", {
|
|
802
|
+
projectId: args.projectId,
|
|
803
|
+
cellId: args.cellId,
|
|
804
|
+
rung: args.rung,
|
|
805
|
+
rungKey: args.rungKey
|
|
806
|
+
})
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: "storyboard.set_approved",
|
|
810
|
+
description: "Flip the approved boolean on a cell.",
|
|
811
|
+
inputSchema: {
|
|
812
|
+
type: "object",
|
|
813
|
+
properties: {
|
|
814
|
+
projectId: { type: "string" },
|
|
815
|
+
cellId: { type: "string" },
|
|
816
|
+
approved: { type: "boolean" }
|
|
817
|
+
},
|
|
818
|
+
required: ["projectId", "cellId", "approved"],
|
|
819
|
+
additionalProperties: false
|
|
820
|
+
},
|
|
821
|
+
handler: (args) => callSidecar(sidecar, "storyboard.set_approved", {
|
|
822
|
+
projectId: args.projectId,
|
|
823
|
+
cellId: args.cellId,
|
|
824
|
+
approved: args.approved
|
|
825
|
+
})
|
|
826
|
+
}
|
|
827
|
+
];
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/tools/anchor.ts
|
|
831
|
+
function anchorTools(sidecar) {
|
|
832
|
+
return [
|
|
833
|
+
{
|
|
834
|
+
name: "anchor.list",
|
|
835
|
+
description: "List all anchors in the project.",
|
|
836
|
+
inputSchema: {
|
|
837
|
+
type: "object",
|
|
838
|
+
properties: { projectId: { type: "string" } },
|
|
839
|
+
required: ["projectId"],
|
|
840
|
+
additionalProperties: false
|
|
841
|
+
},
|
|
842
|
+
handler: (args) => callSidecar(sidecar, "anchor.list", { projectId: args.projectId })
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: "anchor.create",
|
|
846
|
+
description: "Create a new anchor (project-scoped reusable asset reference).",
|
|
847
|
+
inputSchema: {
|
|
848
|
+
type: "object",
|
|
849
|
+
properties: {
|
|
850
|
+
projectId: { type: "string" },
|
|
851
|
+
anchor: { type: "object", additionalProperties: true }
|
|
852
|
+
},
|
|
853
|
+
required: ["projectId", "anchor"],
|
|
854
|
+
additionalProperties: false
|
|
855
|
+
},
|
|
856
|
+
handler: (args) => callSidecar(sidecar, "anchor.create", { projectId: args.projectId, anchor: args.anchor })
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "anchor.delete",
|
|
860
|
+
description: "Delete an anchor by id.",
|
|
861
|
+
inputSchema: {
|
|
862
|
+
type: "object",
|
|
863
|
+
properties: {
|
|
864
|
+
projectId: { type: "string" },
|
|
865
|
+
anchorId: { type: "string" }
|
|
866
|
+
},
|
|
867
|
+
required: ["projectId", "anchorId"],
|
|
868
|
+
additionalProperties: false
|
|
869
|
+
},
|
|
870
|
+
handler: (args) => callSidecar(sidecar, "anchor.delete", { projectId: args.projectId, anchorId: args.anchorId })
|
|
871
|
+
}
|
|
872
|
+
];
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/tools/asset.ts
|
|
876
|
+
function assetTools(sidecar) {
|
|
877
|
+
return [
|
|
878
|
+
{
|
|
879
|
+
name: "asset.list",
|
|
880
|
+
description: "List assets in a project. Optional `kind` filter (image, video, audio, font).",
|
|
881
|
+
inputSchema: {
|
|
882
|
+
type: "object",
|
|
883
|
+
properties: {
|
|
884
|
+
projectId: { type: "string" },
|
|
885
|
+
kind: { type: "string" }
|
|
886
|
+
},
|
|
887
|
+
required: ["projectId"],
|
|
888
|
+
additionalProperties: false
|
|
889
|
+
},
|
|
890
|
+
handler: (args) => callSidecar(sidecar, "asset.list", { projectId: args.projectId, kind: args.kind })
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: "asset.import",
|
|
894
|
+
description: "Import an asset from a URL or local path into the project assets dir.",
|
|
895
|
+
inputSchema: {
|
|
896
|
+
type: "object",
|
|
897
|
+
properties: {
|
|
898
|
+
projectId: { type: "string" },
|
|
899
|
+
source: { type: "string", description: "URL or absolute path of the source asset." },
|
|
900
|
+
kind: { type: "string" }
|
|
901
|
+
},
|
|
902
|
+
required: ["projectId", "source"],
|
|
903
|
+
additionalProperties: false
|
|
904
|
+
},
|
|
905
|
+
handler: (args) => callSidecar(sidecar, "asset.import", {
|
|
906
|
+
projectId: args.projectId,
|
|
907
|
+
source: args.source,
|
|
908
|
+
kind: args.kind
|
|
909
|
+
})
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
name: "asset.resolve",
|
|
913
|
+
description: "Resolve an AssetRef (project-relative asset id) to an absolute on-disk path + metadata.",
|
|
914
|
+
inputSchema: {
|
|
915
|
+
type: "object",
|
|
916
|
+
properties: {
|
|
917
|
+
projectId: { type: "string" },
|
|
918
|
+
assetId: { type: "string" }
|
|
919
|
+
},
|
|
920
|
+
required: ["projectId", "assetId"],
|
|
921
|
+
additionalProperties: false
|
|
922
|
+
},
|
|
923
|
+
handler: (args) => callSidecar(sidecar, "asset.resolve", { projectId: args.projectId, assetId: args.assetId })
|
|
924
|
+
}
|
|
925
|
+
];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/tools/composition.ts
|
|
929
|
+
function compositionTools(sidecar, _registry) {
|
|
930
|
+
return [
|
|
931
|
+
{
|
|
932
|
+
name: "composition.render",
|
|
933
|
+
description: "Enqueue a render for a cell. The sidecar resolves the engine via G23 (auto by content_path extension) and validates against G2 capabilities BEFORE enqueueing. Returns { ok:true, recordId, engine } or a structured failure.",
|
|
934
|
+
inputSchema: {
|
|
935
|
+
type: "object",
|
|
936
|
+
properties: {
|
|
937
|
+
projectId: { type: "string" },
|
|
938
|
+
cellId: { type: "string" },
|
|
939
|
+
engine: { type: "string", description: 'Optional. "auto" or undefined triggers G23 resolution.' },
|
|
940
|
+
variant: { type: "string" },
|
|
941
|
+
range: {
|
|
942
|
+
type: "object",
|
|
943
|
+
properties: { start_ms: { type: "number" }, end_ms: { type: "number" } },
|
|
944
|
+
additionalProperties: false
|
|
945
|
+
},
|
|
946
|
+
aspect_ratio: { type: "string", enum: ["16:9", "9:16", "1:1"] },
|
|
947
|
+
resolution: {
|
|
948
|
+
type: "object",
|
|
949
|
+
properties: { w: { type: "number" }, h: { type: "number" } },
|
|
950
|
+
required: ["w", "h"],
|
|
951
|
+
additionalProperties: false
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
required: ["projectId", "cellId"],
|
|
955
|
+
additionalProperties: false
|
|
956
|
+
},
|
|
957
|
+
handler: (args) => callSidecar(sidecar, "render.enqueue", {
|
|
958
|
+
projectId: args.projectId,
|
|
959
|
+
cellId: args.cellId,
|
|
960
|
+
engine: args.engine,
|
|
961
|
+
variant: args.variant,
|
|
962
|
+
range: args.range,
|
|
963
|
+
aspect_ratio: args.aspect_ratio,
|
|
964
|
+
resolution: args.resolution
|
|
965
|
+
})
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
name: "composition.preview",
|
|
969
|
+
description: "Generate a preview frame URL for a cell. Sidecar resolves the engine (G23) and calls adapter.preview.",
|
|
970
|
+
inputSchema: {
|
|
971
|
+
type: "object",
|
|
972
|
+
properties: {
|
|
973
|
+
projectId: { type: "string" },
|
|
974
|
+
cellId: { type: "string" },
|
|
975
|
+
engine: { type: "string" }
|
|
976
|
+
},
|
|
977
|
+
required: ["projectId", "cellId"],
|
|
978
|
+
additionalProperties: false
|
|
979
|
+
},
|
|
980
|
+
handler: (args) => callSidecar(sidecar, "composition.preview", {
|
|
981
|
+
projectId: args.projectId,
|
|
982
|
+
cellId: args.cellId,
|
|
983
|
+
engine: args.engine
|
|
984
|
+
})
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
name: "composition.validate",
|
|
988
|
+
description: "Run the resolved engine's `validate` against a cell. Returns Diagnostic[] under `diagnostics`.",
|
|
989
|
+
inputSchema: {
|
|
990
|
+
type: "object",
|
|
991
|
+
properties: {
|
|
992
|
+
projectId: { type: "string" },
|
|
993
|
+
cellId: { type: "string" },
|
|
994
|
+
engine: { type: "string" }
|
|
995
|
+
},
|
|
996
|
+
required: ["projectId", "cellId"],
|
|
997
|
+
additionalProperties: false
|
|
998
|
+
},
|
|
999
|
+
handler: (args) => callSidecar(sidecar, "composition.validate", {
|
|
1000
|
+
projectId: args.projectId,
|
|
1001
|
+
cellId: args.cellId,
|
|
1002
|
+
engine: args.engine
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/tools/render.ts
|
|
1009
|
+
function renderTools(sidecar) {
|
|
1010
|
+
return [
|
|
1011
|
+
{
|
|
1012
|
+
name: "render.list_engines",
|
|
1013
|
+
description: "Return the G2 capability matrix for every renderer adapter known to the sidecar registry. No open project required.",
|
|
1014
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1015
|
+
handler: () => callSidecar(sidecar, "render.list_engines", {})
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
name: "render.status",
|
|
1019
|
+
description: "Look up a render record by id from the sidecar queue.",
|
|
1020
|
+
inputSchema: {
|
|
1021
|
+
type: "object",
|
|
1022
|
+
properties: { recordId: { type: "string" } },
|
|
1023
|
+
required: ["recordId"],
|
|
1024
|
+
additionalProperties: false
|
|
1025
|
+
},
|
|
1026
|
+
handler: (args) => callSidecar(sidecar, "render.status", { recordId: args.recordId })
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
name: "render.cancel",
|
|
1030
|
+
description: "Cancel a queued or running render (aborts the in-flight adapter or marks a queued row cancelled).",
|
|
1031
|
+
inputSchema: {
|
|
1032
|
+
type: "object",
|
|
1033
|
+
properties: { recordId: { type: "string" } },
|
|
1034
|
+
required: ["recordId"],
|
|
1035
|
+
additionalProperties: false
|
|
1036
|
+
},
|
|
1037
|
+
handler: (args) => callSidecar(sidecar, "render.cancel", { recordId: args.recordId })
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
name: "render.list",
|
|
1041
|
+
description: "List render records from the sidecar queue. Optional projectId + status filters.",
|
|
1042
|
+
inputSchema: {
|
|
1043
|
+
type: "object",
|
|
1044
|
+
properties: {
|
|
1045
|
+
projectId: { type: "string" },
|
|
1046
|
+
status: { type: "string", enum: ["queued", "running", "done", "failed", "cancelled"] }
|
|
1047
|
+
},
|
|
1048
|
+
additionalProperties: false
|
|
1049
|
+
},
|
|
1050
|
+
handler: (args) => callSidecar(sidecar, "render.list", { projectId: args.projectId, status: args.status })
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
name: "render.read_bytes",
|
|
1054
|
+
description: "Read a finished render's MP4 off disk, base64-encoded, so a UI can preview it as a blob: URL in-pane (file:// is unloadable from a sandboxed srcdoc pane). Returns { ok:true, base64, mime, sizeBytes, path }. Keyed on the render record id.",
|
|
1055
|
+
inputSchema: {
|
|
1056
|
+
type: "object",
|
|
1057
|
+
properties: { recordId: { type: "string" } },
|
|
1058
|
+
required: ["recordId"],
|
|
1059
|
+
additionalProperties: false
|
|
1060
|
+
},
|
|
1061
|
+
handler: (args) => callSidecar(sidecar, "render.read_bytes", { recordId: args.recordId })
|
|
1062
|
+
}
|
|
1063
|
+
];
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/tools/export.ts
|
|
1067
|
+
function exportTools(sidecar) {
|
|
1068
|
+
return [
|
|
1069
|
+
{
|
|
1070
|
+
name: "export.compose",
|
|
1071
|
+
description: "Compose a project's rendered cells into a single deliverable MP4 (concat + transitions + narration/music mix). Selection defaults to all cells in beat order; pass `rung` to restrict to one rung or `cellIds` for an explicit ordered subset. Returns { ok:true, exportId, outputPath, reveal:true }. The export runs serially in the sidecar — poll export.status for completion.",
|
|
1072
|
+
inputSchema: {
|
|
1073
|
+
type: "object",
|
|
1074
|
+
properties: {
|
|
1075
|
+
projectId: { type: "string" },
|
|
1076
|
+
rung: { type: "number", description: "Optional. 0|1|2 — restrict to one rung." },
|
|
1077
|
+
cellIds: {
|
|
1078
|
+
type: "array",
|
|
1079
|
+
items: { type: "string" },
|
|
1080
|
+
description: "Optional. Explicit cell uids, in export order (wins over rung)."
|
|
1081
|
+
},
|
|
1082
|
+
music_preset: {
|
|
1083
|
+
type: "string",
|
|
1084
|
+
enum: ["none", "silent", "ambient", "upbeat"],
|
|
1085
|
+
description: "Music bed. none/silent ship now; ambient/upbeat fall back to silent until bed asset files exist (assets/music/<preset>.mp3)."
|
|
1086
|
+
},
|
|
1087
|
+
outputPath: {
|
|
1088
|
+
type: "string",
|
|
1089
|
+
description: "Optional. Absolute or project-relative output path; defaults to exports/<ISO-8601>.mp4."
|
|
1090
|
+
},
|
|
1091
|
+
engine: {
|
|
1092
|
+
type: "string",
|
|
1093
|
+
description: "Optional. Preferred render-engine subfolder to pull cell MP4s from."
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
required: ["projectId"],
|
|
1097
|
+
additionalProperties: false
|
|
1098
|
+
},
|
|
1099
|
+
handler: (args) => callSidecar(sidecar, "export.compose", {
|
|
1100
|
+
projectId: args.projectId,
|
|
1101
|
+
rung: args.rung,
|
|
1102
|
+
cellIds: args.cellIds,
|
|
1103
|
+
music_preset: args.music_preset,
|
|
1104
|
+
outputPath: args.outputPath,
|
|
1105
|
+
engine: args.engine
|
|
1106
|
+
})
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: "export.status",
|
|
1110
|
+
description: "Fetch the status of a single export by id. Returns { ok:true, record: { exportId, projectId, status, outputPath?, error? } }.",
|
|
1111
|
+
inputSchema: {
|
|
1112
|
+
type: "object",
|
|
1113
|
+
properties: { exportId: { type: "string" } },
|
|
1114
|
+
required: ["exportId"],
|
|
1115
|
+
additionalProperties: false
|
|
1116
|
+
},
|
|
1117
|
+
handler: (args) => callSidecar(sidecar, "export.status", { exportId: args.exportId })
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
name: "export.list",
|
|
1121
|
+
description: "List export records (most recent first). Optionally scope to one project. Returns { ok:true, exports: [...] }.",
|
|
1122
|
+
inputSchema: {
|
|
1123
|
+
type: "object",
|
|
1124
|
+
properties: { projectId: { type: "string" } },
|
|
1125
|
+
additionalProperties: false
|
|
1126
|
+
},
|
|
1127
|
+
handler: (args) => callSidecar(sidecar, "export.list", { projectId: args.projectId })
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
name: "export.read_bytes",
|
|
1131
|
+
description: "Read a finished export's MP4 off disk, base64-encoded, for in-pane composed playback as a blob: URL. Returns { ok:true, base64, mime, sizeBytes, path }. Keyed on the export id.",
|
|
1132
|
+
inputSchema: {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
properties: { exportId: { type: "string" } },
|
|
1135
|
+
required: ["exportId"],
|
|
1136
|
+
additionalProperties: false
|
|
1137
|
+
},
|
|
1138
|
+
handler: (args) => callSidecar(sidecar, "export.read_bytes", { exportId: args.exportId })
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
name: "export.check_bed",
|
|
1142
|
+
description: "Pre-flight audio-bed check: does a real music-bed file exist on disk for the chosen preset? Returns { ok:true, hasBed, willBeSilent, byDesign, path? }. none/silent are silent by design; ambient/upbeat need assets/music/<preset>.mp3.",
|
|
1143
|
+
inputSchema: {
|
|
1144
|
+
type: "object",
|
|
1145
|
+
properties: {
|
|
1146
|
+
projectId: { type: "string" },
|
|
1147
|
+
music_preset: { type: "string", enum: ["none", "silent", "ambient", "upbeat"] }
|
|
1148
|
+
},
|
|
1149
|
+
required: ["projectId"],
|
|
1150
|
+
additionalProperties: false
|
|
1151
|
+
},
|
|
1152
|
+
handler: (args) => callSidecar(sidecar, "export.check_bed", {
|
|
1153
|
+
projectId: args.projectId,
|
|
1154
|
+
music_preset: args.music_preset
|
|
1155
|
+
})
|
|
1156
|
+
}
|
|
1157
|
+
];
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/tools/block.ts
|
|
1161
|
+
function blockTools(catalog, registry) {
|
|
1162
|
+
return [
|
|
1163
|
+
{
|
|
1164
|
+
name: "block.list",
|
|
1165
|
+
description: "List blocks in the catalog. Filters: kind, tags (OR-match). Project-scoped custom blocks merge in when projectId is supplied.",
|
|
1166
|
+
inputSchema: {
|
|
1167
|
+
type: "object",
|
|
1168
|
+
properties: {
|
|
1169
|
+
projectId: { type: "string" },
|
|
1170
|
+
kind: { type: "string" },
|
|
1171
|
+
tags: { type: "array", items: { type: "string" } }
|
|
1172
|
+
},
|
|
1173
|
+
additionalProperties: false
|
|
1174
|
+
},
|
|
1175
|
+
async handler(args) {
|
|
1176
|
+
const projectId = args.projectId;
|
|
1177
|
+
if (projectId) {
|
|
1178
|
+
const open = registry.get(projectId);
|
|
1179
|
+
if (open)
|
|
1180
|
+
catalog.refreshForProject(projectId, open.path);
|
|
1181
|
+
}
|
|
1182
|
+
const blocks = catalog.listBlocks({
|
|
1183
|
+
projectId,
|
|
1184
|
+
kind: args.kind,
|
|
1185
|
+
tags: args.tags
|
|
1186
|
+
});
|
|
1187
|
+
return {
|
|
1188
|
+
ok: true,
|
|
1189
|
+
blocks: blocks.map((b) => ({
|
|
1190
|
+
block_id: b.block_id,
|
|
1191
|
+
kind: b.kind,
|
|
1192
|
+
name: b.name,
|
|
1193
|
+
tags: b.tags ?? [],
|
|
1194
|
+
source: b.source
|
|
1195
|
+
}))
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
name: "block.get",
|
|
1201
|
+
description: "Fetch the full block.json body by id.",
|
|
1202
|
+
inputSchema: {
|
|
1203
|
+
type: "object",
|
|
1204
|
+
properties: {
|
|
1205
|
+
block_id: { type: "string" },
|
|
1206
|
+
projectId: { type: "string" }
|
|
1207
|
+
},
|
|
1208
|
+
required: ["block_id"],
|
|
1209
|
+
additionalProperties: false
|
|
1210
|
+
},
|
|
1211
|
+
async handler(args) {
|
|
1212
|
+
const projectId = args.projectId;
|
|
1213
|
+
if (projectId) {
|
|
1214
|
+
const open = registry.get(projectId);
|
|
1215
|
+
if (open)
|
|
1216
|
+
catalog.refreshForProject(projectId, open.path);
|
|
1217
|
+
}
|
|
1218
|
+
const b = catalog.getBlock(args.block_id, projectId);
|
|
1219
|
+
if (!b)
|
|
1220
|
+
return { ok: false, error: "block-not-found", message: args.block_id };
|
|
1221
|
+
return { ok: true, block: b.body, source: b.source };
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
name: "block.instantiate",
|
|
1226
|
+
description: "Instantiate a block into a concrete cell JSON given bindings. Does NOT write to disk — pipe the result into storyboard.write_cell.",
|
|
1227
|
+
inputSchema: {
|
|
1228
|
+
type: "object",
|
|
1229
|
+
properties: {
|
|
1230
|
+
block_id: { type: "string" },
|
|
1231
|
+
bindings: { type: "object", additionalProperties: true },
|
|
1232
|
+
projectId: { type: "string" }
|
|
1233
|
+
},
|
|
1234
|
+
required: ["block_id", "bindings"],
|
|
1235
|
+
additionalProperties: false
|
|
1236
|
+
},
|
|
1237
|
+
async handler(args) {
|
|
1238
|
+
const projectId = args.projectId;
|
|
1239
|
+
const b = catalog.getBlock(args.block_id, projectId);
|
|
1240
|
+
if (!b)
|
|
1241
|
+
return { ok: false, error: "block-not-found", message: args.block_id };
|
|
1242
|
+
const template = b.body.template ?? {};
|
|
1243
|
+
const merged = { ...template, ...args.bindings ?? {}, block_id: b.block_id };
|
|
1244
|
+
return { ok: true, cell: merged };
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
name: "block.create_custom",
|
|
1249
|
+
description: "Write a custom block under the project blocks/custom/ dir. Requires an open project.",
|
|
1250
|
+
inputSchema: {
|
|
1251
|
+
type: "object",
|
|
1252
|
+
properties: {
|
|
1253
|
+
block: { type: "object", additionalProperties: true },
|
|
1254
|
+
project_id: { type: "string" }
|
|
1255
|
+
},
|
|
1256
|
+
required: ["block", "project_id"],
|
|
1257
|
+
additionalProperties: false
|
|
1258
|
+
},
|
|
1259
|
+
async handler(args) {
|
|
1260
|
+
const projectId = args.project_id;
|
|
1261
|
+
const open = registry.get(projectId);
|
|
1262
|
+
if (!open)
|
|
1263
|
+
return { ok: false, error: "project-not-open", message: `projectId ${projectId} is not open` };
|
|
1264
|
+
try {
|
|
1265
|
+
const entry = catalog.writeCustomBlock(open.path, projectId, args.block);
|
|
1266
|
+
return { ok: true, block_id: entry.block_id, path: entry.path };
|
|
1267
|
+
} catch (e) {
|
|
1268
|
+
return { ok: false, error: "internal-error", message: e.message };
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
name: "block.delete",
|
|
1274
|
+
description: 'Delete a custom block. Built-in blocks return { ok:false, error:"cannot-delete-builtin" }.',
|
|
1275
|
+
inputSchema: {
|
|
1276
|
+
type: "object",
|
|
1277
|
+
properties: {
|
|
1278
|
+
block_id: { type: "string" },
|
|
1279
|
+
project_id: { type: "string" }
|
|
1280
|
+
},
|
|
1281
|
+
required: ["block_id", "project_id"],
|
|
1282
|
+
additionalProperties: false
|
|
1283
|
+
},
|
|
1284
|
+
async handler(args) {
|
|
1285
|
+
const projectId = args.project_id;
|
|
1286
|
+
const open = registry.get(projectId);
|
|
1287
|
+
if (!open)
|
|
1288
|
+
return { ok: false, error: "project-not-open", message: `projectId ${projectId} is not open` };
|
|
1289
|
+
const r = catalog.deleteCustomBlock(open.path, projectId, args.block_id);
|
|
1290
|
+
if (r.ok)
|
|
1291
|
+
return { ok: true };
|
|
1292
|
+
if (r.reason === "cannot-delete-builtin")
|
|
1293
|
+
return { ok: false, error: "cannot-delete-builtin" };
|
|
1294
|
+
if (r.reason === "not-found")
|
|
1295
|
+
return { ok: false, error: "block-not-found" };
|
|
1296
|
+
return { ok: false, error: "internal-error", message: r.reason };
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
];
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/tools/archetype.ts
|
|
1303
|
+
function archetypeTools(catalog, registry, sidecar) {
|
|
1304
|
+
return [
|
|
1305
|
+
{
|
|
1306
|
+
name: "archetype.list",
|
|
1307
|
+
description: "List archetypes in the catalog. Project-scoped custom archetypes merge in when projectId is supplied.",
|
|
1308
|
+
inputSchema: {
|
|
1309
|
+
type: "object",
|
|
1310
|
+
properties: { projectId: { type: "string" } },
|
|
1311
|
+
additionalProperties: false
|
|
1312
|
+
},
|
|
1313
|
+
async handler(args) {
|
|
1314
|
+
const projectId = args.projectId;
|
|
1315
|
+
if (projectId) {
|
|
1316
|
+
const open = registry.get(projectId);
|
|
1317
|
+
if (open)
|
|
1318
|
+
catalog.refreshForProject(projectId, open.path);
|
|
1319
|
+
}
|
|
1320
|
+
const archetypes = catalog.listArchetypes({ projectId });
|
|
1321
|
+
return {
|
|
1322
|
+
ok: true,
|
|
1323
|
+
archetypes: archetypes.map((a) => ({
|
|
1324
|
+
archetype_id: a.archetype_id,
|
|
1325
|
+
name: a.name,
|
|
1326
|
+
source: a.source
|
|
1327
|
+
}))
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
name: "archetype.get",
|
|
1333
|
+
description: "Fetch the full archetype.json body by id.",
|
|
1334
|
+
inputSchema: {
|
|
1335
|
+
type: "object",
|
|
1336
|
+
properties: {
|
|
1337
|
+
archetype_id: { type: "string" },
|
|
1338
|
+
projectId: { type: "string" }
|
|
1339
|
+
},
|
|
1340
|
+
required: ["archetype_id"],
|
|
1341
|
+
additionalProperties: false
|
|
1342
|
+
},
|
|
1343
|
+
async handler(args) {
|
|
1344
|
+
const projectId = args.projectId;
|
|
1345
|
+
if (projectId) {
|
|
1346
|
+
const open = registry.get(projectId);
|
|
1347
|
+
if (open)
|
|
1348
|
+
catalog.refreshForProject(projectId, open.path);
|
|
1349
|
+
}
|
|
1350
|
+
const a = catalog.getArchetype(args.archetype_id, projectId);
|
|
1351
|
+
if (!a)
|
|
1352
|
+
return { ok: false, error: "archetype-not-found", message: args.archetype_id };
|
|
1353
|
+
return { ok: true, archetype: a.body, source: a.source };
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
name: "archetype.instantiate_into_project",
|
|
1358
|
+
description: "Scaffold an archetype into an already-open project. Forwards to the sidecar, which materializes the archetype chain into the project cells. Returns archetype-not-found (WP-09) if no definition exists yet.",
|
|
1359
|
+
inputSchema: {
|
|
1360
|
+
type: "object",
|
|
1361
|
+
properties: {
|
|
1362
|
+
archetype_id: { type: "string" },
|
|
1363
|
+
project_id: { type: "string" }
|
|
1364
|
+
},
|
|
1365
|
+
required: ["archetype_id", "project_id"],
|
|
1366
|
+
additionalProperties: false
|
|
1367
|
+
},
|
|
1368
|
+
handler: (args) => callSidecar(sidecar, "archetype.instantiate_into_project", {
|
|
1369
|
+
projectId: args.project_id,
|
|
1370
|
+
archetypeId: args.archetype_id
|
|
1371
|
+
})
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
name: "archetype.save_custom",
|
|
1375
|
+
description: "Write a custom archetype under the project archetypes/ dir.",
|
|
1376
|
+
inputSchema: {
|
|
1377
|
+
type: "object",
|
|
1378
|
+
properties: {
|
|
1379
|
+
archetype: { type: "object", additionalProperties: true },
|
|
1380
|
+
project_id: { type: "string" }
|
|
1381
|
+
},
|
|
1382
|
+
required: ["archetype", "project_id"],
|
|
1383
|
+
additionalProperties: false
|
|
1384
|
+
},
|
|
1385
|
+
async handler(args) {
|
|
1386
|
+
const projectId = args.project_id;
|
|
1387
|
+
const open = registry.get(projectId);
|
|
1388
|
+
if (!open)
|
|
1389
|
+
return { ok: false, error: "project-not-open", message: `projectId ${projectId} is not open` };
|
|
1390
|
+
try {
|
|
1391
|
+
const entry = catalog.writeCustomArchetype(open.path, projectId, args.archetype);
|
|
1392
|
+
return { ok: true, archetype_id: entry.archetype_id, path: entry.path };
|
|
1393
|
+
} catch (e) {
|
|
1394
|
+
return { ok: false, error: "internal-error", message: e.message };
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
name: "archetype.delete",
|
|
1400
|
+
description: 'Delete a custom archetype. Built-ins return { ok:false, error:"cannot-delete-builtin" }.',
|
|
1401
|
+
inputSchema: {
|
|
1402
|
+
type: "object",
|
|
1403
|
+
properties: {
|
|
1404
|
+
archetype_id: { type: "string" },
|
|
1405
|
+
project_id: { type: "string" }
|
|
1406
|
+
},
|
|
1407
|
+
required: ["archetype_id", "project_id"],
|
|
1408
|
+
additionalProperties: false
|
|
1409
|
+
},
|
|
1410
|
+
async handler(args) {
|
|
1411
|
+
const projectId = args.project_id;
|
|
1412
|
+
const open = registry.get(projectId);
|
|
1413
|
+
if (!open)
|
|
1414
|
+
return { ok: false, error: "project-not-open", message: `projectId ${projectId} is not open` };
|
|
1415
|
+
const r = catalog.deleteCustomArchetype(open.path, projectId, args.archetype_id);
|
|
1416
|
+
if (r.ok)
|
|
1417
|
+
return { ok: true };
|
|
1418
|
+
if (r.reason === "cannot-delete-builtin")
|
|
1419
|
+
return { ok: false, error: "cannot-delete-builtin" };
|
|
1420
|
+
if (r.reason === "not-found")
|
|
1421
|
+
return { ok: false, error: "archetype-not-found" };
|
|
1422
|
+
return { ok: false, error: "internal-error", message: r.reason };
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
];
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/tools/index.ts
|
|
1429
|
+
function buildTools(opts) {
|
|
1430
|
+
const { sidecar, catalog, registry } = opts;
|
|
1431
|
+
return [
|
|
1432
|
+
...projectTools(sidecar, registry),
|
|
1433
|
+
...storyboardTools(sidecar),
|
|
1434
|
+
...anchorTools(sidecar),
|
|
1435
|
+
...assetTools(sidecar),
|
|
1436
|
+
...compositionTools(sidecar, registry),
|
|
1437
|
+
...renderTools(sidecar),
|
|
1438
|
+
...exportTools(sidecar),
|
|
1439
|
+
...blockTools(catalog, registry),
|
|
1440
|
+
...archetypeTools(catalog, registry, sidecar)
|
|
1441
|
+
];
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/index.ts
|
|
1445
|
+
var NAME = "studio-mcp";
|
|
1446
|
+
var VERSION = "0.0.0";
|
|
1447
|
+
|
|
1448
|
+
class InMemoryRegistry {
|
|
1449
|
+
map = new Map;
|
|
1450
|
+
set(projectId, entry) {
|
|
1451
|
+
this.map.set(projectId, entry);
|
|
1452
|
+
}
|
|
1453
|
+
get(projectId) {
|
|
1454
|
+
return this.map.get(projectId);
|
|
1455
|
+
}
|
|
1456
|
+
delete(projectId) {
|
|
1457
|
+
return this.map.delete(projectId);
|
|
1458
|
+
}
|
|
1459
|
+
values() {
|
|
1460
|
+
return this.map.values();
|
|
1461
|
+
}
|
|
1462
|
+
keys() {
|
|
1463
|
+
return this.map.keys();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async function main() {
|
|
1467
|
+
const sidecar = new SidecarClient;
|
|
1468
|
+
const catalog = new Catalog;
|
|
1469
|
+
const registry = new InMemoryRegistry;
|
|
1470
|
+
sidecar.start();
|
|
1471
|
+
const tools = buildTools({ sidecar, catalog, registry });
|
|
1472
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
1473
|
+
const server = new Server({ name: NAME, version: VERSION }, { capabilities: { tools: {}, logging: {} } });
|
|
1474
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1475
|
+
tools: tools.map((t) => ({
|
|
1476
|
+
name: t.name,
|
|
1477
|
+
description: t.description,
|
|
1478
|
+
inputSchema: t.inputSchema
|
|
1479
|
+
}))
|
|
1480
|
+
}));
|
|
1481
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1482
|
+
const { name, arguments: args } = req.params;
|
|
1483
|
+
const tool = byName.get(name);
|
|
1484
|
+
if (!tool) {
|
|
1485
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
1486
|
+
}
|
|
1487
|
+
try {
|
|
1488
|
+
const result = await tool.handler(args ?? {});
|
|
1489
|
+
return {
|
|
1490
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
1491
|
+
isError: result.ok === false
|
|
1492
|
+
};
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
if (err instanceof McpError)
|
|
1495
|
+
throw err;
|
|
1496
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1497
|
+
return {
|
|
1498
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "internal-error", message: msg }) }],
|
|
1499
|
+
isError: true
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
sidecar.onEvent((evt) => {
|
|
1504
|
+
server.sendLoggingMessage({
|
|
1505
|
+
level: "info",
|
|
1506
|
+
logger: "studio-mcp/sidecar-event",
|
|
1507
|
+
data: evt
|
|
1508
|
+
}).catch((err) => {
|
|
1509
|
+
process.stderr.write(`[studio-mcp] failed to forward event: ${err.message}
|
|
1510
|
+
`);
|
|
1511
|
+
});
|
|
1512
|
+
});
|
|
1513
|
+
process.stderr.write(`[studio-mcp] ready name=${NAME} pid=${process.pid}
|
|
1514
|
+
`);
|
|
1515
|
+
const transport = new StdioServerTransport;
|
|
1516
|
+
await server.connect(transport);
|
|
1517
|
+
const shutdown = async (sig) => {
|
|
1518
|
+
process.stderr.write(`[studio-mcp] received ${sig}, shutting down
|
|
1519
|
+
`);
|
|
1520
|
+
try {
|
|
1521
|
+
await sidecar.stop();
|
|
1522
|
+
} catch {}
|
|
1523
|
+
try {
|
|
1524
|
+
await server.close();
|
|
1525
|
+
} catch {}
|
|
1526
|
+
process.exit(0);
|
|
1527
|
+
};
|
|
1528
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
1529
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
1530
|
+
}
|
|
1531
|
+
main().catch((err) => {
|
|
1532
|
+
process.stderr.write(`[studio-mcp] fatal: ${err.stack ?? String(err)}
|
|
1533
|
+
`);
|
|
1534
|
+
process.exit(1);
|
|
1535
|
+
});
|