cozyclay 1.2.0 → 1.5.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 (113) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/LICENSE +67 -80
  3. package/LICENSES/GPL-3.0-or-later.txt +674 -0
  4. package/LICENSING.md +32 -0
  5. package/README.md +48 -6
  6. package/THIRD_PARTY_NOTICES.md +47 -1
  7. package/bin/cozyclay.mjs +120 -50
  8. package/bin/mcp-runtime.mjs +115 -0
  9. package/bin/open-browser.mjs +10 -0
  10. package/bin/update-check.mjs +158 -0
  11. package/dist/ai-camera-control/index.html +406 -0
  12. package/dist/app/index.html +5 -5
  13. package/dist/ardy/assembled/run-jump-15s.npz +0 -0
  14. package/dist/assets/app-CqOk49Bk.css +1 -0
  15. package/dist/assets/app-zu633VCI.js +4811 -0
  16. package/dist/assets/vision_bundle-jFkh-fIS.js +41 -0
  17. package/dist/fonts/InstrumentSerif-OFL.txt +93 -0
  18. package/dist/fonts/Inter-OFL.txt +92 -0
  19. package/dist/fonts/README.md +15 -0
  20. package/dist/index.html +62 -17
  21. package/dist/sitemap.xml +7 -1
  22. package/mcp/LIVE-PROTOCOL.md +144 -0
  23. package/mcp/README.md +160 -0
  24. package/mcp/ardy-prompts.mjs +188 -0
  25. package/mcp/live-hub.mjs +319 -0
  26. package/mcp/package.json +27 -0
  27. package/mcp/runtime/package-lock.json +1211 -0
  28. package/mcp/runtime/package.json +16 -0
  29. package/mcp/server.mjs +2008 -0
  30. package/package.json +142 -90
  31. package/src/App.jsx +5475 -1028
  32. package/src/ardy/cskel27.js +7 -2
  33. package/src/ardy/ik.js +25 -15
  34. package/src/ardy/motion-edit.js +172 -0
  35. package/src/ardy/npz.js +64 -3
  36. package/src/ardy/playback.js +31 -1
  37. package/src/ardy/pose-pin.js +97 -0
  38. package/src/ardy/prompt-clips.js +7 -2
  39. package/src/ardy/retime.js +211 -0
  40. package/src/ardy/root-drop.js +180 -0
  41. package/src/ardy/timeline-coordinates.js +13 -0
  42. package/src/ardy/timeline-resize.js +3 -1
  43. package/src/ardy/timeline.jsx +471 -63
  44. package/src/ardy/to-cskel27.js +34 -12
  45. package/src/ardy/trim.js +33 -0
  46. package/src/ardy/waypoints.js +4 -1
  47. package/src/asset-pane.jsx +354 -0
  48. package/src/asset-shelf.js +82 -0
  49. package/src/camera-block.js +42 -0
  50. package/src/camera-follow.js +149 -27
  51. package/src/camera-move.js +45 -19
  52. package/src/camera-rail-schedule.js +6 -0
  53. package/src/cuts.js +67 -54
  54. package/src/dualview.jsx +58 -12
  55. package/src/generation/generation-request.js +17 -0
  56. package/src/generation/use-generation.js +3 -14
  57. package/src/hierarchy-model.js +100 -16
  58. package/src/hierarchy-panel.jsx +139 -6
  59. package/src/live-control.js +142 -0
  60. package/src/matte-editor.js +543 -0
  61. package/src/matte.js +503 -0
  62. package/src/mp4-muxer.js +52 -0
  63. package/src/multimodel-ingest.js +344 -0
  64. package/src/object-gizmo.jsx +248 -25
  65. package/src/offscreen-export.js +166 -0
  66. package/src/otio.js +217 -0
  67. package/src/planview.jsx +57 -38
  68. package/src/pose-extract/detector.js +80 -0
  69. package/src/pose-extract/image-frame.js +40 -0
  70. package/src/pose-extract/index.js +4 -0
  71. package/src/pose-extract/take.js +114 -0
  72. package/src/pose-extract/video-frames.js +91 -0
  73. package/src/pose-thumbs.js +152 -0
  74. package/src/poses.js +19 -265
  75. package/src/posestudio.jsx +441 -17
  76. package/src/project-browser.jsx +135 -0
  77. package/src/project-poses.js +9 -0
  78. package/src/project.js +373 -0
  79. package/src/props.jsx +69 -3
  80. package/src/room.jsx +89 -37
  81. package/src/sample-at.js +136 -0
  82. package/src/scene-asset-cache.js +166 -0
  83. package/src/scene-assets.js +378 -0
  84. package/src/scene-objects.js +308 -7
  85. package/src/scenes.js +235 -26
  86. package/src/shot-authoring.js +120 -41
  87. package/src/shot.js +53 -13
  88. package/src/source-offer.jsx +12 -0
  89. package/src/stable-items.js +83 -0
  90. package/src/styles.css +2283 -349
  91. package/src/ui.jsx +32 -4
  92. package/src/usd-camera.js +145 -0
  93. package/tools/ardy/BRIDGE.md +11 -7
  94. package/tools/ardy/README.md +24 -5
  95. package/tools/ardy/artifacts.mjs +34 -0
  96. package/tools/ardy/bridge.mjs +89 -12
  97. package/tools/ardy/bvh-cskel27.mjs +1209 -0
  98. package/tools/ardy/cclay_constrained_generate.py +123 -11
  99. package/tools/ardy/cclay_sequence_generate.py +49 -0
  100. package/tools/ardy/extract.mjs +372 -0
  101. package/tools/ardy/footage.mjs +467 -0
  102. package/tools/ardy/npz.mjs +74 -9
  103. package/tools/ardy/run-on-box.sh +25 -0
  104. package/tools/ardy/run-sequence-on-box.sh +15 -0
  105. package/tools/ardy/runners/local.mjs +4 -1
  106. package/tools/ardy/runners/remote.mjs +8 -2
  107. package/tools/ardy/setup-text-encoder.py +14 -4
  108. package/tools/dev-full.mjs +63 -5
  109. package/tools/generation/bridge.mjs +14 -1
  110. package/tools/process-supervisor.mjs +97 -1
  111. package/tools/run-tests.mjs +169 -0
  112. package/dist/assets/app-Cgpk2hwX.js +0 -4803
  113. package/dist/assets/app-DgZvaAE1.css +0 -1
package/LICENSING.md ADDED
@@ -0,0 +1,32 @@
1
+ # CozyClay licensing
2
+
3
+ CozyClay is distributed as a combined work under the GNU Affero General
4
+ Public License, version 3 or later (`AGPL-3.0-or-later`). The complete license
5
+ text is in [`LICENSE`](LICENSE).
6
+
7
+ If you modify CozyClay and let users interact with that modified version over
8
+ a network, section 13 of the AGPL requires you to offer those users the
9
+ Corresponding Source of the version you run, at no charge. CozyClay keeps that
10
+ offer visible in the Studio footer as **Source code (AGPL)**.
11
+
12
+ The official build points that link at this repository. Operators deploying a
13
+ modified version must set `VITE_SOURCE_CODE_URL` at build time to a public URL
14
+ containing the complete Corresponding Source for the version they run.
15
+
16
+ ## Earlier contributions
17
+
18
+ Before 2026-08-21, CozyClay was published under `GPL-3.0-or-later`. Copyright
19
+ in those contributions remains with its respective holders and those portions
20
+ remain available under that license; its text is preserved at
21
+ [`LICENSES/GPL-3.0-or-later.txt`](LICENSES/GPL-3.0-or-later.txt). Section 13 of
22
+ AGPLv3 expressly permits GPLv3 and AGPLv3 code to be combined, with the AGPL
23
+ network-source requirement applying to the combined program.
24
+
25
+ Contributions submitted on or after 2026-08-21 are accepted under
26
+ `AGPL-3.0-or-later` unless a file clearly says otherwise.
27
+
28
+ ## Third-party material
29
+
30
+ The AGPL does not replace the licenses of dependencies, fonts, models, model
31
+ weights, or other third-party material. See
32
+ [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) for their separate terms.
package/README.md CHANGED
@@ -7,18 +7,23 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- <a href="LICENSE"><img alt="License: GPL-3.0" src="https://img.shields.io/badge/license-GPL--3.0-blue"></a>
10
+ Created and maintained by <a href="https://github.com/HaD0Yun">Doyun</a> at <a href="https://github.com/NomaDamas">NomaDamas</a>.
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="LICENSE"><img alt="License: AGPL-3.0" src="https://img.shields.io/badge/license-AGPL--3.0-blue"></a>
11
15
  <a href="https://www.npmjs.com/package/cozyclay"><img alt="npm" src="https://img.shields.io/npm/v/cozyclay"></a>
12
16
  <img alt="Node 22+" src="https://img.shields.io/badge/node-22%2B-brightgreen">
13
- <a href="https://github.com/HaD0Yun/CozyClay/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/HaD0Yun/CozyClay?style=flat"></a>
17
+ <a href="https://github.com/NomaDamas/CozyClay/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/NomaDamas/CozyClay?style=flat"></a>
14
18
  </p>
15
19
 
16
20
  <p align="center">
17
21
  <a href="https://cozyclay.org/">Demo reel</a> ·
18
22
  <a href="#quick-start">Quick start</a> ·
19
23
  <a href="#what-you-can-do">Features</a> ·
24
+ <a href="#ai-control-mcp">AI control</a> ·
20
25
  <a href="#controls">Controls</a> ·
21
- <a href="https://github.com/HaD0Yun/CozyClay/issues">Issues</a>
26
+ <a href="https://github.com/NomaDamas/CozyClay/issues">Issues</a>
22
27
  </p>
23
28
 
24
29
  ---
@@ -43,6 +48,7 @@ https://github.com/user-attachments/assets/1d0113e5-6922-443d-affc-1bdabc666247
43
48
  | **Fly the camera** | Right-drag flies (WASD walks, Q/E cranes), middle-drag pans, Alt+drag orbits the selection, click selects, `F` frames — the muscle memory you already have from a 3D editor. |
44
49
  | **Undo anything** | Every scene mutation goes through one history store: a drag, a scrub, an inspector edit is exactly one undo entry. `Esc` cancels an in-flight drag and restores the pre-drag transform. |
45
50
  | **Generate motion** | Pose characters and export poses, sequence multi-phase motion as Prompt Blocks on a resizable timeline, send them to ARDY, then play the result back with sparse IK correction where the generated motion needs fixing. |
51
+ | **Direct it with an AI** | Connect Claude — or any MCP client — and ask for a shot in plain language. It places the cast, frames “a low wide profile”, generates multi-phase motion, and the viewport moves in front of you. See [AI control](#ai-control-mcp). |
46
52
 
47
53
  ## Requirements
48
54
 
@@ -61,6 +67,8 @@ bunx cozyclay
61
67
 
62
68
  That downloads the built studio and opens it at `http://127.0.0.1:5180`. Nothing to compile, no dependency tree to install. Useful flags: `--port 5200`, `--no-open`, `--no-ardy`.
63
69
 
70
+ A global install gives you `cclay`, the same command with less typing. Once a day the launcher checks npm for a newer release and prints a one-line notice after the studio is up; it stays quiet when you're current or offline. `cclay update` installs the latest release, and `--no-update-check` skips the check entirely.
71
+
64
72
  Motion generation stays off until you point it at a machine that can run it:
65
73
 
66
74
  ```bash
@@ -69,10 +77,40 @@ CCLAY_ARDY_HOST=user@your-gpu-box npx cozyclay
69
77
 
70
78
  Everything else — staging through camera work and playback — runs without it.
71
79
 
80
+ ## AI control (MCP)
81
+
82
+ The studio ships an [MCP](https://modelcontextprotocol.io) server, so an AI assistant can drive it — the same scene, the same viewport, live:
83
+
84
+ > “Put a detective and a courier in an alley, give me a low wide profile shot,
85
+ > then make her stand up from the chair, sprint, and trip.”
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "cozyclay": {
91
+ "command": "npx",
92
+ "args": ["-y", "cozyclay", "mcp"]
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Drop that into `claude_desktop_config.json` (or any MCP client config) and restart the client. The
99
+ first run automatically installs the MCP SDK's 95-package tree; opening the studio never waits on
100
+ it, so those dependencies are fetched only when you actually want the server.
101
+
102
+ - **Editor open?** Tool calls move the visible viewport — camera, cast, set, generated motion, prompt blocks on the timeline.
103
+ - **No editor?** Scene and project tools run headless: block scenes, derive film vocabulary
104
+ (“wide shot · right profile · knee level · 24mm”), render AI video prompts, and write
105
+ `.cclayproject` files. `capture_frame`, `set_prompt_blocks`, `generate_motion`, and
106
+ `apply_batch` require the live editor.
107
+
108
+ Tools, transports and the live-control protocol are documented in [`mcp/README.md`](mcp/README.md).
109
+
72
110
  ### From a clone
73
111
 
74
112
  ```bash
75
- git clone https://github.com/HaD0Yun/CozyClay.git
113
+ git clone https://github.com/NomaDamas/CozyClay.git
76
114
  cd CozyClay
77
115
  npm install
78
116
  npm run dev
@@ -123,6 +161,8 @@ See [`tools/ardy/README.md`](tools/ardy/README.md) for details. This workflow is
123
161
  | `npm run test:theme` / `test:appearance` / `test:layout` | UI theme, appearance, layout |
124
162
  | `npm run test:lifecycle` | Dev-server process lifecycle |
125
163
  | `npm run test:ardy` | ARDY conversion, playback, and IK pipeline |
164
+ | `cd mcp && npm install && npm run verify` | MCP server over real stdio — all 420 framing combinations |
165
+ | `cd mcp && npm run verify:live` | Live-control protocol against a fake editor (same `npm install` first) |
126
166
  | `npm run build` | Production build |
127
167
 
128
168
  Ad-hoc browser QA, while a dev server is available:
@@ -133,12 +173,14 @@ npm run qa:browser -- <qa-script>
133
173
 
134
174
  ## Contributing
135
175
 
136
- Found something broken, or want a feature? [Open an issue](https://github.com/HaD0Yun/CozyClay/issues) — bug reports with a repro are the most useful thing you can send.
176
+ Found something broken, or want a feature? [Open an issue](https://github.com/NomaDamas/CozyClay/issues) — bug reports with a repro are the most useful thing you can send. Contributions are accepted under `AGPL-3.0-or-later`.
137
177
 
138
178
  **Repository hygiene.** Generated motion archives, QA output, build output, logs and local runtime artifacts are not source files and must not be committed. Keep `tools/ardy/out/`, `artifacts/`, `dist/`, `.gjc/` and `.npz` files local.
139
179
 
180
+ All runtime libraries intentionally live in `devDependencies` because the published npm package ships the prebuilt `dist/`, so `npx cozyclay` must not install the studio's dependency tree.
181
+
140
182
  ## License & credits
141
183
 
142
- GNU General Public License v3.0 or later — see [`LICENSE`](LICENSE). Third-party projects retain their own licenses and copyright; see [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
184
+ GNU Affero General Public License v3.0 or later — see [`LICENSE`](LICENSE) and the transition details in [`LICENSING.md`](LICENSING.md). Modified network services must offer their users the corresponding source. Third-party projects retain their own licenses and copyright; see [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
143
185
 
144
186
  CozyClay can connect to [NVIDIA ARDY](https://github.com/nv-tlabs/ardy) for motion generation. ARDY is a separate third-party project owned and maintained by NVIDIA; it is not included in this repository, and CozyClay is not affiliated with or endorsed by NVIDIA.
@@ -11,6 +11,16 @@ CozyClay's browser-based 3D studio uses [Three.js](https://threejs.org/) through
11
11
  - Source: https://github.com/mrdoob/three.js
12
12
  - License text: https://github.com/mrdoob/three.js/blob/dev/LICENSE
13
13
 
14
+ ## Mediabunny
15
+
16
+ CozyClay uses [Mediabunny](https://mediabunny.dev/) to mux recorded WebCodecs
17
+ video frames into MP4 files in the browser.
18
+
19
+ - Copyright (c) 2024-2026 Vanilagy
20
+ - License: MPL-2.0
21
+ - Source: https://github.com/Vanilagy/mediabunny
22
+ - License text: https://github.com/Vanilagy/mediabunny/blob/main/LICENSE
23
+
14
24
  ## NVIDIA ARDY
15
25
 
16
26
  CozyClay provides an optional bridge and data-conversion workflow for externally installed [ARDY](https://github.com/nv-tlabs/ardy), an interactive human-motion generation project from NVIDIA Research.
@@ -55,6 +65,42 @@ bundled.
55
65
  - Source: https://github.com/McGill-NLP/llm2vec
56
66
  - License text: https://github.com/McGill-NLP/llm2vec/blob/main/LICENSE
57
67
 
68
+ ## Fonts
69
+
70
+ CozyClay bundles subsets of two typefaces. Both are licensed under the SIL Open
71
+ Font License 1.1, which allows them to be redistributed with software as long as
72
+ the copyright notice and the licence travel with the files. The licence texts are
73
+ in `public/fonts/` next to the fonts themselves.
74
+
75
+ ### Inter
76
+
77
+ - Copyright (c) 2016 The Inter Project Authors
78
+ - License: SIL Open Font License 1.1
79
+ - Source: https://github.com/rsms/inter
80
+ - License text: `public/fonts/Inter-OFL.txt`
81
+
82
+ ### Instrument Serif
83
+
84
+ - Copyright 2022 The Instrument Serif Project Authors
85
+ - License: SIL Open Font License 1.1
86
+ - Source: https://github.com/Instrument/instrument-serif
87
+ - License text: `public/fonts/InstrumentSerif-OFL.txt`
88
+
89
+ ## Character models
90
+
91
+ The rigs in `public/models/` are Mixamo characters from Adobe. Adobe permits
92
+ their use in projects but not the distribution of the raw character files, which
93
+ is what shipping them in this repository, the npm package and the hosted site
94
+ amounts to. They are also outside the scope of this repository's AGPL-3.0 grant:
95
+ nothing here relicenses them, and a fork does not acquire the right to
96
+ redistribute them.
97
+
98
+ They are being replaced with a CC0 rig. Until that lands, treat these two files
99
+ as third-party content that this licence does not cover.
100
+
101
+ - `x-bot-tpose.fbx`, `y-bot-tpose.fbx` — Adobe Mixamo
102
+ - Terms: https://www.adobe.com/legal/terms.html
103
+
58
104
  ## CozyClay license scope
59
105
 
60
- The CozyClay source code in this repository is licensed under GPL-3.0-or-later. That license does not replace or relicense Three.js, ARDY, ARDY model checkpoints, or any other third-party component.
106
+ The CozyClay combined work in this repository is distributed under AGPL-3.0-or-later, subject to the transition details in `LICENSING.md`. That license does not replace or relicense Three.js, ARDY, ARDY model checkpoints, the bundled fonts, the character rigs, or any other third-party component.
package/bin/cozyclay.mjs CHANGED
@@ -8,24 +8,28 @@
8
8
  * `dist/`, so this launcher only has to
9
9
  *
10
10
  * - serve those files over loopback,
11
- * - forward /ardy to the sidecar on 5181 (the job Vite's dev proxy does),
11
+ * - forward /ardy to its dynamically selected sidecar port (the 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
15
15
  * before it can serve a prebuilt app is a launcher that will break.
16
16
  */
17
17
  import { spawn } from "node:child_process";
18
+ import { startBridge, terminateOwned } from "../tools/process-supervisor.mjs";
18
19
  import { createReadStream, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
19
20
  import { createServer, request as httpRequest } from "node:http";
20
21
  import { homedir } from "node:os";
21
22
  import { extname, join, normalize, resolve } from "node:path";
22
23
  import { createInterface } from "node:readline";
23
24
  import { fileURLToPath } from "node:url";
25
+ import { runMcp } from "./mcp-runtime.mjs";
26
+ import { openBrowser } from "./open-browser.mjs";
27
+ import { checkForUpdate, runUpdate } from "./update-check.mjs";
24
28
 
25
29
  const PKG_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)));
26
30
  const DIST = join(PKG_ROOT, "dist");
27
31
  const BRIDGE = join(PKG_ROOT, "tools", "ardy", "bridge.mjs");
28
- const BRIDGE_PORT = Number(process.env.COZYCLAY_BRIDGE_PORT ?? 5181);
32
+ let bridgePort = 5181;
29
33
 
30
34
  const TYPES = {
31
35
  ".html": "text/html; charset=utf-8",
@@ -45,7 +49,7 @@ const TYPES = {
45
49
  };
46
50
 
47
51
  function parseArgs(argv) {
48
- const opts = { port: 5180, host: "127.0.0.1", ardy: true, open: true, star: true };
52
+ const opts = { port: 5180, host: "127.0.0.1", ardy: true, open: true, star: true, updateCheck: true };
49
53
  for (let i = 0; i < argv.length; i += 1) {
50
54
  const arg = argv[i];
51
55
  if (arg === "--port" || arg === "-p") opts.port = Number(argv[++i]);
@@ -55,21 +59,29 @@ function parseArgs(argv) {
55
59
  else if (arg === "--no-ardy") opts.ardy = false;
56
60
  else if (arg === "--no-open") opts.open = false;
57
61
  else if (arg === "--no-star") opts.star = false;
62
+ else if (arg === "--no-update-check") opts.updateCheck = false;
58
63
  else if (arg === "--help" || arg === "-h") opts.help = true;
59
64
  else if (arg === "--version" || arg === "-v") opts.version = true;
60
65
  else {
61
66
  console.error(`cozyclay: unknown option ${arg}`);
62
67
  opts.help = true;
68
+ opts.invalid = true;
63
69
  }
64
70
  }
65
- if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
66
- console.error("cozyclay: --port must be a port number");
71
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65534) {
72
+ console.error("cozyclay: --port must be an integer in 1..65534");
67
73
  opts.help = true;
74
+ opts.invalid = true;
75
+ }
76
+ if (opts.host !== "127.0.0.1") {
77
+ console.error("cozyclay: --host is restricted to 127.0.0.1");
78
+ opts.help = true;
79
+ opts.invalid = true;
68
80
  }
69
81
  return opts;
70
82
  }
71
83
 
72
- const REPO_URL = "https://github.com/HaD0Yun/CozyClay";
84
+ const REPO_URL = "https://github.com/NomaDamas/CozyClay";
73
85
  const STATE_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cozyclay");
74
86
  const STATE_FILE = join(STATE_DIR, "state.json");
75
87
  // Only worth asking someone who actually used the thing. A prompt three
@@ -79,10 +91,16 @@ const STAR_AFTER_MS = Number(process.env.COZYCLAY_STAR_AFTER_MS ?? 60_000);
79
91
  const HELP = `cozyclay - browser-based 3D staging studio
80
92
 
81
93
  npx cozyclay start the studio and open it
94
+ npx cozyclay mcp run the MCP server (for Claude, Cursor, any MCP client)
95
+ cclay update install the latest cozyclay globally (npm install -g)
82
96
  npx cozyclay --port 5200 serve on another port
83
97
  npx cozyclay --no-ardy skip the optional motion-generation sidecar
84
98
  npx cozyclay --no-open do not open a browser
85
99
  npx cozyclay --no-star never ask about starring the repo
100
+ npx cozyclay --no-update-check
101
+ do not look for a newer release
102
+
103
+ cclay is the same command, shorter: a global install gives you both.
86
104
 
87
105
  Motion generation needs an SSH-reachable NVIDIA machine running ARDY; point
88
106
  the sidecar at it with CCLAY_ARDY_HOST. Everything else - staging, posing,
@@ -104,7 +122,7 @@ function serveFile(res, path) {
104
122
  // browser code needs no build-time knowledge of how it was launched.
105
123
  function proxyToBridge(req, res) {
106
124
  const upstream = httpRequest(
107
- { host: "127.0.0.1", port: BRIDGE_PORT, path: req.url, method: req.method, headers: req.headers },
125
+ { host: "127.0.0.1", port: bridgePort, path: req.url, method: req.method, headers: req.headers },
108
126
  (upstreamRes) => {
109
127
  res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
110
128
  upstreamRes.pipe(res);
@@ -138,7 +156,7 @@ function writeState(patch) {
138
156
 
139
157
  async function starCount() {
140
158
  try {
141
- const res = await fetch("https://api.github.com/repos/HaD0Yun/CozyClay", {
159
+ const res = await fetch("https://api.github.com/repos/NomaDamas/CozyClay", {
142
160
  headers: { accept: "application/vnd.github+json" },
143
161
  signal: AbortSignal.timeout(2500),
144
162
  });
@@ -179,26 +197,42 @@ async function maybeAskForStar(startedAt, opts) {
179
197
  }
180
198
  }
181
199
 
182
- function openBrowser(url) {
183
- const cmd =
184
- process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
185
- const child = spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" });
186
- child.on("error", () => {
187
- /* headless box, no browser: the URL is printed anyway */
188
- });
189
- child.unref();
200
+ // Read on every launch now, so it must not be able to take the launcher down:
201
+ // the version is cosmetic here, and "0.0.0" just suppresses a bogus notice.
202
+ 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
+ }
190
208
  }
191
209
 
192
- const opts = parseArgs(process.argv.slice(2));
193
- if (opts.help) {
194
- console.log(HELP);
195
- process.exit(0);
210
+ const argv = process.argv.slice(2);
211
+ if (argv[0] === "mcp") {
212
+ await runMcp(argv.slice(1));
213
+ } else if (argv[0] === "update") {
214
+ // This branch bypasses parseArgs, so anything trailing would be swallowed
215
+ // and `cclay update --help` would perform an unrequested global install.
216
+ if (argv.length > 1) {
217
+ console.error(`cozyclay: update takes no arguments (got ${argv.slice(1).join(" ")})`);
218
+ process.exit(1);
219
+ }
220
+ runUpdate();
221
+ } else {
222
+
223
+ const opts = parseArgs(argv);
224
+ if (opts.help) {
225
+ console.log(HELP);
226
+ process.exit(opts.invalid ? 1 : 0);
196
227
  }
228
+ const version = readVersion();
197
229
  if (opts.version) {
198
- const pkg = JSON.parse(await import("node:fs").then((fs) => fs.promises.readFile(join(PKG_ROOT, "package.json"), "utf8")));
199
- console.log(pkg.version);
230
+ console.log(version);
200
231
  process.exit(0);
201
232
  }
233
+ // Fired before anything blocking and never awaited on the launch path: the
234
+ // notice is worth a line of output, never a second of startup.
235
+ const updatePending = opts.updateCheck && !process.env.CI && !process.env.COZYCLAY_NO_UPDATE_CHECK ? checkForUpdate(version, STATE_DIR) : null;
202
236
  if (!existsSync(join(DIST, "app", "index.html"))) {
203
237
  console.error("cozyclay: this package is missing its build (dist/app/index.html).");
204
238
  console.error("cozyclay: from a clone, run `npm install && npm run build` first.");
@@ -210,16 +244,62 @@ if (!existsSync(join(DIST, "app", "index.html"))) {
210
244
  // cannot act on. An unset CCLAY_ARDY_HOST is the normal case, not a fault.
211
245
  const ardyHost = process.env.CCLAY_ARDY_HOST?.trim();
212
246
  let bridge = null;
247
+ let server = null;
248
+ const startedAt = Date.now();
249
+ let shuttingDown = false;
250
+ async function shutdown(exitCode = 0) {
251
+ if (shuttingDown) return;
252
+ shuttingDown = true;
253
+ if (bridge) await terminateOwned(bridge);
254
+ if (server?.listening) await new Promise((resolvePromise) => server.close(() => resolvePromise()));
255
+ try {
256
+ await maybeAskForStar(startedAt, opts);
257
+ } catch {
258
+ /* never let the goodbye prompt hold the process hostage */
259
+ }
260
+ process.exit(exitCode);
261
+ }
262
+ process.on("SIGINT", () => shutdown(130));
263
+ process.on("SIGTERM", () => shutdown(143));
264
+
213
265
  if (opts.ardy && ardyHost && existsSync(BRIDGE)) {
214
- bridge = spawn(process.execPath, [BRIDGE], { cwd: PKG_ROOT, stdio: "inherit" });
215
- bridge.on("error", () => {
216
- bridge = null;
217
- });
266
+ try {
267
+ ({ child: bridge, port: bridgePort } = await startBridge({
268
+ command: process.execPath,
269
+ args: [BRIDGE],
270
+ cwd: PKG_ROOT,
271
+ env: process.env,
272
+ mainPort: opts.port,
273
+ onSpawn: (child) => {
274
+ bridge = child;
275
+ },
276
+ onReady: (child) => {
277
+ child.once("exit", (code, signal) => {
278
+ if (shuttingDown) return;
279
+ console.error(`cozyclay: motion generation sidecar exited ${signal ? `from ${signal}` : `with code ${code ?? 0}`}.`);
280
+ void shutdown(1);
281
+ });
282
+ child.once("error", (err) => {
283
+ if (shuttingDown) return;
284
+ console.error(`cozyclay: motion generation sidecar failed: ${err.message}`);
285
+ void shutdown(1);
286
+ });
287
+ },
288
+ }));
289
+ } catch (err) {
290
+ 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.");
292
+ if (bridge) await terminateOwned(bridge);
293
+ process.exit(1);
294
+ }
218
295
  }
219
296
 
220
- const server = createServer((req, res) => {
297
+ server = createServer((req, res) => {
221
298
  const url = new URL(req.url ?? "/", "http://localhost");
222
- if (url.pathname.startsWith("/ardy/")) {
299
+ // Only the routes the bridge actually owns: /ardy/ is ALSO a public asset
300
+ // directory (cskel27-rest.json), and those files live in dist/, not behind
301
+ // the sidecar. Same rule as the Vite dev proxy bypass.
302
+ if (/^\/ardy\/(health|bases|generate|footage|extract|motions)(\/|$)/.test(url.pathname)) {
223
303
  proxyToBridge(req, res);
224
304
  return;
225
305
  }
@@ -245,37 +325,20 @@ const server = createServer((req, res) => {
245
325
  serveFile(res, target);
246
326
  });
247
327
 
248
- const startedAt = Date.now();
249
- let shuttingDown = false;
250
- async function shutdown() {
251
- if (shuttingDown) process.exit(0);
252
- shuttingDown = true;
253
- if (bridge) bridge.kill("SIGTERM");
254
- server.close();
255
- try {
256
- await maybeAskForStar(startedAt, opts);
257
- } catch {
258
- /* never let the goodbye prompt hold the process hostage */
259
- }
260
- process.exit(0);
261
- }
262
- process.on("SIGINT", shutdown);
263
- process.on("SIGTERM", shutdown);
264
-
265
328
  server.on("error", (err) => {
266
329
  if (err && err.code === "EADDRINUSE") {
267
- console.error(`cozyclay: port ${opts.port} is taken. Try --port ${opts.port + 1}.`);
268
- if (bridge) bridge.kill("SIGTERM");
269
- process.exit(1);
330
+ console.error(`cozyclay: port ${opts.port} is taken. Try --port 5200.`);
331
+ void shutdown(1);
332
+ return;
270
333
  }
271
334
  throw err;
272
335
  });
273
336
 
274
- server.listen(opts.port, opts.host, () => {
337
+ server.listen({ port: opts.port, host: "127.0.0.1", ipv6Only: false }, () => {
275
338
  // The package exists to open the studio, which the site serves from /app/.
276
339
  // Landing on "/" would greet someone who just typed `npx cozyclay` with a
277
340
  // marketing page.
278
- const url = `http://${opts.host}:${opts.port}/app/`;
341
+ const url = `http://127.0.0.1:${opts.port}/app/`;
279
342
  console.log(`CozyClay is running at ${url}`);
280
343
  if (!opts.ardy) console.log("Motion generation: off (--no-ardy).");
281
344
  else if (bridge) console.log(`Motion generation: sidecar running against ${ardyHost}.`);
@@ -285,4 +348,11 @@ server.listen(opts.port, opts.host, () => {
285
348
  "set CCLAY_ARDY_HOST=user@host to turn it on. Everything else works without it.",
286
349
  );
287
350
  if (opts.open) openBrowser(url);
351
+ void updatePending?.then((latest) => {
352
+ // `cclay` only exists after a global install, so name the command that
353
+ // works from an npx run too.
354
+ if (latest) console.log(`cozyclay ${latest} is available (you have ${version}). Run: npm install -g cozyclay@latest`);
355
+ });
288
356
  });
357
+
358
+ }
@@ -0,0 +1,115 @@
1
+ import { spawn } from "node:child_process";
2
+ import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { createRequire } from "node:module";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const PKG_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)));
9
+ const PACKAGE_VERSION = JSON.parse(await import("node:fs/promises").then((fs) => fs.readFile(join(PKG_ROOT, "package.json"), "utf8"))).version;
10
+ const MCP_RUNTIME_SOURCE = join(PKG_ROOT, "mcp", "runtime");
11
+ const MCP_RUNTIME = process.env.COZYCLAY_MCP_RUNTIME_DIR || join(
12
+ process.env.XDG_CACHE_HOME || process.env.LOCALAPPDATA || join(homedir(), ".cache"),
13
+ "cozyclay",
14
+ "mcp-runtime",
15
+ PACKAGE_VERSION,
16
+ );
17
+
18
+ function probeMcpRuntime(runtime = MCP_RUNTIME) {
19
+ const server = join(runtime, "mcp", "server.mjs");
20
+ if (!existsSync(server)) throw new Error("MCP server is missing");
21
+ const runtimeRequire = createRequire(join(runtime, "package.json"));
22
+ for (const dependency of ["@modelcontextprotocol/sdk/server/mcp.js", "three", "ws", "zod"]) runtimeRequire.resolve(dependency);
23
+ return server;
24
+ }
25
+
26
+ function installMcpRuntime() {
27
+ return new Promise((done) => {
28
+ const parent = dirname(MCP_RUNTIME);
29
+ mkdirSync(parent, { recursive: true });
30
+ try {
31
+ probeMcpRuntime();
32
+ done({ code: 0 });
33
+ return;
34
+ } catch {
35
+ // A killed older install can leave an incomplete final directory.
36
+ // Only complete stagings are ever renamed here, so an invalid final
37
+ // cache is safe to discard before this attempt begins.
38
+ rmSync(MCP_RUNTIME, { recursive: true, force: true });
39
+ }
40
+ const staging = mkdtempSync(join(parent, `${basename(MCP_RUNTIME)}.install-`));
41
+ copyFileSync(join(MCP_RUNTIME_SOURCE, "package.json"), join(staging, "package.json"));
42
+ copyFileSync(join(MCP_RUNTIME_SOURCE, "package-lock.json"), join(staging, "package-lock.json"));
43
+ cpSync(join(PKG_ROOT, "mcp"), join(staging, "mcp"), { recursive: true });
44
+ cpSync(join(PKG_ROOT, "src"), join(staging, "src"), { recursive: true });
45
+
46
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
47
+ const child = spawn(npm, ["ci", "--no-audit", "--no-fund"], {
48
+ cwd: staging,
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ shell: process.platform === "win32",
51
+ });
52
+ child.stdout.pipe(process.stderr);
53
+ child.stderr.pipe(process.stderr);
54
+ child.on("error", (error) => {
55
+ rmSync(staging, { recursive: true, force: true });
56
+ done({ code: 1, error });
57
+ });
58
+ child.on("exit", (code) => {
59
+ if (code !== 0) {
60
+ rmSync(staging, { recursive: true, force: true });
61
+ done({ code: code ?? 1 });
62
+ return;
63
+ }
64
+ try {
65
+ probeMcpRuntime(staging);
66
+ try {
67
+ probeMcpRuntime();
68
+ rmSync(staging, { recursive: true, force: true });
69
+ } catch {
70
+ try {
71
+ renameSync(staging, MCP_RUNTIME);
72
+ } catch (error) {
73
+ // Another first run can publish its complete staging
74
+ // directory first. Accept that winner only after the
75
+ // same full probe; never execute a partial cache.
76
+ probeMcpRuntime();
77
+ rmSync(staging, { recursive: true, force: true });
78
+ }
79
+ }
80
+ done({ code: 0 });
81
+ } catch (error) {
82
+ rmSync(staging, { recursive: true, force: true });
83
+ done({ code: 1, error });
84
+ }
85
+ });
86
+ });
87
+ }
88
+
89
+ export async function runMcp(rest) {
90
+ if (!existsSync(join(PKG_ROOT, "mcp", "server.mjs")) || !existsSync(join(MCP_RUNTIME_SOURCE, "package-lock.json"))) {
91
+ console.error("cozyclay: this build does not include the MCP server runtime.");
92
+ process.exit(1);
93
+ }
94
+
95
+ let server;
96
+ try {
97
+ server = probeMcpRuntime();
98
+ } catch {
99
+ console.error("cozyclay: installing MCP server dependencies (one-time per CozyClay version)...");
100
+ const result = await installMcpRuntime();
101
+ if (result.code !== 0) {
102
+ console.error(`cozyclay: npm ci failed${result.error ? `: ${result.error.message}` : ` (exit ${result.code})`}. Check your network connection and retry.`);
103
+ console.error(`cozyclay: the published package was not changed; remove ${JSON.stringify(MCP_RUNTIME)} before retrying a damaged cache.`);
104
+ process.exit(1);
105
+ }
106
+ try {
107
+ server = probeMcpRuntime();
108
+ } catch (error) {
109
+ console.error(`cozyclay: MCP runtime is incomplete after npm ci: ${error.message}`);
110
+ process.exit(1);
111
+ }
112
+ }
113
+ const child = spawn(process.execPath, [server, ...rest], { stdio: "inherit" });
114
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
115
+ }
@@ -0,0 +1,10 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function openBrowser(url) {
4
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
5
+ const child = spawn(command, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" });
6
+ child.on("error", () => {
7
+ /* headless box, no browser: the URL is printed anyway */
8
+ });
9
+ child.unref();
10
+ }