@leotang92/dsh-claude-motion 0.0.0-stage → 0.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.
Files changed (33) hide show
  1. package/LICENSE +27 -0
  2. package/NOTICE.md +25 -0
  3. package/README.md +21 -2
  4. package/cordis.patch.yml +7 -0
  5. package/docs/DETAIL.md +32 -0
  6. package/docs/DETAIL.zh.md +17 -0
  7. package/icon.svg +8 -0
  8. package/lib/client.js +1800 -0
  9. package/lib/index.js +878 -0
  10. package/locale/en.json +7 -0
  11. package/locale/zh.json +7 -0
  12. package/package.json +69 -4
  13. package/skills/claude-motion/SKILL.md +689 -0
  14. package/skills/claude-motion/artifact-type/reference/by-hand.md +84 -0
  15. package/skills/claude-motion/artifact-type/reference/film-api.md +278 -0
  16. package/skills/claude-motion/artifact-type/reference/finalize.md +213 -0
  17. package/skills/claude-motion/artifact-type/reference/motion.md +260 -0
  18. package/skills/claude-motion/artifact-type/reference/plan.md +239 -0
  19. package/skills/claude-motion/artifact-type/reference/questions.md +240 -0
  20. package/skills/claude-motion/artifact-type/reference/recreate-ui.md +140 -0
  21. package/skills/claude-motion/artifact-type/reference/render-offline.md +126 -0
  22. package/skills/claude-motion/artifact-type/reference/rough-cut.md +251 -0
  23. package/skills/claude-motion/artifact-type/reference/sound.md +106 -0
  24. package/skills/claude-motion/artifact-type/reference/styles.md +230 -0
  25. package/skills/claude-motion/artifact-type/reference/walkthrough.md +83 -0
  26. package/skills/claude-motion/scripts/artifact.mjs +8390 -0
  27. package/skills/claude-motion/scripts/editor/artifact-type/anthropicons-fizzy-lake-DtzHkSJH.woff2 +0 -0
  28. package/skills/claude-motion/scripts/editor/artifact-type/app.css +1 -0
  29. package/skills/claude-motion/scripts/editor/artifact-type/app.js +17230 -0
  30. package/skills/claude-motion/scripts/editor/index.html +145 -0
  31. package/skills/claude-motion/scripts/editor/local-host.js +12 -0
  32. package/skills/claude-motion/scripts/editor/share-ui.js +208 -0
  33. package/skills/claude-motion/scripts/editor/third-party-licenses.txt +970 -0
package/lib/index.js ADDED
@@ -0,0 +1,878 @@
1
+ // @leotang92/dsh-claude-motion 0.1.0: packaged from tools/, host adapted from baoyu-design.
2
+ var define_PLUGIN_default = { name: "claude-motion", provider: "@leotang92/dsh-claude-motion", skills: [{ name: "claude-motion", description: "Use this for a local Motion film: a short animated video (10–30 s unless asked for more, 900 s at most), such as an explainer, product walkthrough or UI demo, launch teaser, logo sting, social clip, animated chart or title sequence, that plays in the bundled Motion player and exports from there to MP4 or GIF. A narrated one carries a voice track: a take a scene, made here with a speech provider of the user's or recorded by them, spoken and captioned by the player, the motion timed to its words. Everything on screen is code, a pure function of time, written scene by scene into the film's own store with this skill's script while the user watches it fill in. Read it when creating, continuing, finalizing or revising one. Not for editing real footage, subtitles or audio (that is a video editor), a still picture, a slide deck or a static HTML page.", scriptAlias: "A", comments: true, requests: true }] };
3
+
4
+ import { spawn as spawn2 } from "node:child_process";
5
+ import { randomUUID } from "node:crypto";
6
+ import { statSync as statSync2 } from "node:fs";
7
+ import { readFile } from "node:fs/promises";
8
+ import { basename as basename2, delimiter, dirname as dirname2, isAbsolute, join as join2, sep } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ import { spawn } from "node:child_process";
12
+ import { createHash } from "node:crypto";
13
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { basename, dirname, join, parse } from "node:path";
16
+
17
+ var base = (skill) => `/api/baoyu-design.${skill}.artifacts`;
18
+ var listPath = base;
19
+ var openPath = (skill) => `${base(skill)}.open`;
20
+ var previewPath = (skill) => `${base(skill)}.preview`;
21
+ var releasePath = (skill) => `${base(skill)}.release`;
22
+ var thumbnailPath = (skill) => `${base(skill)}.thumbnail`;
23
+ var updatePath = (skill) => `${base(skill)}.update`;
24
+ var deletePath = (skill) => `${base(skill)}.delete`;
25
+ var restorePath = (skill) => `${base(skill)}.restore`;
26
+
27
+ var TITLES = {
28
+ "claude-slides": ["project/deck.json", "title"],
29
+ "claude-design": ["project/canvas.json", "title"],
30
+ "claude-design-system": ["project/design-system.json", "title"],
31
+ "claude-dashboard": ["db/dash/meta.json", "title"],
32
+ "claude-docs": ["doc.json", "name"],
33
+ "claude-motion": ["db/film/meta.json", "title"]
34
+ };
35
+ var OPEN_TIMEOUT_MS = 3e4;
36
+ var TITLE_MAX = 200;
37
+ var EDITOR_FILES = [".artifact-server.json", ".artifact-server.log", ".sharing.json", ".gitignore", ".DS_Store", "comments.json", "history", "exports"];
38
+ var FOLDER = {
39
+ "claude-slides": ["project"],
40
+ "claude-design": ["project"],
41
+ "claude-design-system": ["project"],
42
+ "claude-dashboard": ["db", "_blobs", "src"],
43
+ "claude-docs": ["doc.json", "tabs", "_blobs", ".sessions.json"],
44
+ "claude-motion": ["db", "_blobs", "src"]
45
+ };
46
+ var NOT_CONTENT = /* @__PURE__ */ new Set([...EDITOR_FILES, ".sessions.json", ".git", "node_modules"]);
47
+ var STAMP_MAX_ENTRIES = 2e4;
48
+ function dshHome() {
49
+ const set = process.env.DSH_HOME?.trim();
50
+ return set ? set : join(homedir(), ".dsh");
51
+ }
52
+ function artifactsDir() {
53
+ return join(dshHome(), "baoyu-design", "artifacts");
54
+ }
55
+ var folderHash = (root) => createHash("sha256").update(root).digest("hex").slice(0, 24);
56
+ function recordFile(root) {
57
+ return join(artifactsDir(), `${folderHash(root)}.json`);
58
+ }
59
+ function thumbnailFile(root) {
60
+ return join(dshHome(), "baoyu-design", "thumbnails", `${folderHash(root)}.jpg`);
61
+ }
62
+ function trashDir() {
63
+ return join(dshHome(), "baoyu-design", "trash");
64
+ }
65
+ function artifactTitle(skill, root) {
66
+ const at = TITLES[skill];
67
+ if (at === void 0) return null;
68
+ try {
69
+ const value = JSON.parse(readFileSync(join(root, at[0]), "utf8"))[at[1]];
70
+ const title = typeof value === "string" ? value.trim() : "";
71
+ if (title && title !== "project") return title;
72
+ } catch {
73
+ }
74
+ return basename(root);
75
+ }
76
+ function isRecord(value) {
77
+ const r = value;
78
+ return typeof r === "object" && r !== null && r.v === 1 && typeof r.root === "string" && typeof r.skill === "string" && typeof r.plugin === "string" && typeof r.sessionId === "string" && typeof r.title === "string" && typeof r.openedAt === "string";
79
+ }
80
+ function readRecord(file) {
81
+ try {
82
+ const value = JSON.parse(readFileSync(file, "utf8"));
83
+ return isRecord(value) ? value : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+ function writeWhole(file, data) {
89
+ mkdirSync(dirname(file), { recursive: true });
90
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
91
+ writeFileSync(tmp, data);
92
+ renameSync(tmp, file);
93
+ }
94
+ function writeRecord(record) {
95
+ writeWhole(recordFile(record.root), `${JSON.stringify(record, null, 2)}
96
+ `);
97
+ }
98
+ function recordOpened(skill, plugin, sessionId, root, now = /* @__PURE__ */ new Date()) {
99
+ const file = recordFile(root);
100
+ const before = readRecord(file);
101
+ const mine = before?.root === root ? before : null;
102
+ const record = {
103
+ v: 1,
104
+ root,
105
+ skill,
106
+ plugin,
107
+ sessionId,
108
+ title: artifactTitle(skill, root) ?? before?.title ?? basename(root),
109
+ openedAt: now.toISOString(),
110
+ firstOpenedAt: mine ? mine.firstOpenedAt ?? mine.openedAt : now.toISOString(),
111
+ ...mine?.pinned ? { pinned: true } : {},
112
+ ...mine?.thumbStamp ? { thumbStamp: mine.thumbStamp } : {}
113
+ };
114
+ writeRecord(record);
115
+ return record;
116
+ }
117
+ function contentStamp(root) {
118
+ let newest = 0;
119
+ let entries = 0;
120
+ const walk = (dir, top) => {
121
+ let names;
122
+ try {
123
+ names = readdirSync(dir);
124
+ } catch {
125
+ return;
126
+ }
127
+ for (const name2 of names) {
128
+ if (++entries > STAMP_MAX_ENTRIES) return;
129
+ if (name2.endsWith(".tmp") || top && NOT_CONTENT.has(name2)) continue;
130
+ const path = join(dir, name2);
131
+ try {
132
+ const stat = statSync(path);
133
+ if (stat.isDirectory()) walk(path, false);
134
+ else if (stat.mtimeMs > newest) newest = stat.mtimeMs;
135
+ } catch {
136
+ }
137
+ }
138
+ };
139
+ walk(root, true);
140
+ return newest > 0 ? new Date(newest).toISOString() : "";
141
+ }
142
+ function isShared(root) {
143
+ try {
144
+ return JSON.parse(readFileSync(join(root, ".sharing.json"), "utf8")).lan === true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ function isDeletable(skill, root) {
150
+ const own = FOLDER[skill];
151
+ if (own === void 0) return false;
152
+ if (root === homedir() || root === parse(root).root) return false;
153
+ try {
154
+ return readdirSync(root).every((name2) => own.includes(name2) || EDITOR_FILES.includes(name2) || name2.endsWith(".tmp"));
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+ function listArtifacts() {
160
+ let names;
161
+ try {
162
+ names = readdirSync(artifactsDir());
163
+ } catch {
164
+ return [];
165
+ }
166
+ const views = [];
167
+ for (const name2 of names) {
168
+ if (!name2.endsWith(".json")) continue;
169
+ const record = readRecord(join(artifactsDir(), name2));
170
+ if (record === null || !existsSync(record.root)) continue;
171
+ views.push({
172
+ root: record.root,
173
+ skill: record.skill,
174
+ plugin: record.plugin,
175
+ sessionId: record.sessionId,
176
+ title: artifactTitle(record.skill, record.root) ?? record.title,
177
+ openedAt: record.openedAt,
178
+ firstOpenedAt: record.firstOpenedAt ?? record.openedAt,
179
+ updatedAt: contentStamp(record.root) || record.openedAt,
180
+ pinned: record.pinned === true,
181
+ shared: isShared(record.root),
182
+ deletable: isDeletable(record.skill, record.root),
183
+ thumbnail: record.thumbStamp !== void 0 && existsSync(thumbnailFile(record.root)) ? record.thumbStamp : null
184
+ });
185
+ }
186
+ return views.sort((a, b) => a.openedAt < b.openedAt ? 1 : a.openedAt > b.openedAt ? -1 : 0);
187
+ }
188
+ function findArtifact(root) {
189
+ const record = readRecord(recordFile(root));
190
+ return record?.root === root ? record : null;
191
+ }
192
+ function scriptEnv(node) {
193
+ const env = { ...process.env };
194
+ if (node.env) {
195
+ const [name2, value] = node.env.split("=");
196
+ if (name2) env[name2] = value ?? "";
197
+ }
198
+ return env;
199
+ }
200
+ function runScript(node, script, args, input) {
201
+ return new Promise((resolve, reject) => {
202
+ const child = spawn(node.path, [script, ...args], { env: scriptEnv(node), stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "pipe"], windowsHide: true });
203
+ let stdout = "";
204
+ let stderr = "";
205
+ const timer = setTimeout(() => {
206
+ child.kill();
207
+ reject(new Error(`${args[0]} did not finish in time`));
208
+ }, OPEN_TIMEOUT_MS);
209
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
210
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
211
+ child.stdin?.end(input);
212
+ child.on("error", (error) => {
213
+ clearTimeout(timer);
214
+ reject(error);
215
+ });
216
+ child.on("close", (code) => {
217
+ clearTimeout(timer);
218
+ if (code === 0) return resolve(firstJson(stdout));
219
+ const failure = firstJson(stderr) ?? firstJson(stdout);
220
+ const message = typeof failure?.message === "string" ? failure.message : stderr.trim() || `${args[0]} exited with ${code ?? "a signal"}`;
221
+ reject(new Error(message));
222
+ });
223
+ });
224
+ }
225
+ async function runOpen(node, script, root) {
226
+ const out = await runScript(node, script, ["open", root]);
227
+ if (typeof out?.url !== "string") throw new Error("open printed no address");
228
+ return { url: out.url, reused: out.reused === true };
229
+ }
230
+ async function renameArtifact(node, script, record, title) {
231
+ const { root, skill } = record;
232
+ const at = TITLES[skill];
233
+ if (at === void 0) throw new Error(`no title to change for ${skill}`);
234
+ if (skill === "claude-dashboard" || skill === "claude-motion") {
235
+ const path = at[0].replace(/^db\//, "").replace(/\.json$/, "");
236
+ await runScript(node, script, ["write-db", root, "-"], JSON.stringify([{ op: "update", path, data: { [at[1]]: title } }]));
237
+ return;
238
+ }
239
+ const file = join(root, at[0]);
240
+ const index = JSON.parse(readFileSync(file, "utf8"));
241
+ if (skill === "claude-docs") {
242
+ await runScript(node, script, ["update", root, JSON.stringify({ ref: { object: "project", id: String(index.id) }, payload: { name: title } })]);
243
+ return;
244
+ }
245
+ writeWhole(file, `${JSON.stringify({ ...index, [at[1]]: title }, null, 2)}
246
+ `);
247
+ }
248
+ function cleanTitle(raw) {
249
+ if (typeof raw !== "string") return null;
250
+ const title = raw.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX);
251
+ return title || null;
252
+ }
253
+ function moveFolder(from, to) {
254
+ mkdirSync(dirname(to), { recursive: true });
255
+ try {
256
+ renameSync(from, to);
257
+ } catch (error) {
258
+ if (error.code !== "EXDEV") throw error;
259
+ cpSync(from, to, { recursive: true, preserveTimestamps: true });
260
+ rmSync(from, { recursive: true, force: true });
261
+ }
262
+ }
263
+ var TRASH_ID = /^[0-9a-f]{24}-\d+$/;
264
+ function trashArtifact(record) {
265
+ const id = `${folderHash(record.root)}-${Date.now()}`;
266
+ const entry = join(trashDir(), id);
267
+ try {
268
+ for (const name2 of readdirSync(trashDir())) {
269
+ if (TRASH_ID.test(name2) && !existsSync(join(trashDir(), name2, "folder"))) rmSync(join(trashDir(), name2), { recursive: true, force: true });
270
+ }
271
+ } catch {
272
+ }
273
+ mkdirSync(entry, { recursive: true });
274
+ writeFileSync(join(entry, "record.json"), `${JSON.stringify(record, null, 2)}
275
+ `);
276
+ const thumbnail = thumbnailFile(record.root);
277
+ if (existsSync(thumbnail)) renameSync(thumbnail, join(entry, "thumbnail.jpg"));
278
+ const moved = isDeletable(record.skill, record.root);
279
+ const path = join(entry, "folder", basename(record.root));
280
+ if (moved) moveFolder(record.root, path);
281
+ rmSync(recordFile(record.root), { force: true });
282
+ return { trash: id, moved, path: moved ? path : record.root };
283
+ }
284
+ function restoreArtifact(id) {
285
+ if (!TRASH_ID.test(id)) throw new Error("not a trash entry");
286
+ const entry = join(trashDir(), id);
287
+ const record = readRecord(join(entry, "record.json"));
288
+ if (record === null) throw new Error("nothing to put back");
289
+ const folder = join(entry, "folder", basename(record.root));
290
+ if (existsSync(folder)) {
291
+ if (existsSync(record.root)) throw new Error(`${record.root} is taken`);
292
+ moveFolder(folder, record.root);
293
+ }
294
+ writeRecord(record);
295
+ const thumbnail = join(entry, "thumbnail.jpg");
296
+ if (existsSync(thumbnail)) {
297
+ mkdirSync(dirname(thumbnailFile(record.root)), { recursive: true });
298
+ renameSync(thumbnail, thumbnailFile(record.root));
299
+ }
300
+ rmSync(entry, { recursive: true, force: true });
301
+ return record;
302
+ }
303
+ function firstJson(text) {
304
+ const start = text.indexOf("{");
305
+ const end = text.lastIndexOf("}");
306
+ if (start < 0 || end <= start) return null;
307
+ try {
308
+ const value = JSON.parse(text.slice(start, end + 1));
309
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+ var json = (body, status = 200) => Response.json(body, { status, headers: { "cache-control": "no-store" } });
315
+ function registerRoutes(routes, skill, opened) {
316
+ routes.register({
317
+ path: listPath(skill.name),
318
+ methods: ["GET"],
319
+ requestBody: "buffered",
320
+ fetch: () => Promise.resolve(json({ artifacts: listArtifacts() }))
321
+ });
322
+ routes.register({
323
+ path: openPath(skill.name),
324
+ methods: ["POST"],
325
+ requestBody: "buffered",
326
+ fetch: async (request) => {
327
+ const root = new URL(request.url).searchParams.get("root") ?? "";
328
+ const record = root ? findArtifact(root) : null;
329
+ if (record === null || record.skill !== skill.name) return json({ error: "not a recorded artifact of this skill" }, 404);
330
+ if (!existsSync(root)) return json({ error: "the folder is gone" }, 404);
331
+ try {
332
+ const { url, reused } = await runOpen(skill.node, skill.script, root);
333
+ const again = recordOpened(skill.name, skill.plugin, record.sessionId, root);
334
+ opened(again);
335
+ return json({ url, reused, sessionId: again.sessionId, title: again.title });
336
+ } catch (error) {
337
+ return json({ error: error.message }, 500);
338
+ }
339
+ }
340
+ });
341
+ const rootOf = (request) => new URL(request.url).searchParams.get("root") ?? "";
342
+ const mine = (root) => {
343
+ const record = root ? findArtifact(root) : null;
344
+ return record?.skill === skill.name && existsSync(root) ? record : null;
345
+ };
346
+ const notMine = () => json({ error: "not a recorded artifact of this skill" }, 404);
347
+ const started = /* @__PURE__ */ new Map();
348
+ routes.register({
349
+ path: previewPath(skill.name),
350
+ methods: ["POST"],
351
+ requestBody: "buffered",
352
+ fetch: async (request) => {
353
+ const root = rootOf(request);
354
+ const record = mine(root);
355
+ if (record === null) return notMine();
356
+ try {
357
+ const { url, reused } = await runOpen(skill.node, skill.script, root);
358
+ if (!reused) started.set(root, record.openedAt);
359
+ return json({ url, reused });
360
+ } catch (error) {
361
+ return json({ error: error.message }, 500);
362
+ }
363
+ }
364
+ });
365
+ routes.register({
366
+ path: releasePath(skill.name),
367
+ methods: ["POST"],
368
+ requestBody: "buffered",
369
+ fetch: async (request) => {
370
+ const root = rootOf(request);
371
+ const since = started.get(root);
372
+ started.delete(root);
373
+ const record = mine(root);
374
+ if (record === null) return notMine();
375
+ if (since === void 0 || since !== record.openedAt || new URL(request.url).searchParams.get("keep") === "1") return json({ stopped: false });
376
+ try {
377
+ await runScript(skill.node, skill.script, ["stop", root]);
378
+ return json({ stopped: true });
379
+ } catch (error) {
380
+ return json({ error: error.message }, 500);
381
+ }
382
+ }
383
+ });
384
+ routes.register({
385
+ path: thumbnailPath(skill.name),
386
+ methods: ["GET", "POST"],
387
+ requestBody: "buffered",
388
+ fetch: async (request) => {
389
+ const root = rootOf(request);
390
+ const record = root ? findArtifact(root) : null;
391
+ if (record === null) return json({ error: "not a recorded artifact" }, 404);
392
+ if (request.method === "GET") {
393
+ try {
394
+ const bytes2 = readFileSync(thumbnailFile(root));
395
+ return new Response(bytes2, { headers: { "content-type": "image/jpeg", "cache-control": "private, max-age=31536000, immutable" } });
396
+ } catch {
397
+ return json({ error: "no thumbnail yet" }, 404);
398
+ }
399
+ }
400
+ const bytes = new Uint8Array(await request.arrayBuffer());
401
+ if (bytes.length < 3 || bytes[0] !== 255 || bytes[1] !== 216) return json({ error: "a thumbnail is a JPEG" }, 400);
402
+ writeWhole(thumbnailFile(root), bytes);
403
+ const stamp = contentStamp(root) || record.openedAt;
404
+ writeRecord({ ...findArtifact(root) ?? record, thumbStamp: stamp });
405
+ return json({ thumbnail: stamp, updatedAt: stamp });
406
+ }
407
+ });
408
+ routes.register({
409
+ path: updatePath(skill.name),
410
+ methods: ["POST"],
411
+ requestBody: "buffered",
412
+ fetch: async (request) => {
413
+ const root = rootOf(request);
414
+ const record = mine(root);
415
+ if (record === null) return notMine();
416
+ const patch = await request.json().catch(() => null);
417
+ if (patch === null || typeof patch !== "object") return json({ error: "not a JSON object" }, 400);
418
+ try {
419
+ if (patch.title !== void 0) {
420
+ const title = cleanTitle(patch.title);
421
+ if (title === null) return json({ error: "a title cannot be empty" }, 400);
422
+ await renameArtifact(skill.node, skill.script, record, title);
423
+ }
424
+ const now = findArtifact(root) ?? record;
425
+ const next = { ...now, title: artifactTitle(skill.name, root) ?? now.title };
426
+ if (patch.pinned === true) next.pinned = true;
427
+ else if (patch.pinned === false) delete next.pinned;
428
+ writeRecord(next);
429
+ return json({ title: next.title, pinned: next.pinned === true });
430
+ } catch (error) {
431
+ return json({ error: error.message }, 500);
432
+ }
433
+ }
434
+ });
435
+ routes.register({
436
+ path: deletePath(skill.name),
437
+ methods: ["POST"],
438
+ requestBody: "buffered",
439
+ fetch: async (request) => {
440
+ const root = rootOf(request);
441
+ const record = mine(root);
442
+ if (record === null) return notMine();
443
+ try {
444
+ if (isDeletable(skill.name, root)) await runScript(skill.node, skill.script, ["stop", root]).catch(() => null);
445
+ started.delete(root);
446
+ return json(trashArtifact(record));
447
+ } catch (error) {
448
+ return json({ error: error.message }, 500);
449
+ }
450
+ }
451
+ });
452
+ routes.register({
453
+ path: restorePath(skill.name),
454
+ methods: ["POST"],
455
+ requestBody: "buffered",
456
+ fetch: (request) => {
457
+ try {
458
+ return Promise.resolve(json({ root: restoreArtifact(new URL(request.url).searchParams.get("trash") ?? "").root }));
459
+ } catch (error) {
460
+ return Promise.resolve(json({ error: error.message }, 400));
461
+ }
462
+ }
463
+ });
464
+ }
465
+
466
+ var PLUGIN = define_PLUGIN_default;
467
+ var name = PLUGIN.name;
468
+ var inject = ["skills"];
469
+ var BUNDLED_SKILL_RANK = 600;
470
+ var packageRoot = fileURLToPath(new URL("../", import.meta.url));
471
+ function isFile(path) {
472
+ try {
473
+ return statSync2(path).isFile();
474
+ } catch {
475
+ return false;
476
+ }
477
+ }
478
+ function resolveNode(config = {}) {
479
+ if (config.node !== void 0) {
480
+ if (!isAbsolute(config.node) || !isFile(config.node)) {
481
+ throw new Error(`${PLUGIN.name}: config.node must be an absolute path to an executable file: ${config.node}`);
482
+ }
483
+ return { path: config.node, from: "config" };
484
+ }
485
+ if (process.versions.electron === void 0) return { path: process.execPath, from: "host" };
486
+ const exe = process.platform === "win32" ? "node.exe" : "node";
487
+ const runtime = ["runtime", "primary-runtime", "dependencies", "node", "bin", exe];
488
+ const roots = [];
489
+ const resourcesPath = process.resourcesPath;
490
+ if (typeof resourcesPath === "string") roots.push(resourcesPath);
491
+ let dir = dirname2(process.execPath);
492
+ for (let i = 0; i < 6; i++) {
493
+ roots.push(join2(dir, "Resources"), join2(dir, "resources"));
494
+ const parent = dirname2(dir);
495
+ if (parent === dir) break;
496
+ dir = parent;
497
+ }
498
+ for (const root of roots) {
499
+ const candidate = join2(root, ...runtime);
500
+ if (isFile(candidate)) return { path: candidate, from: "desktop runtime" };
501
+ }
502
+ for (const entry of (process.env.PATH ?? "").split(delimiter)) {
503
+ if (entry === "" || !isAbsolute(entry)) continue;
504
+ const candidate = join2(entry, exe);
505
+ if (isFile(candidate)) return { path: candidate, from: "PATH" };
506
+ }
507
+ return { path: process.execPath, env: "ELECTRON_RUN_AS_NODE=1", from: "electron" };
508
+ }
509
+ function shellQuote(path) {
510
+ return `"${path.replace(/(["\\$`])/g, "\\$1")}"`;
511
+ }
512
+ function runtimeSection(skill, skillDir, node) {
513
+ const script = join2(skillDir, "scripts", "artifact.mjs");
514
+ const run = (...args) => [node.env, shellQuote(node.path), shellQuote(script), ...args].filter(Boolean).join(" ");
515
+ const written = skill.scriptAlias === void 0 ? "`node <this skill's folder>/scripts/artifact.mjs <command>`" : `\`node $${skill.scriptAlias} <command>\` (\`${skill.scriptAlias}\` being \`<this skill's folder>/scripts/artifact.mjs\`)`;
516
+ const lines = [
517
+ "",
518
+ "",
519
+ "## Running the calls in DeepSeek Harness",
520
+ "",
521
+ "Wherever these pages, or the script's own output (its `stop`, `next` and other hints), say",
522
+ `${written}, run this in the shell instead, with the quotes (the paths can contain spaces):`,
523
+ "",
524
+ "```sh",
525
+ run("<command> …"),
526
+ "```",
527
+ "",
528
+ "`open` prints the editor's `url` (on 127.0.0.1): give it to the user as a Markdown link. In the DeepSeek Harness",
529
+ "app a chat link opens in the right sidebar's Browser, next to this conversation."
530
+ ];
531
+ if (skill.comments) {
532
+ const sent = skill.commentsBy === "mention" ? "A comment, reply or edit in the viewer that says `@agent` is sent to you (the viewer has no Send to Agent box)." : "The editor shows **Send to Agent** on a comment while comments can reach you; a comment sent with it is sent to you.";
533
+ lines.push(
534
+ "",
535
+ "### Comments sent to you from the editor",
536
+ "",
537
+ sent,
538
+ "Here DeepSeek Harness waits for them for you: after you run `open` it runs `comments <root> --wait` itself, and",
539
+ "each comment sent to you arrives in this conversation as a message with the threads (it wakes you when you are",
540
+ "idle; the next one is held until you are done with this one, so the page shows it as waiting its turn). Handle it",
541
+ "as Comments says",
542
+ skill.commentsBy === "mention" ? "(read the block, change the doc with `update`, then `comment-reply`)." : "(change the files, then `comment-reply`).",
543
+ "So do not start `comments --wait` yourself, where Comments says to: run `open` in the foreground (not",
544
+ "`run_in_background`), and the wait starts with it. It stops when the editor stops; after `stop`, the next `open`",
545
+ "starts it again.",
546
+ ...skill.commentsBy === "mention" ? ["", "When you first give the link, tell the user that a comment with `@agent` in it reaches you (type `@` and pick agent)."] : [],
547
+ ...skill.requests ? [
548
+ "",
549
+ "The page's buttons reach you the same way: a request one of them sends arrives as such a message, under",
550
+ "`requests` (its words in `text`, the button in `label`). Those are the user's words, as if typed here: handle",
551
+ "them as this skill says for those words."
552
+ ] : []
553
+ );
554
+ }
555
+ return lines.join("\n");
556
+ }
557
+ var COMMENT_SOURCE = "baoyu-design-comment";
558
+ var SUMMARY_MAX = 120;
559
+ var SESSION_CHECK_MS = 6e4;
560
+ var RETRY_MS = 5e3;
561
+ var MAX_QUICK_FAILURES = 3;
562
+ var IDLE_CHECK_MS = 500;
563
+ var AGENT_GRACE_MS = 3e4;
564
+ var TURN_START_MS = 3e3;
565
+ var TURN_MAX_MS = 30 * 6e4;
566
+ function jsonObject(text) {
567
+ const trimmed = text.trim();
568
+ const starts = [0];
569
+ for (let i = trimmed.indexOf("\n{"); i >= 0; i = trimmed.indexOf("\n{", i + 1)) starts.push(i + 1);
570
+ for (const start of starts) {
571
+ if (trimmed[start] !== "{") continue;
572
+ const end = trimmed.lastIndexOf("}");
573
+ for (let stop = end; stop > start; stop = trimmed.lastIndexOf("}", stop - 1)) {
574
+ try {
575
+ const value = JSON.parse(trimmed.slice(start, stop + 1));
576
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
577
+ } catch {
578
+ }
579
+ }
580
+ }
581
+ return null;
582
+ }
583
+ function jsonEnd(text, start) {
584
+ let depth = 0;
585
+ let inString = false;
586
+ for (let i = start; i < text.length; i++) {
587
+ const c = text[i];
588
+ if (inString) {
589
+ if (c === "\\") i++;
590
+ else if (c === '"') inString = false;
591
+ } else if (c === '"') inString = true;
592
+ else if (c === "{" || c === "[") depth++;
593
+ else if ((c === "}" || c === "]") && --depth === 0) return i;
594
+ }
595
+ return -1;
596
+ }
597
+ function jsonObjects(text) {
598
+ const found = [];
599
+ const starts = text.startsWith("{") ? [0] : [];
600
+ for (let i = text.indexOf("\n{"); i >= 0; i = text.indexOf("\n{", i + 1)) starts.push(i + 1);
601
+ let after = 0;
602
+ for (const start of starts) {
603
+ if (start < after) continue;
604
+ const end = jsonEnd(text, start);
605
+ if (end < 0) continue;
606
+ try {
607
+ const value = JSON.parse(text.slice(start, end + 1));
608
+ if (value && typeof value === "object" && !Array.isArray(value)) {
609
+ found.push(value);
610
+ after = end + 1;
611
+ }
612
+ } catch {
613
+ }
614
+ }
615
+ return found;
616
+ }
617
+ function bashStdout(result) {
618
+ const value = result.value;
619
+ if (typeof value === "string") return value;
620
+ if (value && typeof value === "object") {
621
+ if (value.kind !== "foreground" || value.exitCode !== 0) return null;
622
+ return typeof value.stdout?.text === "string" ? value.stdout.text : null;
623
+ }
624
+ const text = result.content.map((block) => block.type === "text" ? block.text ?? "" : "").join("\n");
625
+ return text || null;
626
+ }
627
+ function openedEditor(script, exec, result) {
628
+ if (exec.name !== "bash" || exec.agent === void 0 || result.isError) return null;
629
+ const args = exec.arguments;
630
+ const command = args?.command;
631
+ if (typeof command !== "string" || args?.run_in_background === true) return null;
632
+ if (!/(?:^|[\s"'])open(?:[\s"']|$)/.test(command)) return null;
633
+ const stdout = bashStdout(result);
634
+ const opened = stdout === null ? void 0 : jsonObjects(stdout).findLast((o) => typeof o.root === "string" && typeof o.url === "string");
635
+ if (typeof opened?.root !== "string" || typeof opened.url !== "string") return null;
636
+ if (!ranScript(script, command, opened.stop)) return null;
637
+ return { session: String(exec.agent.id), root: opened.root, url: opened.url };
638
+ }
639
+ function ranScript(script, command, stop) {
640
+ if (typeof stop !== "string") return command.includes(script);
641
+ if (stop.includes(script)) return true;
642
+ const folder = basename2(dirname2(dirname2(script)));
643
+ return new RegExp(`[/\\\\]${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[/\\\\]scripts[/\\\\]artifact\\.mjs`).test(stop);
644
+ }
645
+ var bound = (text) => text.length <= SUMMARY_MAX ? text : `${text.slice(0, SUMMARY_MAX - 1)}…`;
646
+ function commentMessage(skill, plugin, out, root) {
647
+ const title = typeof out.title === "string" && out.title ? out.title : root.split(/[\\/]/).pop() || root;
648
+ const url = typeof out.url === "string" ? out.url : "";
649
+ const threads = (out.threads ?? []).map((t) => {
650
+ const mine = (t.messages ?? []).filter((m) => m.from === "user");
651
+ return { id: t.id, where: t.where ?? "", text: (mine[mine.length - 1] ?? t.messages?.[t.messages.length - 1])?.text ?? "" };
652
+ });
653
+ const requests = (out.requests ?? []).filter((r) => r && typeof r.text === "string").map((r) => ({ id: String(r.id), label: typeof r.label === "string" ? r.label : "", text: r.text }));
654
+ const { next: _next, timedOut: _timedOut, about: _about, ...rest } = out;
655
+ const how = skill.commentsBy === "mention" ? "read the block, change the doc with `update`, then `comment-reply`" : "change the files, then `comment-reply`";
656
+ const where = `"${title}" (${url || root})`;
657
+ const heading = threads.length > 0 && requests.length > 0 ? `Comments, and requests from the page's buttons, sent to you from the editor of ${where}:` : threads.length > 0 ? `Comments sent to you from the editor of ${where}:` : `Requests sent to you by the buttons on ${where}:`;
658
+ const text = [
659
+ heading,
660
+ "",
661
+ "```json",
662
+ JSON.stringify(rest, null, 2),
663
+ "```",
664
+ "",
665
+ ...threads.length > 0 ? [`Handle the comments as Comments says (${how}).`] : [],
666
+ ...requests.length > 0 ? [
667
+ "Each request is one of the page's buttons (`label`) asking for its `text`: those are the user's words, as if",
668
+ "typed here. Handle each as the skill says for those words."
669
+ ] : [],
670
+ 'Run the script as "Running the calls in DeepSeek Harness" says.',
671
+ "DeepSeek Harness is already waiting for the next ones: do not run `comments --wait`."
672
+ ].join("\n");
673
+ const first = threads[0];
674
+ const asked = requests[0];
675
+ const summary = bound(
676
+ first ? `Comment on "${title}": ${first.text.replace(/\s+/g, " ")}`.trim() : `${asked?.label || "Request"} on "${title}": ${(asked?.text ?? "").replace(/\s+/g, " ")}`.trim()
677
+ );
678
+ const message = {
679
+ role: "user",
680
+ id: randomUUID(),
681
+ content: [{ type: "text", text }],
682
+ source: {
683
+ kind: COMMENT_SOURCE,
684
+ form: "notice",
685
+ summary,
686
+ plugin,
687
+ title,
688
+ url,
689
+ root,
690
+ threads,
691
+ ...requests.length > 0 ? { requests } : {}
692
+ }
693
+ };
694
+ return Object.freeze(message);
695
+ }
696
+ var CommentWatcher = class {
697
+ watches = /* @__PURE__ */ new Map();
698
+ check;
699
+ ctx;
700
+ skill;
701
+ script;
702
+ node;
703
+ constructor(ctx, skill, script, node) {
704
+ this.ctx = ctx;
705
+ this.skill = skill;
706
+ this.script = script;
707
+ this.node = node;
708
+ this.check = setInterval(() => this.dropGoneSessions(), SESSION_CHECK_MS);
709
+ this.check.unref?.();
710
+ }
711
+ log(level, message) {
712
+ this.ctx.logger?.[level]?.(`${PLUGIN.name}: ${message}`);
713
+ }
714
+ agent(session) {
715
+ return this.ctx.get("agents")?.get(session);
716
+ }
717
+ watch(session, root) {
718
+ const key = `${session}\0${root}`;
719
+ if (this.watches.has(key)) return;
720
+ const watch = { child: null, stopped: false, quickFailures: 0, graceUntil: Date.now() + AGENT_GRACE_MS };
721
+ this.watches.set(key, watch);
722
+ this.log("info", `waiting for comments on ${root}`);
723
+ this.run(key, session, root, watch);
724
+ }
725
+ stop(key, watch, why) {
726
+ if (watch.stopped) return;
727
+ watch.stopped = true;
728
+ if (watch.retry) clearTimeout(watch.retry);
729
+ watch.child?.kill();
730
+ watch.child = null;
731
+ if (this.watches.get(key) === watch) this.watches.delete(key);
732
+ this.log("info", `stopped waiting for comments (${why})`);
733
+ }
734
+ run(key, session, root, watch) {
735
+ watch.retry = void 0;
736
+ if (watch.stopped) return;
737
+ if (this.agent(session) === void 0) {
738
+ if (Date.now() >= watch.graceUntil) return this.stop(key, watch, "the session is gone");
739
+ watch.retry = setTimeout(() => this.run(key, session, root, watch), IDLE_CHECK_MS);
740
+ return;
741
+ }
742
+ const started = Date.now();
743
+ const child = spawn2(this.node.path, [this.script, "comments", root, "--wait"], { env: scriptEnv(this.node), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
744
+ watch.child = child;
745
+ let stdout = "";
746
+ let stderr = "";
747
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
748
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
749
+ child.on("error", (error) => this.stop(key, watch, `the wait did not start: ${error.message}`));
750
+ child.on("close", (code) => {
751
+ if (watch.child === child) watch.child = null;
752
+ if (watch.stopped) return;
753
+ if (code === 0) {
754
+ watch.quickFailures = 0;
755
+ const out = jsonObject(stdout);
756
+ const came = (list) => Array.isArray(list) && list.length > 0;
757
+ if (out && (came(out.threads) || came(out.requests))) {
758
+ this.deliver(session, out, root);
759
+ return this.runWhenIdle(key, session, root, watch);
760
+ }
761
+ this.run(key, session, root, watch);
762
+ return;
763
+ }
764
+ const failure = jsonObject(stderr);
765
+ const reason = typeof failure?.code === "string" ? failure.code : `exit ${code ?? "signal"}`;
766
+ if (failure && typeof failure.code === "string") return this.stop(key, watch, reason);
767
+ if (Date.now() - started < RETRY_MS && ++watch.quickFailures > MAX_QUICK_FAILURES) return this.stop(key, watch, reason);
768
+ watch.retry = setTimeout(() => this.run(key, session, root, watch), RETRY_MS);
769
+ });
770
+ }
771
+ /** Start the next wait once the agent is done with the batch it was just handed. */
772
+ runWhenIdle(key, session, root, watch) {
773
+ const handed = Date.now();
774
+ let working = false;
775
+ const look = () => {
776
+ watch.retry = void 0;
777
+ if (watch.stopped) return;
778
+ const agent = this.agent(session);
779
+ if (agent === void 0) return this.stop(key, watch, "the session is gone");
780
+ const idle = agent.status === "idle";
781
+ if (!idle) working = true;
782
+ const waited = Date.now() - handed;
783
+ if (idle && (working || waited > TURN_START_MS) || waited > TURN_MAX_MS) return this.run(key, session, root, watch);
784
+ watch.retry = setTimeout(look, IDLE_CHECK_MS);
785
+ };
786
+ look();
787
+ }
788
+ deliver(session, out, root) {
789
+ const agent = this.agent(session);
790
+ if (agent === void 0) return;
791
+ const message = commentMessage(this.skill, PLUGIN.provider, out, root);
792
+ try {
793
+ if (agent.status === "idle") agent.followup(message);
794
+ else agent.inject(message);
795
+ } catch (error) {
796
+ this.log("warn", `could not hand the comments or requests to the agent: ${error.message}`);
797
+ }
798
+ }
799
+ dropGoneSessions() {
800
+ for (const [key, watch] of [...this.watches]) {
801
+ const session = key.slice(0, key.indexOf("\0"));
802
+ if (this.agent(session) === void 0 && Date.now() >= watch.graceUntil) this.stop(key, watch, "the session is gone");
803
+ }
804
+ }
805
+ dispose() {
806
+ clearInterval(this.check);
807
+ for (const [key, watch] of [...this.watches]) this.stop(key, watch, "the plugin stopped");
808
+ }
809
+ };
810
+ function skillBody(raw, path) {
811
+ const frontmatter = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(raw);
812
+ if (frontmatter === null) throw new Error(`${PLUGIN.name}: ${path} has no YAML frontmatter`);
813
+ return raw.slice(frontmatter[0].length).trim();
814
+ }
815
+ function apply(ctx, config = {}) {
816
+ const node = resolveNode(config ?? {});
817
+ const skills = new Map(PLUGIN.skills.map((skill) => [skill.name, skill]));
818
+ const candidates = PLUGIN.skills.map(({ name: skillName, description }) => {
819
+ const directory = join2(packageRoot, "skills", skillName);
820
+ const path = join2(directory, "SKILL.md");
821
+ if (!isFile(path)) throw new Error(`${PLUGIN.name}: ${path} is missing`);
822
+ return {
823
+ name: skillName,
824
+ description,
825
+ invocation: { modelInvocable: true, userInvocable: true },
826
+ provider: PLUGIN.provider,
827
+ source: "bundled",
828
+ rank: BUNDLED_SKILL_RANK,
829
+ path,
830
+ resourceBase: { kind: "directory", path: directory.split(sep).join("/") }
831
+ };
832
+ });
833
+ const provider = {
834
+ name: PLUGIN.provider,
835
+ list: () => Promise.resolve(candidates),
836
+ async get(candidate, options) {
837
+ const { rank: _rank, ...summary } = candidate;
838
+ const skill = skills.get(candidate.name);
839
+ if (skill === void 0) throw new Error(`${PLUGIN.name}: no skill named ${candidate.name}`);
840
+ const raw = await readFile(candidate.path, { encoding: "utf8", signal: options?.signal });
841
+ const directory = dirname2(candidate.path);
842
+ return { ...summary, content: skillBody(raw, candidate.path) + runtimeSection(skill, directory, node) };
843
+ }
844
+ };
845
+ ctx.skills.registerProvider(() => provider);
846
+ for (const skill of PLUGIN.skills) {
847
+ const script = join2(packageRoot, "skills", skill.name, "scripts", "artifact.mjs");
848
+ const watcher = skill.comments ? new CommentWatcher(ctx, skill, script, node) : null;
849
+ if (watcher) ctx.effect(() => () => watcher.dispose(), `${PLUGIN.name}: comment waits`);
850
+ ctx.on("tools/result", (exec, result) => {
851
+ const opened = openedEditor(script, exec, result);
852
+ if (opened === null) return;
853
+ watcher?.watch(opened.session, opened.root);
854
+ try {
855
+ recordOpened(skill.name, PLUGIN.provider, opened.session, opened.root);
856
+ } catch (error) {
857
+ ctx.logger?.warn?.(`${PLUGIN.name}: could not note ${opened.root} for the Artifacts page: ${error.message}`);
858
+ }
859
+ });
860
+ ctx.inject?.(["connection"], (sub) => {
861
+ registerRoutes(sub.connection.fetch, { name: skill.name, plugin: PLUGIN.provider, script, node }, (record) => {
862
+ watcher?.watch(record.sessionId, record.root);
863
+ });
864
+ });
865
+ }
866
+ }
867
+ export {
868
+ COMMENT_SOURCE,
869
+ apply,
870
+ commentMessage,
871
+ inject,
872
+ jsonObject,
873
+ jsonObjects,
874
+ name,
875
+ openedEditor,
876
+ resolveNode,
877
+ runtimeSection
878
+ };