cozyclay 3.2.0 → 3.2.1

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 (185) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/INSTALL_FOR_AGENTS.md +4 -4
  3. package/README.md +4 -2
  4. package/bin/agent/agent-failure-codes.mjs +34 -8
  5. package/bin/agent/agent-routes.mjs +6 -6
  6. package/bin/agent/agent-runner.mjs +16 -13
  7. package/bin/agent/agent-stream.mjs +26 -3
  8. package/bin/agent/main-prompt.mjs +1 -1
  9. package/bin/agent/studio-prompt.mjs +2 -2
  10. package/bin/agent/supervisor-followup.mjs +34 -0
  11. package/bin/agent/supervisor.mjs +6 -6
  12. package/bin/cozyclay.mjs +23 -60
  13. package/bin/star-prompt.mjs +15 -14
  14. package/bin/studio-routes.mjs +6 -3
  15. package/bin/telemetry-state.mjs +5 -7
  16. package/bin/update-check.mjs +61 -7
  17. package/bin/update-install.mjs +16 -3
  18. package/bin/update-restart.mjs +95 -0
  19. package/bin/update-routes.mjs +25 -3
  20. package/dist/app/index.html +13 -7
  21. package/dist/assets/TaskBoard-tgO0MNKL.js +1 -0
  22. package/dist/assets/analytics-CaTUuLPM.js +1 -0
  23. package/dist/assets/app-CnDkB5WO.css +1 -0
  24. package/dist/assets/app-DJt06Oyb.js +5144 -0
  25. package/dist/assets/{compile-worker-DRWRa9pr.js → compile-worker-D1pl6G7x.js} +5 -5
  26. package/dist/assets/landing-2Fy7DU41.js +1 -0
  27. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  28. package/dist/assets/motion3-compile-BxaM0Xwl.js +1 -0
  29. package/dist/assets/scene-asset-sync-CLGNbEBq.js +13 -0
  30. package/dist/assets/task-board-UVGTZLRF.js +15 -0
  31. package/dist/assets/workflow-CSiBlX1s.js +179 -0
  32. package/dist/cozyclay-package.json +1 -1
  33. package/dist/index.html +4 -1
  34. package/dist/privacy/index.html +9 -6
  35. package/dist/sitemap.xml +2 -2
  36. package/dist/workflow/index.html +5 -4
  37. package/mcp/README.md +1 -1
  38. package/mcp/live-hub.mjs +4 -4
  39. package/mcp/tool-handlers.mjs +14 -16
  40. package/package.json +2 -1
  41. package/src/App.jsx +37 -70
  42. package/src/analytics-controls.js +25 -3
  43. package/src/analytics-strict.js +16 -4
  44. package/src/analytics.js +32 -26
  45. package/src/app-version.js +13 -0
  46. package/src/asset-drop.js +39 -0
  47. package/src/asset-pane.css +34 -0
  48. package/src/asset-pane.jsx +4 -35
  49. package/src/bus-tasks.js +1 -1
  50. package/src/character-kind.js +8 -6
  51. package/src/click-friction.js +5 -4
  52. package/src/command-bus-telemetry.js +8 -7
  53. package/src/command-bus.js +2 -16
  54. package/src/commands/cast.js +2 -1
  55. package/src/commands/motion3.js +3 -1
  56. package/src/commands/objects.js +2 -37
  57. package/src/commands/project.js +3 -3
  58. package/src/commands/sculpt-compare.js +53 -0
  59. package/src/content-tile.jsx +36 -0
  60. package/src/domains/cast.js +7 -6
  61. package/src/domains/motion.js +6 -5
  62. package/src/domains/scenes.js +41 -31
  63. package/src/execution-telemetry.js +7 -3
  64. package/src/export-menu-telemetry.js +26 -0
  65. package/src/finger-handles.jsx +128 -0
  66. package/src/friction-hint.js +82 -0
  67. package/src/live-control.js +2 -1
  68. package/src/locale-zh/panels.js +20 -0
  69. package/src/locale-zh/shell.js +4 -0
  70. package/src/locale-zh/workspace.js +6 -0
  71. package/src/motion/find-bone.js +13 -0
  72. package/src/motion/fingers.js +170 -0
  73. package/src/motion/fingertip-pick.js +21 -0
  74. package/src/motion/ik-exposure.js +48 -0
  75. package/src/motion/ik.js +4 -11
  76. package/src/motion3/compiler/head-lead.js +4 -2
  77. package/src/motion3/hybrid-hint.js +19 -0
  78. package/src/motion3/load.js +19 -12
  79. package/src/motion3/qa/hints-contact.js +12 -0
  80. package/src/motion3/qa/patch.js +4 -1
  81. package/src/motion3/qa/verdict.js +7 -2
  82. package/src/motion3/repair.js +28 -8
  83. package/src/motion3/skills/catch.js +2 -1
  84. package/src/panels/FingerPanel.jsx +38 -0
  85. package/src/panels/RigControlPanel.jsx +9 -1
  86. package/src/panels/SculptPanel.jsx +12 -12
  87. package/src/panels/SculptPartTree.jsx +46 -0
  88. package/src/panels/SubjectsPanel.jsx +1 -1
  89. package/src/panels/sculpt.css +11 -0
  90. package/src/pose-fade.js +7 -3
  91. package/src/posestudio.jsx +14 -27
  92. package/src/project-browser.css +6 -0
  93. package/src/project-browser.jsx +9 -18
  94. package/src/project-start-choices.jsx +45 -0
  95. package/src/project.js +16 -12
  96. package/src/proxy-figure-shape.js +11 -0
  97. package/src/proxy-figure.jsx +25 -18
  98. package/src/scene-assets.js +17 -13
  99. package/src/scenes.js +22 -11
  100. package/src/sculpt-blueprint.js +56 -0
  101. package/src/sculpt-mode.js +5 -0
  102. package/src/sculpt-part-edit.js +115 -0
  103. package/src/sculpt-part-gizmo.jsx +159 -0
  104. package/src/sculpt-part-outline.jsx +72 -0
  105. package/src/sculpt-pick.js +53 -0
  106. package/src/sculpt-run-text.js +51 -0
  107. package/src/sculpt-run.js +10 -46
  108. package/src/sculpt-shelf-file.js +92 -0
  109. package/src/sculpt-shelf-tiles.jsx +69 -0
  110. package/src/sculpt-shelf.js +108 -0
  111. package/src/sculpt-stage.jsx +39 -2
  112. package/src/sculpt-thumb.js +72 -0
  113. package/src/sculpt-views.js +75 -0
  114. package/src/semantic-edit.js +0 -87
  115. package/src/settings-menu.jsx +2 -2
  116. package/src/shell/DetailsSlot.jsx +5 -6
  117. package/src/shell/LibrarySlot.jsx +8 -10
  118. package/src/shell/MenuBar.jsx +6 -6
  119. package/src/shell/PreferencesDialog.jsx +2 -3
  120. package/src/shell/ViewportToolbar.jsx +15 -18
  121. package/src/shell/add-character-entries.js +13 -0
  122. package/src/shell/pose-controls.js +32 -0
  123. package/src/shell/preference-pages.jsx +2 -11
  124. package/src/shell/storyboard-caption.js +21 -0
  125. package/src/shell/studio-shell-context.js +1 -0
  126. package/src/shortcut-friction.js +2 -1
  127. package/src/site/landing-analytics.js +28 -0
  128. package/src/studio-actions.js +3 -2
  129. package/src/studio-agent-protocol.js +4 -4
  130. package/src/studio-app-binding.js +4 -3
  131. package/src/update/UpdateChip.jsx +21 -2
  132. package/src/update/UpdateDialog.jsx +26 -6
  133. package/src/update/update-client.js +123 -17
  134. package/src/update/update-telemetry.js +15 -3
  135. package/src/use-sculpt-mode.js +62 -8
  136. package/src/workflow/agent-failure-codes.js +28 -9
  137. package/src/workflow/agent-turn-telemetry.js +1 -1
  138. package/src/workflow/task-board.js +2 -2
  139. package/tools/dev/page-shell.py +1 -1
  140. package/tools/dev/pages/greybox-to-video.html +21 -2
  141. package/tools/dev/pages/previs-software.html +3 -3
  142. package/tools/dev/pages/privacy.html +9 -6
  143. package/tools/dev/pages/seedance-camera-control.html +2 -2
  144. package/tools/loc-baseline.json +10 -10
  145. package/tools/run-tests.mjs +45 -69
  146. package/tools/select-tests.mjs +119 -0
  147. package/tools/test-durations.json +455 -0
  148. package/tools/test-graph.mjs +181 -0
  149. package/tools/test-shards.mjs +78 -0
  150. package/tools/tutorial/README.md +74 -0
  151. package/tools/tutorial/assemble.mjs +43 -0
  152. package/tools/tutorial/captions.mjs +43 -0
  153. package/tools/tutorial/cdp.mjs +55 -0
  154. package/tools/tutorial/cursor.mjs +159 -0
  155. package/tools/tutorial/episode.mjs +173 -0
  156. package/tools/tutorial/episodes/beginner-guide/00-install.mjs +42 -0
  157. package/tools/tutorial/episodes/beginner-guide/01-start.mjs +50 -0
  158. package/tools/tutorial/episodes/beginner-guide/02-layout.mjs +67 -0
  159. package/tools/tutorial/episodes/beginner-guide/03-stage.mjs +58 -0
  160. package/tools/tutorial/episodes/beginner-guide/04-motion.mjs +98 -0
  161. package/tools/tutorial/episodes/beginner-guide/05-camera.mjs +47 -0
  162. package/tools/tutorial/episodes/beginner-guide/06-export.mjs +59 -0
  163. package/tools/tutorial/episodes/beginner-guide/steps.mjs +32 -0
  164. package/tools/tutorial/episodes/beginner-guide.mjs +17 -0
  165. package/tools/tutorial/episodes/cast.mjs +12 -0
  166. package/tools/tutorial/episodes/first-video.mjs +121 -0
  167. package/tools/tutorial/episodes/stub-tour.mjs +97 -0
  168. package/tools/tutorial/launch.mjs +63 -0
  169. package/tools/tutorial/mux.mjs +61 -0
  170. package/tools/tutorial/overlay.mjs +142 -0
  171. package/tools/tutorial/record.mjs +156 -0
  172. package/tools/tutorial/scenes/terminal.html +63 -0
  173. package/tools/tutorial/steps.mjs +111 -0
  174. package/tools/tutorial/terminal.mjs +43 -0
  175. package/tools/tutorial/voice.mjs +49 -0
  176. package/tools/update-test-durations.mjs +42 -0
  177. package/tools/verify-package-tarball.mjs +18 -12
  178. package/dist/assets/TaskBoard-BUjjuvm5.js +0 -1
  179. package/dist/assets/app-Dige7q3X.css +0 -1
  180. package/dist/assets/app-DxoI941o.js +0 -5144
  181. package/dist/assets/motion3-compile-B4SX4TGx.js +0 -1
  182. package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
  183. package/dist/assets/scene-asset-sync-LeI3eSnO.js +0 -13
  184. package/dist/assets/task-board-5PKn7ZaA.js +0 -15
  185. package/dist/assets/workflow-Dg9jCKOq.js +0 -179
@@ -0,0 +1,63 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Terminal</title>
6
+ <style>
7
+ html, body { margin: 0; height: 100%; background: #2b2f36; }
8
+ body { display: flex; align-items: center; justify-content: center; }
9
+ .window { width: 1240px; height: 700px; border-radius: 12px; overflow: hidden; background: #16181d; box-shadow: 0 30px 80px rgba(0, 0, 0, .5); display: flex; flex-direction: column; }
10
+ .bar { height: 40px; display: flex; align-items: center; gap: 9px; padding: 0 16px; background: #23262d; color: #9aa3b2; font: 500 15px/1 system-ui, sans-serif; }
11
+ .bar i { width: 13px; height: 13px; border-radius: 50%; background: #4b5160; }
12
+ .bar span { flex: 1; text-align: center; margin-right: 60px; }
13
+ pre { flex: 1; margin: 0; padding: 26px 30px; color: #e5e7eb; font: 23px/1.6 ui-monospace, "DejaVu Sans Mono", "Noto Sans Mono", monospace; white-space: pre-wrap; }
14
+ .line { min-height: 1.6em; }
15
+ .prompt { color: #7dd3a7; }
16
+ .out { color: #b6bfcc; }
17
+ .caret { display: inline-block; width: .6em; height: 1.1em; vertical-align: -.18em; background: #e5e7eb; animation: blink 1s steps(1) infinite; }
18
+ @keyframes blink { 50% { opacity: 0; } }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div class="window">
23
+ <div class="bar"><i></i><i></i><i></i><span>Terminal</span></div>
24
+ <pre id="screen"><div class="line"><span class="prompt">~ $ </span><span class="text"></span></div></pre>
25
+ </div>
26
+ <script>
27
+ // Played by tools/tutorial/terminal.mjs: events [{ at, kind: "line" | "char" | "enter", text }].
28
+ // A line prints on a new row (or fills the empty row an Enter left), a char
29
+ // types on the last row, an Enter opens an empty row. The caret follows.
30
+ const screen = document.getElementById("screen");
31
+ const caret = document.createElement("span");
32
+ caret.className = "caret";
33
+ const last = () => screen.lastElementChild;
34
+ const textOf = (line) => line.querySelector(".text");
35
+ const newLine = (className = "") => {
36
+ const line = document.createElement("div");
37
+ line.className = "line";
38
+ const text = document.createElement("span");
39
+ text.className = "text " + className;
40
+ line.append(text);
41
+ screen.append(line);
42
+ return line;
43
+ };
44
+ const placeCaret = () => last().append(caret);
45
+ placeCaret();
46
+ window.__terminal = {
47
+ play(events) {
48
+ for (const event of events) setTimeout(() => {
49
+ if (event.kind === "char") textOf(last()).textContent += event.text;
50
+ else if (event.kind === "enter") newLine();
51
+ else {
52
+ const blank = !last().querySelector(".prompt") && !textOf(last()).textContent;
53
+ const line = blank ? last() : newLine();
54
+ textOf(line).classList.add("out");
55
+ textOf(line).textContent = event.text;
56
+ }
57
+ placeCaret();
58
+ }, event.at);
59
+ },
60
+ };
61
+ </script>
62
+ </body>
63
+ </html>
@@ -0,0 +1,111 @@
1
+ // Runs one episode step against the page: input actions through cursor.mjs,
2
+ // teaching overlays through overlay.mjs, the install scene through
3
+ // terminal.mjs, and the Studio itself.
4
+ import { actionOf, joinTexts, localizeTarget, readingMs } from "./episode.mjs";
5
+ import { playTerminal } from "./terminal.mjs";
6
+
7
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
8
+ const CLOSING_TITLE = { ko: "이번 영상에서 본 것", en: "What we covered" };
9
+ const CHAPTER = { ko: (number) => `챕터 ${number}`, en: (number) => `Chapter ${number}` };
10
+
11
+ /** Runs on every new document: the chosen UI language before the Studio's own scripts read it. */
12
+ export const localeScript = (origin, locale) => `if (location.origin === ${JSON.stringify(origin)}) { try { localStorage.setItem("cozyclay.locale", ${JSON.stringify(locale)}); } catch {} }`;
13
+
14
+ /** Empty origin storage (no project session, no saved projects), then the Studio: the chooser a first-time author sees. */
15
+ export async function openStudio(cdp, url) {
16
+ const origin = new URL(url).origin;
17
+ // A Studio still open on the origin could write its session back after the clear.
18
+ if ((await cdp.evaluate("location.origin")) === origin) {
19
+ const blank = cdp.once("Page.loadEventFired");
20
+ await cdp.send("Page.navigate", { url: "about:blank" });
21
+ await blank;
22
+ }
23
+ await cdp.send("Storage.clearDataForOrigin", { origin, storageTypes: "all" });
24
+ const loaded = cdp.once("Page.loadEventFired");
25
+ await cdp.send("Page.navigate", { url });
26
+ await loaded;
27
+ }
28
+
29
+ /** An action value with every target in it resolved for the UI language. */
30
+ function localizeStep(value, local) {
31
+ if (value === null || typeof value !== "object") return value;
32
+ if (Array.isArray(value)) return value.map(local);
33
+ if (Object.hasOwn(value, "from")) return { from: local(value.from), to: Number.isFinite(value.to?.dx) ? value.to : local(value.to) };
34
+ if (Object.hasOwn(value, "open")) return { open: local(value.open), then: local(value.then) };
35
+ if (Object.hasOwn(value, "into")) return { ...value, into: local(value.into) };
36
+ return local(value);
37
+ }
38
+
39
+ /** The words a card shows, in `locale`. */
40
+ function cardContent(step, { episode, chapter, locale }) {
41
+ const titled = episode.chapters.filter((item) => item.title);
42
+ if (step.card === "opening") return { eyebrow: "CozyClay", title: episode.title[locale], subtitle: episode.subtitle?.[locale] };
43
+ // Titles that carry their own number ("2 · Layout tour") are listed as they are.
44
+ const titles = titled.map((item) => item.title[locale]);
45
+ const items = titles.length ? { titles, numbered: !titles.every((title) => /^\d/.test(title)) } : null;
46
+ if (step.card === "closing") return { title: CLOSING_TITLE[locale], items };
47
+ // A title that already carries its number ("2 · Layout tour") gets no "Chapter N" above it.
48
+ const number = episode.chapters.indexOf(chapter) + episode.firstChapter;
49
+ const title = step.card.title[locale];
50
+ if (step.card.chapters) return { title, subtitle: step.card.subtitle?.[locale], items };
51
+ return { eyebrow: chapter.title && !/^\d/.test(title) ? CHAPTER[locale](number) : null, title, subtitle: step.card.subtitle?.[locale] };
52
+ }
53
+
54
+ /** Reading time of a card in the video's language, for its default hold (2 s at least). */
55
+ function cardReadingMs(step, context, wps) {
56
+ const { eyebrow, title, subtitle, items } = cardContent(step, context);
57
+ const words = [eyebrow, title, subtitle, ...(items?.titles ?? [])].filter(Boolean).join(" ");
58
+ return Math.max(2000, readingMs({ [context.locale]: words }, wps, context.locale));
59
+ }
60
+
61
+ /**
62
+ * Run `step`. `context` carries { cdp, input, wps, locale, url, episode, chapter }:
63
+ * the normalized episode and the chapter the step belongs to.
64
+ */
65
+ export async function runStep(context, step) {
66
+ const { cdp, input, wps, url } = context;
67
+ const action = actionOf(step);
68
+ const local = (target) => localizeTarget(target, context.locale);
69
+ const value = ["click", "wait", "spotlight", "drag", "menu", "type"].includes(action) ? localizeStep(step[action], local) : step[action];
70
+ if (action === "click") for (const target of Array.isArray(value) ? value : [value]) await input.click(target);
71
+ else if (action === "type") await input.type(value);
72
+ else if (action === "key") await input.key(value);
73
+ else if (action === "drag") await input.drag(value.from, value.to);
74
+ else if (action === "hold") await sleep(value);
75
+ else if (action === "eval") {
76
+ const result = await cdp.evaluate(`(async () => (${value}))()`);
77
+ if (result?.ok === false) throw new Error(`step ${step.id}: eval returned ${JSON.stringify(result)}`);
78
+ } else if (action === "wait") {
79
+ if (typeof value === "number") await sleep(value);
80
+ else if (!(await input.locate(value, 20000))) throw new Error(`step ${step.id}: ${JSON.stringify(value)} never showed`);
81
+ } else if (action === "spotlight") {
82
+ if (value === null) {
83
+ await cdp.evaluate("window.__tutorialOverlay.clear()");
84
+ await sleep(350);
85
+ } else {
86
+ // The rect is read now, after any menu or scroll the earlier steps opened.
87
+ if (step.glide !== false) await input.hover(value);
88
+ const shown = await cdp.evaluate(`window.__tutorialOverlay.spotlight(${JSON.stringify(value)}, ${JSON.stringify(step.label?.[context.locale] ?? "")}, ${JSON.stringify(step.note?.[context.locale] ?? "")})`);
89
+ if (!shown) throw new Error(`step ${step.id}: ${JSON.stringify(value)} is not on the page`);
90
+ await sleep(step.hold ?? readingMs(joinTexts(step.label, step.note), wps, context.locale));
91
+ }
92
+ } else if (action === "menu") {
93
+ await input.click(value.open);
94
+ await input.click(value.then);
95
+ } else if (action === "card") {
96
+ await cdp.evaluate("window.__tutorialOverlay.clear()");
97
+ await cdp.evaluate(`window.__tutorialOverlay.card(${JSON.stringify(cardContent(step, context))})`);
98
+ await sleep(step.hold ?? cardReadingMs(step, context, wps));
99
+ await cdp.evaluate("window.__tutorialOverlay.hideCard()");
100
+ await sleep(350);
101
+ } else if (action === "terminal") {
102
+ await playTerminal(cdp, value);
103
+ await sleep(step.hold ?? 1500);
104
+ } else if (action === "studio") {
105
+ await openStudio(cdp, url);
106
+ if (!(await cdp.waitFor("document.querySelector('.v2-start-screen')", 20000))) throw new Error(`step ${step.id}: the startup chooser never showed`);
107
+ }
108
+ if (step.until && !(await cdp.waitFor(`!!window.__tutorialFind(${JSON.stringify(local(step.until))})`, 20000))) {
109
+ throw new Error(`step ${step.id}: ${JSON.stringify(step.until)} never showed`);
110
+ }
111
+ }
@@ -0,0 +1,43 @@
1
+ // The install scene: a local terminal page (scenes/terminal.html) prints
2
+ // output lines and types on the last line, captured like any Studio frame.
3
+ // Consecutive terminal steps continue the same page, so a prompt printed by
4
+ // one step can be answered by the next.
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ export const TERMINAL_URL = pathToFileURL(new URL("./scenes/terminal.html", import.meta.url).pathname).href;
8
+
9
+ /**
10
+ * The timed events the terminal page plays for one step: one `line` per
11
+ * printed line, then one `char` per typed character and an `enter`. Pure,
12
+ * so it is tested. About 12 typed characters a second, lines 0.6 s apart.
13
+ */
14
+ export function terminalSequence({ print = [], type = "" }, { startMs = 400, lineMs = 600, charMs = 83, typeAfterMs = 500, enterMs = 450 } = {}) {
15
+ const events = [];
16
+ let at = startMs;
17
+ print.forEach((line, index) => {
18
+ if (index > 0) at += lineMs;
19
+ events.push({ at, kind: "line", text: line });
20
+ });
21
+ if (type) {
22
+ if (print.length) at += typeAfterMs;
23
+ for (const [index, character] of [...type].entries()) {
24
+ if (index > 0) at += charMs;
25
+ events.push({ at, kind: "char", text: character });
26
+ }
27
+ at += enterMs;
28
+ events.push({ at, kind: "enter" });
29
+ }
30
+ return { events, totalMs: at };
31
+ }
32
+
33
+ /** Play one terminal step: on a fresh page when asked or when the page is elsewhere, else continuing. */
34
+ export async function playTerminal(cdp, scene) {
35
+ if (scene.fresh || (await cdp.evaluate("location.href")) !== TERMINAL_URL) {
36
+ const loaded = cdp.once("Page.loadEventFired");
37
+ await cdp.send("Page.navigate", { url: TERMINAL_URL });
38
+ await loaded;
39
+ }
40
+ const { events, totalMs } = terminalSequence(scene);
41
+ await cdp.evaluate(`window.__terminal.play(${JSON.stringify(events)})`);
42
+ await new Promise((resolve) => setTimeout(resolve, totalMs + 200));
43
+ }
@@ -0,0 +1,49 @@
1
+ // Optional voice stage for the tutorial recorder. It runs before recording,
2
+ // because each cue's hold comes from its clip length. Per narrated step: the
3
+ // author's own recording at tools/tutorial/voice/<episode>/<id>.<lang>.<ext>
4
+ // wins; otherwise macOS `say` reads the step; with neither, the step is silent
5
+ // and the video keeps its burned-in caption.
6
+ import { spawnSync } from "node:child_process";
7
+ import { existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ const SAY_VOICES = { ko: "Yuna", en: "Samantha" };
11
+ const EXTENSIONS = ["wav", "m4a", "mp3", "aiff"];
12
+
13
+ /** The author's clip for one step and language, or null. */
14
+ export function userClip(voiceDir, id, language) {
15
+ return EXTENSIONS.map((extension) => join(voiceDir, `${id}.${language}.${extension}`)).find(existsSync) ?? null;
16
+ }
17
+
18
+ /** What the voice reads: `speech` overrides the caption text (pronunciation of English names in Korean, say). */
19
+ export const speechText = (step, language) => step.speech?.[language] ?? step.narration[language];
20
+
21
+ /** True when `say` can speak this language with its voice (macOS only). */
22
+ function sayAvailable(language) {
23
+ const list = spawnSync("say", ["-v", "?"], { encoding: "utf8" });
24
+ return list.status === 0 && new RegExp(`^${SAY_VOICES[language]}\\s`, "m").test(list.stdout);
25
+ }
26
+
27
+ /**
28
+ * Clips for every narrated step in `language`: Map id -> { path, audioS, source }.
29
+ * Steps without a user clip and without `say` are left out.
30
+ */
31
+ export function prepareVoice({ steps, language, voiceDir, workDir, ffprobe, useSay = true }) {
32
+ const say = useSay && sayAvailable(language);
33
+ const clips = new Map();
34
+ for (const step of steps.filter((item) => item.narration)) {
35
+ let path = userClip(voiceDir, step.id, language);
36
+ const source = path ? "user" : "say";
37
+ if (!path && !say) continue;
38
+ if (!path) {
39
+ path = join(workDir, `${step.id}.${language}.wav`);
40
+ const spoken = spawnSync("say", ["-v", SAY_VOICES[language], "--data-format=LEI16@48000", "-o", path, speechText(step, language)], { encoding: "utf8" });
41
+ if (spoken.status !== 0) throw new Error(`say failed for step ${step.id}: ${spoken.stderr.trim()}`);
42
+ }
43
+ const probe = spawnSync(ffprobe, ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path], { encoding: "utf8" });
44
+ const audioS = Number(probe.stdout.trim());
45
+ if (probe.status !== 0 || !(audioS > 0)) throw new Error(`ffprobe could not read ${path}`);
46
+ clips.set(step.id, { path, audioS, source });
47
+ }
48
+ return clips;
49
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ // Usage: node tools/update-test-durations.mjs [RUN_ID]
3
+ // Refreshes tools/test-durations.json from the test-durations-N artifacts of
4
+ // the latest successful pages.yml run on main (or of RUN_ID). Only main runs
5
+ // are full runs, so only they feed the file. A person runs this and commits
6
+ // the result in a pull request; no workflow writes to main (#632).
7
+ // GH_REPO overrides the repository (default NomaDamas/CozyClay).
8
+ import { execFileSync } from "node:child_process";
9
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { DURATIONS_PATH, readDurations, writeDurations } from "./test-shards.mjs";
13
+
14
+ const repo = process.env.GH_REPO || "NomaDamas/CozyClay";
15
+ const gh = (...arguments_) => execFileSync("gh", [...arguments_, "--repo", repo], { encoding: "utf8" });
16
+ const latest = () => JSON.parse(gh("run", "list", "--workflow", "pages.yml", "--branch", "main", "--status", "success", "--limit", "1", "--json", "databaseId"))[0]?.databaseId;
17
+ const runId = process.argv[2] ?? latest();
18
+ if (!runId) throw new Error(`no successful pages.yml run on main in ${repo}`);
19
+
20
+ const scratch = mkdtempSync(join(tmpdir(), "cozyclay-test-durations-"));
21
+ try {
22
+ gh("run", "download", String(runId), "--pattern", "test-durations-*", "--dir", scratch);
23
+ const merged = readDurations();
24
+ let measured = 0;
25
+ for (const artifact of readdirSync(scratch)) {
26
+ for (const file of readdirSync(join(scratch, artifact)).filter((name) => name.endsWith(".json"))) {
27
+ for (const [test, seconds] of Object.entries(JSON.parse(readFileSync(join(scratch, artifact, file), "utf8")))) {
28
+ if (!Number.isFinite(seconds)) continue;
29
+ merged[test] = seconds;
30
+ measured += 1;
31
+ }
32
+ }
33
+ }
34
+ if (measured === 0) throw new Error(`run ${runId} has no test-durations-N artifacts with entries`);
35
+ // Files deleted since the last refresh drop out; files the run did not
36
+ // execute keep their old record.
37
+ const kept = Object.fromEntries(Object.entries(merged).filter(([test]) => existsSync(new URL(`../${test}`, import.meta.url))));
38
+ writeDurations(DURATIONS_PATH, kept);
39
+ console.log(`test durations: ${measured} files measured in run ${runId}, ${Object.keys(kept).length} recorded in tools/test-durations.json`);
40
+ } finally {
41
+ rmSync(scratch, { recursive: true, force: true });
42
+ }
@@ -6,6 +6,8 @@ import { dirname, join, relative, resolve } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  const packageRoot = fileURLToPath(new URL("..", import.meta.url));
9
+ // CI has no signing key: --imports-only stops after the import check.
10
+ const importsOnly = process.argv.includes("--imports-only");
9
11
  const temporaryRoot = mkdtempSync(join(tmpdir(), "cozyclay-package-check-"));
10
12
  const extractionRoot = join(temporaryRoot, "extracted");
11
13
  // Static, side-effect and dynamic imports with a relative specifier.
@@ -34,6 +36,21 @@ function missingImports(packedRoot) {
34
36
  return missing;
35
37
  }
36
38
 
39
+ async function verifySignature(packedRoot) {
40
+ const metadataPath = join(packedRoot, "package.json");
41
+ const markerPath = join(packedRoot, "dist", "cozyclay-package.json");
42
+ const signatureModulePath = join(packedRoot, "bin", "package-signature.mjs");
43
+ if (!existsSync(metadataPath) || !existsSync(markerPath) || !existsSync(signatureModulePath)) {
44
+ throw new Error("npm tarball is missing package metadata or its signature marker");
45
+ }
46
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
47
+ const { verifyPackageMarker } = await import(pathToFileURL(signatureModulePath).href);
48
+ if (!verifyPackageMarker(markerPath, packedRoot, metadata)) {
49
+ throw new Error("npm tarball signature digest does not match the files npm ships");
50
+ }
51
+ console.log(`package tarball signature PASS (${metadata.name}@${metadata.version})`);
52
+ }
53
+
37
54
  try {
38
55
  mkdirSync(extractionRoot);
39
56
  execFileSync("npm", [
@@ -52,18 +69,7 @@ try {
52
69
  const missing = missingImports(packedRoot);
53
70
  if (missing.length > 0) throw new Error(`npm tarball is missing modules its bin/ and mcp/ code imports:\n ${missing.join("\n ")}`);
54
71
  console.log("package tarball imports PASS (every relative import in bin/ and mcp/ resolves inside the tarball)");
55
- const metadataPath = join(packedRoot, "package.json");
56
- const markerPath = join(packedRoot, "dist", "cozyclay-package.json");
57
- const signatureModulePath = join(packedRoot, "bin", "package-signature.mjs");
58
- if (!existsSync(metadataPath) || !existsSync(markerPath) || !existsSync(signatureModulePath)) {
59
- throw new Error("npm tarball is missing package metadata or its signature marker");
60
- }
61
- const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
62
- const { verifyPackageMarker } = await import(pathToFileURL(signatureModulePath).href);
63
- if (!verifyPackageMarker(markerPath, packedRoot, metadata)) {
64
- throw new Error("npm tarball signature digest does not match the files npm ships");
65
- }
66
- console.log(`package tarball signature PASS (${metadata.name}@${metadata.version})`);
72
+ if (!importsOnly) await verifySignature(packedRoot);
67
73
  } finally {
68
74
  rmSync(temporaryRoot, { recursive: true, force: true });
69
75
  }
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{Za as t,c as n,d as r,f as i,l as a,m as o,o as s,ua as c,v as l}from"./task-board-5PKn7ZaA.js";import{o as u,r as d,t as f}from"./motion3-compile-B4SX4TGx.js";var p=e(t(),1),m=2e3,h=(e,t)=>new Promise(n=>{let r=setTimeout(n,e);t.addEventListener(`abort`,()=>{clearTimeout(r),n()},{once:!0})});function g(e,t){(0,p.useEffect)(()=>{let n=new Map;for(let[r,i]of Object.entries(e))for(let e of i)n.set(e,t[r]??r);let r=()=>{for(let e of document.querySelectorAll(`[data-node-id]`)){let t=n.get(e.getAttribute(`data-node-id`));t?(e.setAttribute(`data-task-scope`,t),e.setAttribute(`title`,c(`In task: ${t}`,`작업 중: ${t}`,`任务中:${t}`))):e.hasAttribute(`data-task-scope`)&&(e.removeAttribute(`data-task-scope`),e.removeAttribute(`title`))}};r();let i=new MutationObserver(r);return i.observe(document.body,{childList:!0,subtree:!0}),()=>{i.disconnect(),n.clear(),r()}},[e,t])}function _({sessionId:e,pulse:t,labelOf:c,onMainNote:_}){let v=(0,p.useMemo)(()=>s(),[]),y=u(),[b,x]=(0,p.useReducer)((e,t)=>t.seed?o(e,t.seed):t.reset?n():r(e,t.event),void 0,n),[S,C]=(0,p.useState)({}),[w,T]=(0,p.useState)(f),E=(0,p.useRef)(0),D=(0,p.useRef)(_);(0,p.useEffect)(()=>{E.current=b.after,D.current=_},[b.after,_]),(0,p.useEffect)(()=>{x({reset:!0}),E.current=0},[e]),(0,p.useEffect)(()=>d(T),[]),(0,p.useEffect)(()=>y.subscribe(e=>{typeof e?.type!=`string`||!e.type.startsWith(`task.`)||(C(t=>i(t,e)),(e.type===`task.userEdit`||e.type===`task.undone`)&&x({event:e}))}),[y]),(0,p.useEffect)(()=>{if(!e)return;let t=new AbortController;return(async()=>{for(;!t.signal.aborted;){try{let{tasks:n}=await v.board(e);x({seed:n}),await v.events(e,E.current,e=>{if(e.type===`main.turn`&&e.turnId){let n=``;v.frames(e.turnId,e=>{e.type===`text.delta`&&(n+=e.text??``),e.type===`tool.start`&&(n=``)},t.signal).then(()=>D.current?.(n),()=>{})}x({event:e})},t.signal)}catch{}await h(m,t.signal)}})(),()=>t.abort()},[v,e,t]);let O=b.order.filter(e=>b.tasks[e]?.status===`running`&&b.tasks[e]?.turnId),k=O.map(e=>`${e}:${b.tasks[e].turnId}`).join();return(0,p.useEffect)(()=>{let e=O.map(e=>{let t=new AbortController,{turnId:n}=b.tasks[e];return v.frames(n,t=>{let n=a(t,c);n&&x({event:{type:`task.progress`,taskId:e,status:`running`,progress:n}})},t.signal).catch(()=>{}),t});return()=>e.forEach(e=>e.abort())},[v,k,c]),g(S,(0,p.useMemo)(()=>Object.fromEntries(b.order.map(e=>[e,b.tasks[e].title])),[b])),(0,p.useEffect)(()=>{l.set({client:v,labelOf:c,undo:async e=>{let t=await y.run(`task.undo`,{taskId:e});if(t?.ok===!1)throw Error(t.message??t.code);x({event:{type:`task.undone`,taskId:e}})}})},[y,v,c]),(0,p.useEffect)(()=>{l.set({board:b,activity:w})},[b,w]),(0,p.useEffect)(()=>()=>l.set({board:n(),open:null}),[e]),null}export{_ as default};