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.
- package/CHANGELOG.md +167 -0
- package/README.md +108 -23
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/cozyclay.mjs +163 -29
- package/bin/package-signature.mjs +95 -0
- package/bin/telemetry-state.mjs +111 -0
- package/dist/ai-camera-control/index.html +4 -5
- package/dist/app/index.html +4 -4
- package/dist/assets/app-BdCI2Bys.js +4851 -0
- package/dist/assets/app-CrB3UF4K.css +1 -0
- package/dist/assets/config-Bx7pYsD9.js +1 -0
- package/dist/assets/config-DeoFTYzb.css +1 -0
- package/dist/assets/demo-DEAy-8Eq.js +1 -0
- package/dist/assets/module-MzlRiBjI.js +3 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/ticket-DbrIKBrJ.js +1 -0
- package/dist/cozyclay-package.json +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/icons/apple-touch-icon.png +0 -0
- package/dist/icons/icon-192.png +0 -0
- package/dist/icons/icon-32.png +0 -0
- package/dist/icons/icon-512.png +0 -0
- package/dist/icons/icon-maskable-512.png +0 -0
- package/dist/index.html +32 -31
- package/dist/robots.txt +2 -0
- package/dist/sw.js +9 -2
- package/mcp/ardy-prompts.mjs +39 -9
- package/mcp/live-hub.mjs +10 -0
- package/mcp/package.json +1 -1
- package/mcp/runtime/package-lock.json +1 -1
- package/mcp/runtime/package.json +1 -1
- package/mcp/server.mjs +124 -22
- package/package.json +18 -8
- package/src/App.jsx +4726 -1969
- package/src/analytics-toggle.jsx +24 -0
- package/src/analytics.js +388 -0
- package/src/app-stage.jsx +2806 -0
- package/src/ardy/export.js +9 -1
- package/src/ardy/ik.js +238 -4
- package/src/ardy/motion-edit.js +41 -0
- package/src/ardy/motion-url.js +55 -0
- package/src/ardy/timeline.jsx +1019 -285
- package/src/asset-pane.jsx +18 -7
- package/src/auto-color.js +63 -0
- package/src/burn-in.js +91 -0
- package/src/camera-block.js +21 -4
- package/src/camera-follow.js +16 -6
- package/src/controls.jsx +44 -4
- package/src/dualview.jsx +89 -10
- package/src/error-boundary.jsx +41 -0
- package/src/gizmo-claim.js +48 -0
- package/src/grid-floor.jsx +97 -0
- package/src/grid-view.js +40 -0
- package/src/hierarchy-model.js +124 -71
- package/src/hierarchy-panel.jsx +101 -6
- package/src/line-edit.js +1348 -0
- package/src/live-control.js +11 -1
- package/src/main.jsx +6 -1
- package/src/matte.js +55 -0
- package/src/motion-trail.js +250 -0
- package/src/object-gizmo.jsx +57 -18
- package/src/object-path.js +224 -0
- package/src/planview.jsx +165 -14
- package/src/pose-thumbs.js +1 -1
- package/src/posestudio.jsx +99 -84
- package/src/project-browser.jsx +63 -6
- package/src/project.js +44 -0
- package/src/props.jsx +169 -27
- package/src/pwa.js +6 -0
- package/src/result-modal.jsx +2 -14
- package/src/room.jsx +37 -10
- package/src/scene-asset-cache.js +3 -0
- package/src/scene-objects.js +156 -3
- package/src/scenes.js +22 -1
- package/src/shot-authoring.js +9 -0
- package/src/shot-guides.js +90 -0
- package/src/shot.js +6 -7
- package/src/speed-envelope.js +254 -0
- package/src/styles.css +4831 -3436
- package/src/take-recipe.js +165 -0
- package/src/timeline-extent.js +39 -0
- package/src/use-render-activity.js +6 -0
- package/tools/ardy/BRIDGE.md +20 -316
- package/tools/ardy/README.md +22 -201
- package/tools/ardy/bridge.mjs +598 -73
- package/tools/ardy/bvh-cskel27.mjs +63 -5
- package/tools/ardy/prompt-limits.mjs +5 -0
- package/tools/ardy/run-on-box.sh +1 -1
- package/tools/ardy/runners/index.mjs +11 -16
- package/tools/ardy/visual-qa.mjs +38 -30
- package/tools/ardy/vq-car.mjs +1 -1
- package/tools/demo-worker/README.md +137 -0
- package/tools/demo-worker/api-client.mjs +239 -0
- package/tools/demo-worker/generate.mjs +303 -0
- package/tools/demo-worker/index.mjs +466 -0
- package/tools/demo-worker/sign-core.mjs +112 -0
- package/tools/demo-worker/sign.mjs +161 -0
- package/tools/dev-full.mjs +54 -36
- package/tools/kimodo/compare-seam.mjs +108 -0
- package/tools/kimodo/constraints.mjs +244 -0
- package/tools/kimodo/edit.mjs +264 -0
- package/tools/kimodo/effector-constraints.mjs +200 -0
- package/tools/kimodo/generate.mjs +597 -0
- package/tools/kimodo/measure-pose.mjs +65 -0
- package/tools/kimodo/measure-preserve.mjs +899 -0
- package/tools/kimodo/measure-waypoints.mjs +55 -0
- package/tools/kimodo/pose-constraints.mjs +250 -0
- package/tools/kimodo/preserve-mask.mjs +543 -0
- package/tools/kimodo/read-npz.mjs +189 -0
- package/tools/kimodo/run-edit-on-box.mjs +155 -0
- package/tools/kimodo/run-sequence-on-box.mjs +178 -0
- package/tools/kimodo/runner.mjs +246 -0
- package/tools/kimodo/seam-batch.mjs +77 -0
- package/tools/kimodo/setup-local.mjs +30 -0
- package/tools/kimodo/setup-on-box.sh +83 -0
- package/tools/kimodo/soma77-to-cskel27.mjs +305 -0
- package/tools/metrics/archive-traffic.mjs +1 -1
- package/tools/metrics-snapshot.mjs +98 -0
- package/tools/prepare-package.mjs +50 -0
- package/tools/process-supervisor.mjs +12 -2
- package/tools/projflow/driver.py +1069 -0
- package/tools/projflow/generate.mjs +805 -0
- package/tools/projflow/hml22-to-cskel27.mjs +735 -0
- package/tools/projflow/line-edit-job.mjs +547 -0
- package/tools/projflow/replay.mjs +452 -0
- package/tools/projflow/runner.mjs +269 -0
- package/tools/projflow/service.mjs +742 -0
- package/tools/run-tests.mjs +112 -19
- package/dist/ardy/assembled/run-jump-15s.npz +0 -0
- package/dist/assets/app-CqOk49Bk.css +0 -1
- package/dist/assets/app-zu633VCI.js +0 -4811
- package/src/generation/client.js +0 -56
- package/src/generation/generation-request.js +0 -17
- package/src/generation/session.js +0 -47
- package/src/generation/shot-spec.js +0 -40
- package/src/generation/use-generation.js +0 -44
- package/tools/generation/README.md +0 -29
- package/tools/generation/bridge.mjs +0 -162
- package/tools/generation/job-store.mjs +0 -51
- package/tools/generation/providers/kling.mjs +0 -76
- package/tools/generation/providers/runway.mjs +0 -101
- package/tools/generation/providers/seedance.mjs +0 -87
- package/tools/generation/providers/shared.mjs +0 -49
- 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
|
|
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
|
-
|
|
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",
|
|
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-
|
|
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-
|
|
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
|
|
106
|
-
the sidecar at it with
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
245
|
-
const
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
|
348
|
-
"set
|
|
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/
|
|
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/
|
|
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/
|
|
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>
|
package/dist/app/index.html
CHANGED
|
@@ -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-
|
|
39
|
-
<link rel="
|
|
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">
|