cozyclay 1.5.0 → 1.7.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 (144) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +108 -23
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/cozyclay.mjs +163 -29
  5. package/bin/package-signature.mjs +95 -0
  6. package/bin/telemetry-state.mjs +111 -0
  7. package/dist/ai-camera-control/index.html +4 -5
  8. package/dist/app/index.html +4 -4
  9. package/dist/assets/app-BdCI2Bys.js +4851 -0
  10. package/dist/assets/app-CrB3UF4K.css +1 -0
  11. package/dist/assets/config-Bx7pYsD9.js +1 -0
  12. package/dist/assets/config-DeoFTYzb.css +1 -0
  13. package/dist/assets/demo-DEAy-8Eq.js +1 -0
  14. package/dist/assets/module-MzlRiBjI.js +3 -0
  15. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  16. package/dist/assets/ticket-DbrIKBrJ.js +1 -0
  17. package/dist/cozyclay-package.json +1 -0
  18. package/dist/favicon.ico +0 -0
  19. package/dist/icons/apple-touch-icon.png +0 -0
  20. package/dist/icons/icon-192.png +0 -0
  21. package/dist/icons/icon-32.png +0 -0
  22. package/dist/icons/icon-512.png +0 -0
  23. package/dist/icons/icon-maskable-512.png +0 -0
  24. package/dist/index.html +32 -31
  25. package/dist/robots.txt +2 -0
  26. package/dist/sw.js +9 -2
  27. package/mcp/ardy-prompts.mjs +39 -9
  28. package/mcp/live-hub.mjs +10 -0
  29. package/mcp/package.json +1 -1
  30. package/mcp/runtime/package-lock.json +1 -1
  31. package/mcp/runtime/package.json +1 -1
  32. package/mcp/server.mjs +124 -22
  33. package/package.json +18 -8
  34. package/src/App.jsx +4726 -1969
  35. package/src/analytics-toggle.jsx +24 -0
  36. package/src/analytics.js +388 -0
  37. package/src/app-stage.jsx +2806 -0
  38. package/src/ardy/export.js +9 -1
  39. package/src/ardy/ik.js +238 -4
  40. package/src/ardy/motion-edit.js +41 -0
  41. package/src/ardy/motion-url.js +55 -0
  42. package/src/ardy/timeline.jsx +1019 -285
  43. package/src/asset-pane.jsx +18 -7
  44. package/src/auto-color.js +63 -0
  45. package/src/burn-in.js +91 -0
  46. package/src/camera-block.js +21 -4
  47. package/src/camera-follow.js +16 -6
  48. package/src/controls.jsx +44 -4
  49. package/src/dualview.jsx +89 -10
  50. package/src/error-boundary.jsx +41 -0
  51. package/src/gizmo-claim.js +48 -0
  52. package/src/grid-floor.jsx +97 -0
  53. package/src/grid-view.js +40 -0
  54. package/src/hierarchy-model.js +124 -71
  55. package/src/hierarchy-panel.jsx +101 -6
  56. package/src/line-edit.js +1348 -0
  57. package/src/live-control.js +11 -1
  58. package/src/main.jsx +6 -1
  59. package/src/matte.js +55 -0
  60. package/src/motion-trail.js +250 -0
  61. package/src/object-gizmo.jsx +57 -18
  62. package/src/object-path.js +224 -0
  63. package/src/planview.jsx +165 -14
  64. package/src/pose-thumbs.js +1 -1
  65. package/src/posestudio.jsx +99 -84
  66. package/src/project-browser.jsx +63 -6
  67. package/src/project.js +44 -0
  68. package/src/props.jsx +169 -27
  69. package/src/pwa.js +6 -0
  70. package/src/result-modal.jsx +2 -14
  71. package/src/room.jsx +37 -10
  72. package/src/scene-asset-cache.js +3 -0
  73. package/src/scene-objects.js +156 -3
  74. package/src/scenes.js +22 -1
  75. package/src/shot-authoring.js +9 -0
  76. package/src/shot-guides.js +90 -0
  77. package/src/shot.js +6 -7
  78. package/src/speed-envelope.js +254 -0
  79. package/src/styles.css +4831 -3436
  80. package/src/take-recipe.js +165 -0
  81. package/src/timeline-extent.js +39 -0
  82. package/src/use-render-activity.js +6 -0
  83. package/tools/ardy/BRIDGE.md +20 -316
  84. package/tools/ardy/README.md +22 -201
  85. package/tools/ardy/bridge.mjs +598 -73
  86. package/tools/ardy/bvh-cskel27.mjs +63 -5
  87. package/tools/ardy/prompt-limits.mjs +5 -0
  88. package/tools/ardy/run-on-box.sh +1 -1
  89. package/tools/ardy/runners/index.mjs +11 -16
  90. package/tools/ardy/visual-qa.mjs +38 -30
  91. package/tools/ardy/vq-car.mjs +1 -1
  92. package/tools/demo-worker/README.md +137 -0
  93. package/tools/demo-worker/api-client.mjs +239 -0
  94. package/tools/demo-worker/generate.mjs +303 -0
  95. package/tools/demo-worker/index.mjs +466 -0
  96. package/tools/demo-worker/sign-core.mjs +112 -0
  97. package/tools/demo-worker/sign.mjs +161 -0
  98. package/tools/dev-full.mjs +54 -36
  99. package/tools/kimodo/compare-seam.mjs +108 -0
  100. package/tools/kimodo/constraints.mjs +244 -0
  101. package/tools/kimodo/edit.mjs +264 -0
  102. package/tools/kimodo/effector-constraints.mjs +200 -0
  103. package/tools/kimodo/generate.mjs +597 -0
  104. package/tools/kimodo/measure-pose.mjs +65 -0
  105. package/tools/kimodo/measure-preserve.mjs +899 -0
  106. package/tools/kimodo/measure-waypoints.mjs +55 -0
  107. package/tools/kimodo/pose-constraints.mjs +250 -0
  108. package/tools/kimodo/preserve-mask.mjs +543 -0
  109. package/tools/kimodo/read-npz.mjs +189 -0
  110. package/tools/kimodo/run-edit-on-box.mjs +155 -0
  111. package/tools/kimodo/run-sequence-on-box.mjs +178 -0
  112. package/tools/kimodo/runner.mjs +246 -0
  113. package/tools/kimodo/seam-batch.mjs +77 -0
  114. package/tools/kimodo/setup-local.mjs +30 -0
  115. package/tools/kimodo/setup-on-box.sh +83 -0
  116. package/tools/kimodo/soma77-to-cskel27.mjs +305 -0
  117. package/tools/metrics/archive-traffic.mjs +1 -1
  118. package/tools/metrics-snapshot.mjs +98 -0
  119. package/tools/prepare-package.mjs +50 -0
  120. package/tools/process-supervisor.mjs +12 -2
  121. package/tools/projflow/driver.py +1069 -0
  122. package/tools/projflow/generate.mjs +805 -0
  123. package/tools/projflow/hml22-to-cskel27.mjs +735 -0
  124. package/tools/projflow/line-edit-job.mjs +547 -0
  125. package/tools/projflow/replay.mjs +452 -0
  126. package/tools/projflow/runner.mjs +269 -0
  127. package/tools/projflow/service.mjs +742 -0
  128. package/tools/run-tests.mjs +112 -19
  129. package/dist/ardy/assembled/run-jump-15s.npz +0 -0
  130. package/dist/assets/app-CqOk49Bk.css +0 -1
  131. package/dist/assets/app-zu633VCI.js +0 -4811
  132. package/src/generation/client.js +0 -56
  133. package/src/generation/generation-request.js +0 -17
  134. package/src/generation/session.js +0 -47
  135. package/src/generation/shot-spec.js +0 -40
  136. package/src/generation/use-generation.js +0 -44
  137. package/tools/generation/README.md +0 -29
  138. package/tools/generation/bridge.mjs +0 -162
  139. package/tools/generation/job-store.mjs +0 -51
  140. package/tools/generation/providers/kling.mjs +0 -76
  141. package/tools/generation/providers/runway.mjs +0 -101
  142. package/tools/generation/providers/seedance.mjs +0 -87
  143. package/tools/generation/providers/shared.mjs +0 -49
  144. package/tools/generation/providers/veo.mjs +0 -84
package/bin/cozyclay.mjs CHANGED
@@ -3,12 +3,12 @@
3
3
  * cozyclay - run the studio from a published package.
4
4
  *
5
5
  * `npx cozyclay` / `bunx cozyclay` should behave like `npm run dev` does in a
6
- * clone: the studio in a browser, with the optional ARDY sidecar wired up.
6
+ * clone: the studio in a browser, with the optional motion sidecar wired up.
7
7
  * The difference is that nothing is built here. The package ships the built
8
8
  * `dist/`, so this launcher only has to
9
9
  *
10
10
  * - serve those files over loopback,
11
- * - forward /ardy to its dynamically selected sidecar port (the job Vite's dev proxy does),
11
+ * - forward /ardy to its dynamically selected sidecar port (the same job Vite's dev proxy does),
12
12
  * - keep the sidecar's lifetime tied to this process.
13
13
  *
14
14
  * It has no dependencies on purpose. A launcher that needs an install step
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { spawn } from "node:child_process";
18
18
  import { startBridge, terminateOwned } from "../tools/process-supervisor.mjs";
19
- import { createReadStream, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
19
+ import { createReadStream, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
20
20
  import { createServer, request as httpRequest } from "node:http";
21
21
  import { homedir } from "node:os";
22
22
  import { extname, join, normalize, resolve } from "node:path";
@@ -25,11 +25,37 @@ import { fileURLToPath } from "node:url";
25
25
  import { runMcp } from "./mcp-runtime.mjs";
26
26
  import { openBrowser } from "./open-browser.mjs";
27
27
  import { checkForUpdate, runUpdate } from "./update-check.mjs";
28
+ import { verifyPackageMarker } from "./package-signature.mjs";
29
+ import {
30
+ markTelemetryNoticeShown,
31
+ markTelemetryFirstLaunch,
32
+ effectiveTelemetryEnabled,
33
+ readTelemetryState,
34
+ setTelemetryEnabled,
35
+ takeRuntimeTelemetryConfig,
36
+ TELEMETRY_NOTICE_VERSION,
37
+ } from "./telemetry-state.mjs";
28
38
 
29
39
  const PKG_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)));
40
+ let packageMetadata = {};
41
+ try {
42
+ packageMetadata = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
43
+ } catch {
44
+ // An unreadable package is handled by the existing missing-build checks.
45
+ }
30
46
  const DIST = join(PKG_ROOT, "dist");
31
47
  const BRIDGE = join(PKG_ROOT, "tools", "ardy", "bridge.mjs");
32
- let bridgePort = 5181;
48
+ const OFFICIAL_PACKAGE = (
49
+ packageMetadata.name === "cozyclay"
50
+ && !existsSync(join(PKG_ROOT, ".git"))
51
+ && verifyPackageMarker(join(DIST, "cozyclay-package.json"), PKG_ROOT, packageMetadata)
52
+ );
53
+ // A bridge is optional. Keep the endpoint unset until this launcher owns a
54
+ // sidecar; otherwise /ardy requests could accidentally reach an unrelated
55
+ // process that happens to be listening on the bridge's historical default
56
+ // port (5181).
57
+ let bridge = null;
58
+ let bridgePort = null;
33
59
 
34
60
  const TYPES = {
35
61
  ".html": "text/html; charset=utf-8",
@@ -49,14 +75,14 @@ const TYPES = {
49
75
  };
50
76
 
51
77
  function parseArgs(argv) {
52
- const opts = { port: 5180, host: "127.0.0.1", ardy: true, open: true, star: true, updateCheck: true };
78
+ const opts = { port: 5180, host: "127.0.0.1", motion: true, open: true, star: true, updateCheck: true };
53
79
  for (let i = 0; i < argv.length; i += 1) {
54
80
  const arg = argv[i];
55
81
  if (arg === "--port" || arg === "-p") opts.port = Number(argv[++i]);
56
82
  else if (arg.startsWith("--port=")) opts.port = Number(arg.slice(7));
57
83
  else if (arg === "--host") opts.host = String(argv[++i]);
58
84
  else if (arg.startsWith("--host=")) opts.host = arg.slice(7);
59
- else if (arg === "--no-ardy") opts.ardy = false;
85
+ else if (arg === "--no-motion") opts.motion = false;
60
86
  else if (arg === "--no-open") opts.open = false;
61
87
  else if (arg === "--no-star") opts.star = false;
62
88
  else if (arg === "--no-update-check") opts.updateCheck = false;
@@ -94,16 +120,19 @@ const HELP = `cozyclay - browser-based 3D staging studio
94
120
  npx cozyclay mcp run the MCP server (for Claude, Cursor, any MCP client)
95
121
  cclay update install the latest cozyclay globally (npm install -g)
96
122
  npx cozyclay --port 5200 serve on another port
97
- npx cozyclay --no-ardy skip the optional motion-generation sidecar
123
+ npx cozyclay --no-motion skip the optional motion-generation sidecar
98
124
  npx cozyclay --no-open do not open a browser
99
125
  npx cozyclay --no-star never ask about starring the repo
100
126
  npx cozyclay --no-update-check
101
127
  do not look for a newer release
128
+ cclay telemetry status show anonymous telemetry status
129
+ cclay telemetry off disable anonymous telemetry
130
+ cclay telemetry on enable anonymous telemetry
102
131
 
103
132
  cclay is the same command, shorter: a global install gives you both.
104
133
 
105
- Motion generation needs an SSH-reachable NVIDIA machine running ARDY; point
106
- the sidecar at it with CCLAY_ARDY_HOST. Everything else - staging, posing,
134
+ Motion generation needs an SSH-reachable NVIDIA machine running Kimodo; point
135
+ the sidecar at it with CCLAY_KIMODO_HOST. Everything else - staging, posing,
107
136
  paths, cameras, timeline, playback - runs locally with no extra setup.`;
108
137
 
109
138
  function serveFile(res, path) {
@@ -121,6 +150,12 @@ function serveFile(res, path) {
121
150
  // Forward /ardy to the sidecar. Same contract as the Vite dev proxy, so the
122
151
  // browser code needs no build-time knowledge of how it was launched.
123
152
  function proxyToBridge(req, res) {
153
+ if (!bridge || bridgePort === null || bridge.exitCode !== null || bridge.signalCode !== null) {
154
+ req.resume();
155
+ res.writeHead(503, { "content-type": "application/json; charset=utf-8" });
156
+ res.end(JSON.stringify({ error: "motion sidecar is not running" }));
157
+ return;
158
+ }
124
159
  const upstream = httpRequest(
125
160
  { host: "127.0.0.1", port: bridgePort, path: req.url, method: req.method, headers: req.headers },
126
161
  (upstreamRes) => {
@@ -132,7 +167,7 @@ function proxyToBridge(req, res) {
132
167
  // An absent sidecar is an expected state, not a crash: the app treats a
133
168
  // failed probe as "generation unavailable" and carries on.
134
169
  res.writeHead(503, { "content-type": "application/json" });
135
- res.end(JSON.stringify({ error: "ardy sidecar is not running" }));
170
+ res.end(JSON.stringify({ error: "motion sidecar is not running" }));
136
171
  });
137
172
  req.pipe(upstream);
138
173
  }
@@ -148,7 +183,9 @@ function readState() {
148
183
  function writeState(patch) {
149
184
  try {
150
185
  mkdirSync(STATE_DIR, { recursive: true });
151
- writeFileSync(STATE_FILE, JSON.stringify({ ...readState(), ...patch }, null, "\t"));
186
+ const temporary = `${STATE_FILE}.${process.pid}.tmp`;
187
+ writeFileSync(temporary, JSON.stringify({ ...readState(), ...patch }, null, "\t"), { mode: 0o600 });
188
+ renameSync(temporary, STATE_FILE);
152
189
  } catch {
153
190
  /* a read-only home is not a reason to fail a local dev server */
154
191
  }
@@ -200,11 +237,7 @@ async function maybeAskForStar(startedAt, opts) {
200
237
  // Read on every launch now, so it must not be able to take the launcher down:
201
238
  // the version is cosmetic here, and "0.0.0" just suppresses a bogus notice.
202
239
  function readVersion() {
203
- try {
204
- return JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version ?? "0.0.0";
205
- } catch {
206
- return "0.0.0";
207
- }
240
+ return typeof packageMetadata.version === "string" ? packageMetadata.version : "0.0.0";
208
241
  }
209
242
 
210
243
  const argv = process.argv.slice(2);
@@ -218,18 +251,50 @@ if (argv[0] === "mcp") {
218
251
  process.exit(1);
219
252
  }
220
253
  runUpdate();
254
+ } else if (argv[0] === "telemetry") {
255
+ const action = argv[1] ?? "status";
256
+ if (argv.length > 2 || !["status", "on", "off"].includes(action)) {
257
+ console.error("cozyclay: telemetry accepts status, on, or off");
258
+ process.exit(1);
259
+ }
260
+ if (action === "on") setTelemetryEnabled(STATE_FILE, true);
261
+ if (action === "off") setTelemetryEnabled(STATE_FILE, false);
262
+ const state = readTelemetryState(STATE_FILE);
263
+ const effective = OFFICIAL_PACKAGE && effectiveTelemetryEnabled(state);
264
+ const reason = !OFFICIAL_PACKAGE
265
+ ? " (source checkout)"
266
+ : state.telemetryEnabled && !effective
267
+ ? " (environment override)"
268
+ : "";
269
+ console.log(`Telemetry: ${effective ? "on" : "off"}${reason}`);
270
+ if (action !== "status") console.log("Reload any open CozyClay studio tab to apply this setting.");
221
271
  } else {
222
272
 
223
273
  const opts = parseArgs(argv);
224
- if (opts.help) {
225
- console.log(HELP);
226
- process.exit(opts.invalid ? 1 : 0);
274
+ if (opts.help) {
275
+ console.log(HELP);
276
+ process.exit(opts.invalid ? 1 : 0);
227
277
  }
228
278
  const version = readVersion();
229
279
  if (opts.version) {
230
280
  console.log(version);
231
281
  process.exit(0);
232
282
  }
283
+ let runtimeTelemetry = takeRuntimeTelemetryConfig(STATE_FILE, {
284
+ appVersion: version,
285
+ officialPackage: OFFICIAL_PACKAGE,
286
+ });
287
+ const telemetryState = readTelemetryState(STATE_FILE);
288
+ if (
289
+ runtimeTelemetry.telemetryEnabled
290
+ && telemetryState.noticeVersion < TELEMETRY_NOTICE_VERSION
291
+ ) {
292
+ console.log(
293
+ "Anonymous usage metrics are on (no prompts, project content, filenames, or account information).\n" +
294
+ "Disable anytime: cclay telemetry off",
295
+ );
296
+ markTelemetryNoticeShown(STATE_FILE);
297
+ }
233
298
  // Fired before anything blocking and never awaited on the launch path: the
234
299
  // notice is worth a line of output, never a second of startup.
235
300
  const updatePending = opts.updateCheck && !process.env.CI && !process.env.COZYCLAY_NO_UPDATE_CHECK ? checkForUpdate(version, STATE_DIR) : null;
@@ -241,9 +306,8 @@ if (!existsSync(join(DIST, "app", "index.html"))) {
241
306
 
242
307
  // The sidecar exits immediately without a box to talk to, so starting it
243
308
  // unconditionally would greet a first-time `npx cozyclay` with an error it
244
- // cannot act on. An unset CCLAY_ARDY_HOST is the normal case, not a fault.
245
- const ardyHost = process.env.CCLAY_ARDY_HOST?.trim();
246
- let bridge = null;
309
+ // cannot act on. An unset CCLAY_KIMODO_HOST is the normal case, not a fault.
310
+ const kimodoHost = process.env.CCLAY_KIMODO_HOST?.trim();
247
311
  let server = null;
248
312
  const startedAt = Date.now();
249
313
  let shuttingDown = false;
@@ -262,7 +326,7 @@ async function shutdown(exitCode = 0) {
262
326
  process.on("SIGINT", () => shutdown(130));
263
327
  process.on("SIGTERM", () => shutdown(143));
264
328
 
265
- if (opts.ardy && ardyHost && existsSync(BRIDGE)) {
329
+ if (opts.motion && kimodoHost && existsSync(BRIDGE)) {
266
330
  try {
267
331
  ({ child: bridge, port: bridgePort } = await startBridge({
268
332
  command: process.execPath,
@@ -288,7 +352,7 @@ if (opts.ardy && ardyHost && existsSync(BRIDGE)) {
288
352
  }));
289
353
  } catch (err) {
290
354
  console.error(`cozyclay: motion generation sidecar failed: ${err.message}`);
291
- console.error("cozyclay: studio did not start; set COZYCLAY_BRIDGE_PORT to an available port or use --no-ardy.");
355
+ console.error("cozyclay: studio did not start; set COZYCLAY_BRIDGE_PORT to an available port or use --no-motion.");
292
356
  if (bridge) await terminateOwned(bridge);
293
357
  process.exit(1);
294
358
  }
@@ -296,6 +360,46 @@ if (opts.ardy && ardyHost && existsSync(BRIDGE)) {
296
360
 
297
361
  server = createServer((req, res) => {
298
362
  const url = new URL(req.url ?? "/", "http://localhost");
363
+ if (url.pathname === "/__cozyclay/telemetry") {
364
+ if (req.method === "POST") {
365
+ const origin = req.headers.origin;
366
+ const expectedOrigin = `http://127.0.0.1:${opts.port}`;
367
+ if (origin !== expectedOrigin) {
368
+ res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
369
+ res.end(JSON.stringify({ error: "forbidden origin" }));
370
+ return;
371
+ }
372
+ let body = "";
373
+ req.setEncoding("utf8");
374
+ req.on("data", (chunk) => {
375
+ body += chunk;
376
+ if (body.length > 1024) req.destroy();
377
+ });
378
+ req.on("end", () => {
379
+ try {
380
+ const value = JSON.parse(body);
381
+ if (typeof value.enabled !== "boolean") throw new TypeError("enabled must be boolean");
382
+ setTelemetryEnabled(STATE_FILE, value.enabled);
383
+ runtimeTelemetry = takeRuntimeTelemetryConfig(STATE_FILE, {
384
+ appVersion: version,
385
+ officialPackage: OFFICIAL_PACKAGE,
386
+ });
387
+ res.writeHead(200, {
388
+ "content-type": "application/json; charset=utf-8",
389
+ "cache-control": "no-store",
390
+ });
391
+ res.end(JSON.stringify(runtimeTelemetry));
392
+ } catch {
393
+ res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
394
+ res.end(JSON.stringify({ error: "invalid telemetry setting" }));
395
+ }
396
+ });
397
+ return;
398
+ }
399
+ res.writeHead(405, { allow: "POST", "content-type": "application/json; charset=utf-8" });
400
+ res.end(JSON.stringify({ error: "method not allowed" }));
401
+ return;
402
+ }
299
403
  // Only the routes the bridge actually owns: /ardy/ is ALSO a public asset
300
404
  // directory (cskel27-rest.json), and those files live in dist/, not behind
301
405
  // the sidecar. Same rule as the Vite dev proxy bypass.
@@ -303,7 +407,14 @@ server = createServer((req, res) => {
303
407
  proxyToBridge(req, res);
304
408
  return;
305
409
  }
306
- const rel = decodeURIComponent(url.pathname);
410
+ let rel;
411
+ try {
412
+ rel = decodeURIComponent(url.pathname);
413
+ } catch {
414
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
415
+ res.end("invalid URL encoding");
416
+ return;
417
+ }
307
418
  // normalize + prefix check: a request must not escape dist/.
308
419
  let target = join(DIST, normalize(rel));
309
420
  if (!target.startsWith(DIST)) {
@@ -322,6 +433,28 @@ server = createServer((req, res) => {
322
433
  res.end("not found");
323
434
  return;
324
435
  }
436
+ if (target === join(DIST, "app", "index.html")) {
437
+ runtimeTelemetry = takeRuntimeTelemetryConfig(STATE_FILE, {
438
+ appVersion: version,
439
+ officialPackage: OFFICIAL_PACKAGE,
440
+ });
441
+ const runtime = runtimeTelemetry;
442
+ if (runtime.firstLaunch) {
443
+ markTelemetryFirstLaunch(STATE_FILE);
444
+ }
445
+ // __COZYCLAY_LIVE__ opts the production build into the loopback live
446
+ // socket (src/App.jsx gates on DEV || this flag); without it an
447
+ // npx-served studio can never attach to the MCP server's live hub.
448
+ const script = `<script>window.__COZYCLAY_RUNTIME__ = ${JSON.stringify(runtime).replaceAll("<", "\\u003c")}; window.__COZYCLAY_LIVE__ = true;</script>`;
449
+ const html = readFileSync(target, "utf8").replace("</head>", `${script}\n</head>`);
450
+ res.writeHead(200, {
451
+ "content-type": "text/html; charset=utf-8",
452
+ "content-length": Buffer.byteLength(html),
453
+ "cache-control": "no-store",
454
+ });
455
+ res.end(html);
456
+ return;
457
+ }
325
458
  serveFile(res, target);
326
459
  });
327
460
 
@@ -340,12 +473,13 @@ server.listen({ port: opts.port, host: "127.0.0.1", ipv6Only: false }, () => {
340
473
  // marketing page.
341
474
  const url = `http://127.0.0.1:${opts.port}/app/`;
342
475
  console.log(`CozyClay is running at ${url}`);
343
- if (!opts.ardy) console.log("Motion generation: off (--no-ardy).");
344
- else if (bridge) console.log(`Motion generation: sidecar running against ${ardyHost}.`);
476
+ console.log("Use a Chromium-based browser — Safari and Firefox are not supported.");
477
+ if (!opts.motion) console.log("Motion generation: off (--no-motion).");
478
+ else if (bridge) console.log(`Motion generation: sidecar running against ${kimodoHost}.`);
345
479
  else
346
480
  console.log(
347
- "Motion generation: off. It runs on an SSH-reachable NVIDIA machine with ARDY;\n" +
348
- "set CCLAY_ARDY_HOST=user@host to turn it on. Everything else works without it.",
481
+ "Motion generation: off. It runs on an SSH-reachable NVIDIA machine with Kimodo;\n" +
482
+ "set CCLAY_KIMODO_HOST=user@host to turn it on. Everything else works without it.",
349
483
  );
350
484
  if (opts.open) openBrowser(url);
351
485
  void updatePending?.then((latest) => {
@@ -0,0 +1,95 @@
1
+ import { createHash, createPublicKey, verify } from "node:crypto";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { join, relative, resolve, sep } from "node:path";
4
+
5
+ export const PACKAGE_SIGNATURE_PUBLIC_KEY = "MCowBQYDK2VwAyEAiDLutMZ8CX/NzpuQF/juhT8v9QwPBUvIOHFRqEZoG3U=";
6
+
7
+ const CONTENT_ROOTS = [
8
+ "bin",
9
+ "dist",
10
+ "src",
11
+ "tools",
12
+ "mcp/runtime",
13
+ "mcp/server.mjs",
14
+ "mcp/live-hub.mjs",
15
+ "mcp/ardy-prompts.mjs",
16
+ "mcp/package.json",
17
+ "mcp/README.md",
18
+ "mcp/LIVE-PROTOCOL.md",
19
+ "package.json",
20
+ "README.md",
21
+ "CHANGELOG.md",
22
+ "LICENSE",
23
+ "LICENSES",
24
+ "LICENSING.md",
25
+ "THIRD_PARTY_NOTICES.md",
26
+ ];
27
+
28
+ function ignored(relativePath) {
29
+ return relativePath === "dist/cozyclay-package.json"
30
+ || (/^dist\/media\/[^/]+\.mp4$/i.test(relativePath))
31
+ || relativePath.startsWith("tools/ardy/out/")
32
+ || /^tools\/qa-[^/]+\.mjs$/i.test(relativePath);
33
+ }
34
+
35
+ function collectFiles(path, packageRoot, output) {
36
+ let entries;
37
+ try {
38
+ entries = readdirSync(path, { withFileTypes: true });
39
+ } catch (error) {
40
+ if (error?.code === "ENOTDIR") {
41
+ const relativePath = relative(packageRoot, path).split(sep).join("/");
42
+ if (!ignored(relativePath)) output.push({ path, relativePath });
43
+ return;
44
+ }
45
+ if (error?.code === "ENOENT") return;
46
+ throw error;
47
+ }
48
+ for (const entry of entries) {
49
+ const child = join(path, entry.name);
50
+ const relativePath = relative(packageRoot, child).split(sep).join("/");
51
+ if (ignored(relativePath)) continue;
52
+ if (entry.isDirectory()) collectFiles(child, packageRoot, output);
53
+ if (entry.isFile()) output.push({ path: child, relativePath });
54
+ }
55
+ }
56
+
57
+ export function computePackageDigest(packageRoot) {
58
+ const absoluteRoot = resolve(packageRoot);
59
+ const files = [];
60
+ for (const entry of CONTENT_ROOTS) collectFiles(join(absoluteRoot, entry), absoluteRoot, files);
61
+ files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
62
+ const hash = createHash("sha256");
63
+ for (const file of files) {
64
+ hash.update(file.relativePath);
65
+ hash.update("\0");
66
+ hash.update(readFileSync(file.path));
67
+ hash.update("\0");
68
+ }
69
+ return hash.digest("hex");
70
+ }
71
+
72
+ export function verifyPackageMarker(
73
+ markerFile,
74
+ packageRoot,
75
+ packageMetadata,
76
+ publicKeyBase64 = PACKAGE_SIGNATURE_PUBLIC_KEY,
77
+ ) {
78
+ try {
79
+ const marker = JSON.parse(readFileSync(markerFile, "utf8"));
80
+ const payload = JSON.parse(marker.payload);
81
+ if (payload.distribution !== "npm") return false;
82
+ if (payload.package !== "cozyclay") return false;
83
+ if (payload.version !== packageMetadata.version) return false;
84
+ if (payload.repository !== "NomaDamas/CozyClay") return false;
85
+ if (payload.content_sha256 !== computePackageDigest(packageRoot)) return false;
86
+ const publicKey = createPublicKey({
87
+ key: Buffer.from(publicKeyBase64, "base64"),
88
+ format: "der",
89
+ type: "spki",
90
+ });
91
+ return verify(null, Buffer.from(marker.payload), publicKey, Buffer.from(marker.signature, "base64"));
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
@@ -0,0 +1,111 @@
1
+ import { randomUUID as nodeRandomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+
5
+ export const TELEMETRY_NOTICE_VERSION = 1;
6
+ export const POSTHOG_PROJECT_TOKEN = "phc_CpizzZ8VhSorSS8yEeQhdpUcvB2erp5xkCbnFD8HTJ5m";
7
+ export const POSTHOG_API_HOST = "https://t.cozyclay.org";
8
+
9
+ const DEFAULT_STATE = Object.freeze({
10
+ installationId: null,
11
+ telemetryEnabled: true,
12
+ firstLaunchedAt: null,
13
+ noticeVersion: 0,
14
+ });
15
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
+
17
+ function rawState(stateFile) {
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
20
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
21
+ } catch (error) {
22
+ return error?.code === "ENOENT" ? {} : { telemetryEnabled: false };
23
+ }
24
+ }
25
+
26
+ function normalizedState(value) {
27
+ return {
28
+ installationId: typeof value.installationId === "string" && UUID_PATTERN.test(value.installationId)
29
+ ? value.installationId
30
+ : null,
31
+ telemetryEnabled: value.telemetryEnabled !== false,
32
+ firstLaunchedAt: typeof value.telemetryFirstLaunchedAt === "string"
33
+ ? value.telemetryFirstLaunchedAt
34
+ : null,
35
+ noticeVersion: Number.isInteger(value.telemetryNoticeVersion)
36
+ ? value.telemetryNoticeVersion
37
+ : 0,
38
+ };
39
+ }
40
+
41
+ function writeState(stateFile, patch) {
42
+ const next = { ...rawState(stateFile), ...patch };
43
+ mkdirSync(dirname(stateFile), { recursive: true });
44
+ const temporary = `${stateFile}.${process.pid}.tmp`;
45
+ writeFileSync(temporary, JSON.stringify(next, null, "\t"), { mode: 0o600 });
46
+ renameSync(temporary, stateFile);
47
+ }
48
+
49
+ function envDisablesTelemetry(env) {
50
+ if (env.CI && !/^(0|false|no|off)$/i.test(env.CI)) return true;
51
+ if (/^(1|true|yes|on)$/i.test(env.DO_NOT_TRACK ?? "")) return true;
52
+ return /^(0|false|no|off)$/i.test(env.COZYCLAY_TELEMETRY ?? "");
53
+ }
54
+
55
+ export function readTelemetryState(stateFile) {
56
+ return normalizedState(rawState(stateFile));
57
+ }
58
+
59
+ export function effectiveTelemetryEnabled(state, env = process.env) {
60
+ return state.telemetryEnabled && !envDisablesTelemetry(env);
61
+ }
62
+
63
+ export function setTelemetryEnabled(stateFile, enabled) {
64
+ writeState(stateFile, {
65
+ telemetryEnabled: enabled === true,
66
+ ...(enabled === true ? {} : { installationId: null }),
67
+ });
68
+ return readTelemetryState(stateFile);
69
+ }
70
+
71
+ export function markTelemetryNoticeShown(stateFile) {
72
+ writeState(stateFile, { telemetryNoticeVersion: TELEMETRY_NOTICE_VERSION });
73
+ }
74
+
75
+ export function markTelemetryFirstLaunch(stateFile, now = () => new Date().toISOString()) {
76
+ if (readTelemetryState(stateFile).firstLaunchedAt) return;
77
+ writeState(stateFile, { telemetryFirstLaunchedAt: now() });
78
+ }
79
+
80
+ export function takeRuntimeTelemetryConfig(
81
+ stateFile,
82
+ {
83
+ appVersion,
84
+ officialPackage = true,
85
+ env = process.env,
86
+ now = () => new Date().toISOString(),
87
+ randomUUID = nodeRandomUUID,
88
+ } = {},
89
+ ) {
90
+ const existing = readTelemetryState(stateFile);
91
+ const telemetryEnabled = officialPackage && effectiveTelemetryEnabled(existing, env);
92
+ let installationId = existing.installationId;
93
+ const firstLaunch = telemetryEnabled && !existing.firstLaunchedAt;
94
+ const patch = {};
95
+
96
+ if (telemetryEnabled && !installationId) {
97
+ installationId = randomUUID();
98
+ patch.installationId = installationId;
99
+ }
100
+ if (Object.keys(patch).length > 0) writeState(stateFile, patch);
101
+
102
+ return {
103
+ distribution: "npm",
104
+ telemetryEnabled,
105
+ installationId: telemetryEnabled ? installationId : null,
106
+ appVersion,
107
+ apiKey: POSTHOG_PROJECT_TOKEN,
108
+ apiHost: POSTHOG_API_HOST,
109
+ firstLaunch,
110
+ };
111
+ }
@@ -6,9 +6,8 @@
6
6
  <meta name="description" content="Camera control for AI video: why prompting an angle keeps drifting, what Seedance, Veo and Kling actually accept, and how blocking the shot in 3D first gives you the same camera on every take." />
7
7
  <meta name="theme-color" content="#232323" />
8
8
  <link rel="canonical" href="https://cozyclay.org/ai-camera-control/" />
9
- <link rel="icon" href="/favicon.ico" sizes="any" />
10
- <link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32.png" />
11
9
  <link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />
10
+ <link rel="icon" href="/favicon.ico" sizes="48x48 96x96 192x192" />
12
11
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
13
12
 
14
13
  <meta property="og:type" content="article" />
@@ -207,7 +206,7 @@
207
206
  <a href="/#what">What it does</a>
208
207
  <a href="/#start">Quick start</a>
209
208
  <a href="/ai-camera-control/">AI camera control</a>
210
- <a href="https://github.com/HaD0Yun/CozyClay">GitHub</a>
209
+ <a href="https://github.com/NomaDamas/CozyClay">GitHub</a>
211
210
  </nav>
212
211
  </div>
213
212
  </header>
@@ -343,7 +342,7 @@
343
342
 
344
343
  <div class="cta">
345
344
  <a class="btn primary" href="/">What CozyClay does</a>
346
- <a class="btn ghost" href="https://github.com/HaD0Yun/CozyClay">View on GitHub</a>
345
+ <a class="btn ghost" href="https://github.com/NomaDamas/CozyClay">View on GitHub</a>
347
346
  </div>
348
347
  <p class="note">
349
348
  Free and open source. Runs locally with <code>npx cozyclay</code>.
@@ -393,7 +392,7 @@
393
392
  <div class="wrap">
394
393
  <p>
395
394
  <a href="/">CozyClay</a> ·
396
- <a href="https://github.com/HaD0Yun/CozyClay">GitHub</a> ·
395
+ <a href="https://github.com/NomaDamas/CozyClay">GitHub</a> ·
397
396
  <a href="https://www.npmjs.com/package/cozyclay">npm</a>
398
397
  </p>
399
398
  <p>
@@ -14,9 +14,8 @@
14
14
  <meta name="robots" content="noindex, follow" />
15
15
  <link rel="canonical" href="https://cozyclay.org/app/" />
16
16
  <link rel="manifest" href="/manifest.webmanifest" />
17
- <link rel="icon" href="/favicon.ico" sizes="any" />
18
- <link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32.png" />
19
17
  <link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />
18
+ <link rel="icon" href="/favicon.ico" sizes="48x48 96x96 192x192" />
20
19
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
21
20
 
22
21
  <meta property="og:type" content="website" />
@@ -35,8 +34,9 @@
35
34
  <title>CozyClay Studio — 3D staging and previs in your browser</title>
36
35
 
37
36
  <script defer src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "6a4384d1375d4ffc8a30a6346a8ad1a0"}'></script>
38
- <script type="module" crossorigin src="/assets/app-zu633VCI.js"></script>
39
- <link rel="stylesheet" crossorigin href="/assets/app-CqOk49Bk.css">
37
+ <script type="module" crossorigin src="/assets/app-BdCI2Bys.js"></script>
38
+ <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
39
+ <link rel="stylesheet" crossorigin href="/assets/app-CrB3UF4K.css">
40
40
  </head>
41
41
  <body>
42
42
  <div id="root">