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.
- package/CHANGELOG.md +193 -0
- package/LICENSE +67 -80
- package/LICENSES/GPL-3.0-or-later.txt +674 -0
- package/LICENSING.md +32 -0
- package/README.md +48 -6
- package/THIRD_PARTY_NOTICES.md +47 -1
- package/bin/cozyclay.mjs +120 -50
- package/bin/mcp-runtime.mjs +115 -0
- package/bin/open-browser.mjs +10 -0
- package/bin/update-check.mjs +158 -0
- package/dist/ai-camera-control/index.html +406 -0
- package/dist/app/index.html +5 -5
- package/dist/ardy/assembled/run-jump-15s.npz +0 -0
- package/dist/assets/app-CqOk49Bk.css +1 -0
- package/dist/assets/app-zu633VCI.js +4811 -0
- package/dist/assets/vision_bundle-jFkh-fIS.js +41 -0
- package/dist/fonts/InstrumentSerif-OFL.txt +93 -0
- package/dist/fonts/Inter-OFL.txt +92 -0
- package/dist/fonts/README.md +15 -0
- package/dist/index.html +62 -17
- package/dist/sitemap.xml +7 -1
- package/mcp/LIVE-PROTOCOL.md +144 -0
- package/mcp/README.md +160 -0
- package/mcp/ardy-prompts.mjs +188 -0
- package/mcp/live-hub.mjs +319 -0
- package/mcp/package.json +27 -0
- package/mcp/runtime/package-lock.json +1211 -0
- package/mcp/runtime/package.json +16 -0
- package/mcp/server.mjs +2008 -0
- package/package.json +142 -90
- package/src/App.jsx +5475 -1028
- package/src/ardy/cskel27.js +7 -2
- package/src/ardy/ik.js +25 -15
- package/src/ardy/motion-edit.js +172 -0
- package/src/ardy/npz.js +64 -3
- package/src/ardy/playback.js +31 -1
- package/src/ardy/pose-pin.js +97 -0
- package/src/ardy/prompt-clips.js +7 -2
- package/src/ardy/retime.js +211 -0
- package/src/ardy/root-drop.js +180 -0
- package/src/ardy/timeline-coordinates.js +13 -0
- package/src/ardy/timeline-resize.js +3 -1
- package/src/ardy/timeline.jsx +471 -63
- package/src/ardy/to-cskel27.js +34 -12
- package/src/ardy/trim.js +33 -0
- package/src/ardy/waypoints.js +4 -1
- package/src/asset-pane.jsx +354 -0
- package/src/asset-shelf.js +82 -0
- package/src/camera-block.js +42 -0
- package/src/camera-follow.js +149 -27
- package/src/camera-move.js +45 -19
- package/src/camera-rail-schedule.js +6 -0
- package/src/cuts.js +67 -54
- package/src/dualview.jsx +58 -12
- package/src/generation/generation-request.js +17 -0
- package/src/generation/use-generation.js +3 -14
- package/src/hierarchy-model.js +100 -16
- package/src/hierarchy-panel.jsx +139 -6
- package/src/live-control.js +142 -0
- package/src/matte-editor.js +543 -0
- package/src/matte.js +503 -0
- package/src/mp4-muxer.js +52 -0
- package/src/multimodel-ingest.js +344 -0
- package/src/object-gizmo.jsx +248 -25
- package/src/offscreen-export.js +166 -0
- package/src/otio.js +217 -0
- package/src/planview.jsx +57 -38
- package/src/pose-extract/detector.js +80 -0
- package/src/pose-extract/image-frame.js +40 -0
- package/src/pose-extract/index.js +4 -0
- package/src/pose-extract/take.js +114 -0
- package/src/pose-extract/video-frames.js +91 -0
- package/src/pose-thumbs.js +152 -0
- package/src/poses.js +19 -265
- package/src/posestudio.jsx +441 -17
- package/src/project-browser.jsx +135 -0
- package/src/project-poses.js +9 -0
- package/src/project.js +373 -0
- package/src/props.jsx +69 -3
- package/src/room.jsx +89 -37
- package/src/sample-at.js +136 -0
- package/src/scene-asset-cache.js +166 -0
- package/src/scene-assets.js +378 -0
- package/src/scene-objects.js +308 -7
- package/src/scenes.js +235 -26
- package/src/shot-authoring.js +120 -41
- package/src/shot.js +53 -13
- package/src/source-offer.jsx +12 -0
- package/src/stable-items.js +83 -0
- package/src/styles.css +2283 -349
- package/src/ui.jsx +32 -4
- package/src/usd-camera.js +145 -0
- package/tools/ardy/BRIDGE.md +11 -7
- package/tools/ardy/README.md +24 -5
- package/tools/ardy/artifacts.mjs +34 -0
- package/tools/ardy/bridge.mjs +89 -12
- package/tools/ardy/bvh-cskel27.mjs +1209 -0
- package/tools/ardy/cclay_constrained_generate.py +123 -11
- package/tools/ardy/cclay_sequence_generate.py +49 -0
- package/tools/ardy/extract.mjs +372 -0
- package/tools/ardy/footage.mjs +467 -0
- package/tools/ardy/npz.mjs +74 -9
- package/tools/ardy/run-on-box.sh +25 -0
- package/tools/ardy/run-sequence-on-box.sh +15 -0
- package/tools/ardy/runners/local.mjs +4 -1
- package/tools/ardy/runners/remote.mjs +8 -2
- package/tools/ardy/setup-text-encoder.py +14 -4
- package/tools/dev-full.mjs +63 -5
- package/tools/generation/bridge.mjs +14 -1
- package/tools/process-supervisor.mjs +97 -1
- package/tools/run-tests.mjs +169 -0
- package/dist/assets/app-Cgpk2hwX.js +0 -4803
- 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="
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 >
|
|
66
|
-
console.error("cozyclay: --port must be
|
|
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/
|
|
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:
|
|
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/
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
297
|
+
server = createServer((req, res) => {
|
|
221
298
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
222
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
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,
|
|
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
|
|
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
|
+
}
|