cozyclay 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +23 -8
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-C9pMpW7s.js +1 -0
- package/dist/assets/app-B_2XB9rw.css +1 -0
- package/dist/assets/app-DAuvKPP8.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
- package/dist/assets/workflow-jJRBR94Z.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/llms.txt +14 -0
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +813 -278
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +121 -36
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +60 -16
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +18 -3
- package/src/shell/ViewportToolbar.jsx +36 -31
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shell/viewport.css +12 -0
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +213 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +60 -24
- package/src/studio-app-binding.js +56 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/gt-render/browser.mjs +23 -0
- package/tools/headless-studio.mjs +85 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +95 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send a storyboard panel to the Workflow canvas: one Upload node holding the
|
|
3
|
+
* panel PNG, wired into a fresh Video generation node whose prompt is the
|
|
4
|
+
* shot prompt plus the panel caption. The helper only appends to the draft
|
|
5
|
+
* (`cozyclay.workflow.v1`); it never runs the workflow and never touches an
|
|
6
|
+
* existing node or edge. Kept free of React so Node tests can drive it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { loadWorkflowGraph, normalizeWorkflowGraph, WORKFLOW_STORAGE_KEY, WORKFLOW_VERSION } from "./project.js";
|
|
10
|
+
import { pastedImageNodeData } from "./workflow/clipboard-image.js";
|
|
11
|
+
import { DEFAULT_NODE_SCHEMAS, defaultFormValues, schemaProperties } from "./workflow/node-schema.js";
|
|
12
|
+
import { buildShotPrompt } from "./shot-prompt.js";
|
|
13
|
+
|
|
14
|
+
const COLUMN_GAP = 380;
|
|
15
|
+
const ROW_GAP = 280;
|
|
16
|
+
const FIRST_POSITION = { x: 80, y: 100 };
|
|
17
|
+
const VIDEO_MODEL = { id: "video-generation", name: "Video generation" };
|
|
18
|
+
|
|
19
|
+
/** The video prompt for a panel: the labelled shot block, then its caption. */
|
|
20
|
+
export function panelVideoPrompt(meta, caption = "") {
|
|
21
|
+
const prompt = buildShotPrompt(meta, { target: "video" });
|
|
22
|
+
const note = typeof caption === "string" ? caption.trim() : "";
|
|
23
|
+
return note ? `${prompt}\nCAPTION: ${note}` : prompt;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function nodeBase(label) {
|
|
27
|
+
return { label, cost: 0, outputHistory: [], outputs: [], resultUrl: null, isLoading: false, errorMsg: null };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function freshId(type, taken, stamp) {
|
|
31
|
+
let id = `${type}-${stamp}`;
|
|
32
|
+
for (let n = 2; taken.has(id); n += 1) id = `${type}-${stamp}-${n}`;
|
|
33
|
+
taken.add(id);
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Pure: the draft with the panel's two nodes and one edge appended. Every
|
|
38
|
+
* pre-existing node and edge is carried over untouched. */
|
|
39
|
+
export function appendPanelToWorkflowGraph(draft, { dataUrl, mimeType = "image/png", fileName = "panel.png", prompt = "", now = Date.now() } = {}) {
|
|
40
|
+
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:image/")) throw new Error("A panel image is required");
|
|
41
|
+
const graph = normalizeWorkflowGraph(draft);
|
|
42
|
+
const taken = new Set(graph.nodes.map((node) => node.id));
|
|
43
|
+
const uploadId = freshId("upload", taken, now);
|
|
44
|
+
const videoId = freshId("video", taken, now);
|
|
45
|
+
// Below everything already on the canvas, left-aligned with it.
|
|
46
|
+
const x = graph.nodes.length ? Math.min(...graph.nodes.map((node) => node.position.x)) : FIRST_POSITION.x;
|
|
47
|
+
const y = graph.nodes.length ? Math.max(...graph.nodes.map((node) => node.position.y)) + ROW_GAP : FIRST_POSITION.y;
|
|
48
|
+
const upload = {
|
|
49
|
+
id: uploadId,
|
|
50
|
+
type: "upload",
|
|
51
|
+
position: { x, y },
|
|
52
|
+
data: { ...nodeBase("Upload"), ...pastedImageNodeData({ name: fileName, type: mimeType }, dataUrl), uploadName: fileName, thumbnail: dataUrl },
|
|
53
|
+
};
|
|
54
|
+
const formValues = { ...defaultFormValues(schemaProperties(DEFAULT_NODE_SCHEMAS, "video", VIDEO_MODEL.id)), prompt };
|
|
55
|
+
const video = {
|
|
56
|
+
id: videoId,
|
|
57
|
+
type: "video",
|
|
58
|
+
position: { x: x + COLUMN_GAP, y },
|
|
59
|
+
data: { ...nodeBase("Video"), prompt, model: VIDEO_MODEL.id, selectedModel: { ...VIDEO_MODEL }, formValues },
|
|
60
|
+
};
|
|
61
|
+
const edge = { id: `e-${uploadId}-${videoId}`, source: uploadId, target: videoId, sourceHandle: "output", targetHandle: "input" };
|
|
62
|
+
return normalizeWorkflowGraph({
|
|
63
|
+
version: WORKFLOW_VERSION,
|
|
64
|
+
nodes: [...graph.nodes, upload, video],
|
|
65
|
+
edges: [...graph.edges, edge],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Write the panel into the stored draft. Unlike storeWorkflowGraph this lets a
|
|
70
|
+
* storage failure (quota, private mode) surface, so the caller can say so. */
|
|
71
|
+
export function sendPanelToWorkflow(panel, storage = globalThis.localStorage, now = Date.now()) {
|
|
72
|
+
if (!storage) throw new Error("Browser storage is unavailable");
|
|
73
|
+
// A corrupt stored draft throws here, before anything is written.
|
|
74
|
+
const draft = loadWorkflowGraph(storage, { strict: true });
|
|
75
|
+
const graph = appendPanelToWorkflowGraph(draft, { ...panel, now });
|
|
76
|
+
storage.setItem(WORKFLOW_STORAGE_KEY, JSON.stringify(graph));
|
|
77
|
+
try { globalThis.dispatchEvent?.(new CustomEvent("cozyclay:workflow-change")); } catch { /* non-browser runtime */ }
|
|
78
|
+
return graph;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A Blob's bytes as a data URL, for a stylized panel read from the asset store. */
|
|
82
|
+
export function bytesToDataUrl(bytes, type = "image/png") {
|
|
83
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
84
|
+
let binary = "";
|
|
85
|
+
for (let i = 0; i < view.length; i += 0x8000) binary += String.fromCharCode.apply(null, view.subarray(i, i + 0x8000));
|
|
86
|
+
return `data:${type};base64,${btoa(binary)}`;
|
|
87
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ESLint ratchet: fail when the error count of any rule exceeds tools/eslint-baseline.json.
|
|
2
|
+
// Usage: node tools/check-eslint.mjs [--update]
|
|
3
|
+
// (no flag) compare current counts with the baseline; exit 1 on any regression
|
|
4
|
+
// --update rewrite the baseline with the current counts
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join, relative } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const baselinePath = join(root, "tools", "eslint-baseline.json");
|
|
12
|
+
|
|
13
|
+
const run = spawnSync(join(root, "node_modules", ".bin", "eslint"), [".", "--format", "json"], {
|
|
14
|
+
cwd: root,
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
// eslint exits 1 when it finds errors; anything else without JSON output is a real failure.
|
|
19
|
+
let results;
|
|
20
|
+
try {
|
|
21
|
+
results = JSON.parse(run.stdout);
|
|
22
|
+
} catch {
|
|
23
|
+
console.error(`eslint did not produce JSON (status ${run.status})\n${run.stderr}`);
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const counts = {};
|
|
28
|
+
const hits = {};
|
|
29
|
+
for (const file of results) {
|
|
30
|
+
for (const m of file.messages) {
|
|
31
|
+
if (m.severity !== 2) continue;
|
|
32
|
+
const rule = m.ruleId ?? "fatal";
|
|
33
|
+
counts[rule] = (counts[rule] ?? 0) + 1;
|
|
34
|
+
(hits[rule] ??= []).push(`${relative(root, file.filePath)}:${m.line}: ${m.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const sorted = Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
|
|
38
|
+
|
|
39
|
+
if (process.argv.includes("--update")) {
|
|
40
|
+
writeFileSync(baselinePath, `${JSON.stringify(sorted, null, "\t")}\n`);
|
|
41
|
+
console.log(`eslint: baseline updated (${Object.keys(sorted).length} rules)`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
|
|
46
|
+
const regressed = Object.keys(sorted).filter((rule) => sorted[rule] > (baseline[rule] ?? 0));
|
|
47
|
+
if (regressed.length) {
|
|
48
|
+
for (const rule of regressed) {
|
|
49
|
+
console.log(`${rule}: ${baseline[rule] ?? 0} -> ${sorted[rule]}`);
|
|
50
|
+
}
|
|
51
|
+
console.log("");
|
|
52
|
+
for (const line of regressed.flatMap((rule) => hits[rule]).slice(0, 10)) console.log(line);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const total = Object.values(sorted).reduce((a, b) => a + b, 0);
|
|
56
|
+
const base = Object.values(baseline).reduce((a, b) => a + b, 0);
|
|
57
|
+
console.log(`eslint: ${total} errors across ${Object.keys(sorted).length} rules (baseline ${base}), 0 regressions`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Usage: node tools/check-knip.mjs [--update]
|
|
3
|
+
// Ratchet for knip: fails (exit 1) on findings that are not in tools/knip-baseline.json.
|
|
4
|
+
// --update rewrites the baseline with the current findings and exits 0.
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const baselinePath = join(root, 'tools', 'knip-baseline.json');
|
|
12
|
+
|
|
13
|
+
// knip exits 1 when it finds issues; only a missing/unparseable report is a failure.
|
|
14
|
+
const run = spawnSync(join(root, 'node_modules', '.bin', 'knip'), ['--reporter', 'json', '--no-progress'], {
|
|
15
|
+
cwd: root,
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
18
|
+
});
|
|
19
|
+
if (run.error) {
|
|
20
|
+
console.error(`knip failed to start: ${run.error.message}`);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
let report;
|
|
24
|
+
try {
|
|
25
|
+
report = JSON.parse(run.stdout);
|
|
26
|
+
} catch {
|
|
27
|
+
console.error(`knip produced no JSON (exit ${run.status})\n${run.stderr}`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const keys = new Set();
|
|
32
|
+
for (const issue of report.issues ?? []) {
|
|
33
|
+
const file = issue.file;
|
|
34
|
+
if (issue.files === true || (Array.isArray(issue.files) && issue.files.length)) keys.add(`file ${file}`);
|
|
35
|
+
for (const e of issue.exports ?? []) keys.add(`export ${file}#${e.name}`);
|
|
36
|
+
for (const e of issue.types ?? []) keys.add(`export ${file}#${e.name}`);
|
|
37
|
+
for (const e of issue.enumMembers ?? []) keys.add(`export ${file}#${e.name}`);
|
|
38
|
+
for (const e of issue.dependencies ?? []) keys.add(`dependency ${file}#${e.name}`);
|
|
39
|
+
for (const e of issue.devDependencies ?? []) keys.add(`dependency ${file}#${e.name}`);
|
|
40
|
+
for (const e of issue.unlisted ?? []) keys.add(`unlisted ${file}#${e.name}`);
|
|
41
|
+
}
|
|
42
|
+
for (const f of report.files ?? []) keys.add(`file ${f}`);
|
|
43
|
+
const current = [...keys].sort();
|
|
44
|
+
|
|
45
|
+
if (process.argv.includes('--update')) {
|
|
46
|
+
writeFileSync(baselinePath, `${JSON.stringify(current, null, '\t')}\n`);
|
|
47
|
+
console.log(`knip: baseline updated, ${current.length} findings`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const baseline = existsSync(baselinePath) ? JSON.parse(readFileSync(baselinePath, 'utf8')) : [];
|
|
52
|
+
const known = new Set(baseline);
|
|
53
|
+
const now = new Set(current);
|
|
54
|
+
const added = current.filter((k) => !known.has(k));
|
|
55
|
+
const fixed = baseline.filter((k) => !now.has(k));
|
|
56
|
+
|
|
57
|
+
for (const k of added) console.log(`new: ${k}`);
|
|
58
|
+
if (fixed.length) console.log(`fixed: ${fixed.length}`);
|
|
59
|
+
console.log(`knip: ${current.length} findings, ${added.length} new, ${fixed.length} fixed (baseline ${baseline.length})`);
|
|
60
|
+
process.exit(added.length ? 1 : 0);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Usage: node tools/check-loc.mjs [--update]
|
|
3
|
+
// Ratchet pure source LOC; --update rewrites tools/loc-baseline.json.
|
|
4
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
9
|
+
const baselinePath = join(root, 'tools', 'loc-baseline.json');
|
|
10
|
+
const extensions = new Set(['.js', '.jsx', '.mjs']);
|
|
11
|
+
const roots = ['src', 'bin', 'mcp', 'tools', 'workers'];
|
|
12
|
+
|
|
13
|
+
function filesUnder(directory) {
|
|
14
|
+
const files = [];
|
|
15
|
+
for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) {
|
|
16
|
+
if (entry.name === 'node_modules' || entry.name === 'dist') continue;
|
|
17
|
+
const path = join(directory, entry.name);
|
|
18
|
+
if (entry.isDirectory()) files.push(...filesUnder(path));
|
|
19
|
+
else if (entry.isFile() && extensions.has(path.slice(path.lastIndexOf('.')))) files.push(path);
|
|
20
|
+
}
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pureLoc(path) {
|
|
25
|
+
return readFileSync(join(root, path), 'utf8')
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.filter((line) => line.trim() && !/^\s*\/\//.test(line)).length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const current = Object.fromEntries(
|
|
31
|
+
roots.flatMap(filesUnder)
|
|
32
|
+
.map((path) => [path.replaceAll('\\', '/'), pureLoc(path)])
|
|
33
|
+
.filter(([, lines]) => lines > 250)
|
|
34
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (process.argv.includes('--update')) {
|
|
38
|
+
writeFileSync(baselinePath, `${JSON.stringify(current, null, '\t')}\n`);
|
|
39
|
+
console.log(`loc: baseline updated (${Object.keys(current).length} files over 250)`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
|
|
44
|
+
const violations = [];
|
|
45
|
+
for (const [path, lines] of Object.entries(current)) {
|
|
46
|
+
if (!(path in baseline)) violations.push(`new oversized file ${path} (${lines} lines)`);
|
|
47
|
+
else if (lines > baseline[path]) violations.push(`${path} grew ${baseline[path]} -> ${lines}`);
|
|
48
|
+
}
|
|
49
|
+
for (const message of violations) console.log(message);
|
|
50
|
+
|
|
51
|
+
const fixed = Object.keys(baseline).filter((path) => !(path in current));
|
|
52
|
+
if (fixed.length) console.log(`fixed: ${fixed.length}`);
|
|
53
|
+
console.log(`loc: ${Object.keys(current).length} files over 250 (baseline ${Object.keys(baseline).length}), ${violations.length} violations`);
|
|
54
|
+
process.exit(violations.length ? 1 : 0);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { bodyBytes, randomNonce, signedHeaders } from "./sign-core.mjs";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
class DemoApiError extends Error {
|
|
12
12
|
constructor(status, code, body = null) {
|
|
13
13
|
super(`demo API ${status}${code ? ` ${code}` : ""}`);
|
|
14
14
|
this.name = "DemoApiError";
|
|
@@ -77,7 +77,7 @@ function asJob(body) {
|
|
|
77
77
|
* `fetchImpl`, `now`, and `nonceFactory` are injectable to make protocol tests
|
|
78
78
|
* deterministic without changing production behavior.
|
|
79
79
|
*/
|
|
80
|
-
|
|
80
|
+
class DemoApiClient {
|
|
81
81
|
constructor({
|
|
82
82
|
baseUrl = process.env.CC_DEMO_API_BASE,
|
|
83
83
|
base,
|
|
@@ -198,42 +198,36 @@ export class DemoApiClient {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
export const ApiClient = DemoApiClient;
|
|
202
|
-
|
|
203
201
|
export function createApiClient(options = {}) {
|
|
204
202
|
return new DemoApiClient(options);
|
|
205
203
|
}
|
|
206
204
|
|
|
207
|
-
export const createDemoApiClient = createApiClient;
|
|
208
|
-
|
|
209
205
|
let defaultClient = null;
|
|
210
206
|
function envClient() {
|
|
211
207
|
if (!defaultClient) defaultClient = createApiClient();
|
|
212
208
|
return defaultClient;
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
|
|
211
|
+
function next(options) {
|
|
216
212
|
return envClient().next(options);
|
|
217
213
|
}
|
|
218
214
|
|
|
219
|
-
|
|
215
|
+
function complete(jobId, leaseToken, npzBuffer, options) {
|
|
220
216
|
return envClient().complete(jobId, leaseToken, npzBuffer, options);
|
|
221
217
|
}
|
|
222
218
|
|
|
223
|
-
|
|
219
|
+
function fail(jobId, leaseToken, reason, options) {
|
|
224
220
|
return envClient().fail(jobId, leaseToken, reason, options);
|
|
225
221
|
}
|
|
226
222
|
|
|
227
|
-
|
|
223
|
+
function heartbeat(jobId, leaseToken, options) {
|
|
228
224
|
return envClient().heartbeat(jobId, leaseToken, options);
|
|
229
225
|
}
|
|
230
226
|
|
|
231
|
-
|
|
227
|
+
function signRequest(secretOrOptions, maybeOptions) {
|
|
232
228
|
if (typeof secretOrOptions === "string") {
|
|
233
229
|
return signedHeaders({ ...(maybeOptions || {}), secret: secretOrOptions });
|
|
234
230
|
}
|
|
235
231
|
return signedHeaders(secretOrOptions);
|
|
236
232
|
}
|
|
237
233
|
|
|
238
|
-
export default createApiClient;
|
|
239
|
-
|
|
@@ -16,7 +16,7 @@ import { createLocalRunner } from "../ardy/runners/local.mjs";
|
|
|
16
16
|
import { killGroup, track } from "../ardy/runners/proc.mjs";
|
|
17
17
|
import { POLICY } from "../../workers/api/src/policy.js";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const JOB_HARD_TIMEOUT_MS = POLICY.JOB_HARD_TIMEOUT_MS;
|
|
20
20
|
const DEFAULT_TIMEOUT_ENV = ["CC_JOB_HARD_TIMEOUT_MS", "JOB_HARD_TIMEOUT_MS"];
|
|
21
21
|
let sharedRunner = null;
|
|
22
22
|
|
|
@@ -122,7 +122,7 @@ function waitForChild(child) {
|
|
|
122
122
|
* installing ARDY. Production callers leave it unset and get
|
|
123
123
|
* createLocalRunner().
|
|
124
124
|
*/
|
|
125
|
-
|
|
125
|
+
async function generate(prompt, durationS, options = {}) {
|
|
126
126
|
// Accept both generate(prompt, duration, options) and generate({prompt,
|
|
127
127
|
// duration}, options). The latter is convenient for queue adapters and does
|
|
128
128
|
// not change the public positional contract.
|
|
@@ -298,6 +298,4 @@ export function createGenerator({ runner = null, workerReady = localWorkerReady,
|
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
export const generateJob = generate;
|
|
302
|
-
export default generate;
|
|
303
301
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"no-async-promise-executor": 1,
|
|
3
|
+
"no-case-declarations": 1,
|
|
4
|
+
"no-constant-binary-expression": 2,
|
|
5
|
+
"no-control-regex": 5,
|
|
6
|
+
"no-dupe-keys": 1,
|
|
7
|
+
"no-loss-of-precision": 1,
|
|
8
|
+
"no-regex-spaces": 2,
|
|
9
|
+
"no-undef": 25,
|
|
10
|
+
"no-unsafe-finally": 5,
|
|
11
|
+
"no-unused-vars": 168,
|
|
12
|
+
"no-useless-assignment": 21,
|
|
13
|
+
"no-useless-catch": 2,
|
|
14
|
+
"no-useless-escape": 7,
|
|
15
|
+
"preserve-caught-error": 16,
|
|
16
|
+
"react-hooks/immutability": 66,
|
|
17
|
+
"react-hooks/purity": 5,
|
|
18
|
+
"react-hooks/refs": 87,
|
|
19
|
+
"react-hooks/rules-of-hooks": 3,
|
|
20
|
+
"react-hooks/set-state-in-effect": 31,
|
|
21
|
+
"react-hooks/use-memo": 1,
|
|
22
|
+
"require-yield": 2
|
|
23
|
+
}
|
|
@@ -154,6 +154,29 @@ export async function openStudioClean(cdp, studioUrl) {
|
|
|
154
154
|
await loaded;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/** Load a portable project into the same Studio that a user opens. The project
|
|
158
|
+
* document is seeded before the app bundle runs, so this path exercises the
|
|
159
|
+
* real scene store and live-control connection instead of a test renderer. */
|
|
160
|
+
export async function openStudioProject(cdp, studioUrl, project) {
|
|
161
|
+
const origin = new URL(studioUrl).origin;
|
|
162
|
+
if (!project?.scenesDocument || typeof project.scenesDocument !== "object") throw new Error("headless Studio needs a parsed project with scenesDocument");
|
|
163
|
+
await cdp.send("Page.enable");
|
|
164
|
+
await cdp.send("Storage.clearDataForOrigin", { origin, storageTypes: "all" });
|
|
165
|
+
const scenes = JSON.stringify(project.scenesDocument), name = String(project.name || "Headless Project");
|
|
166
|
+
const values = [
|
|
167
|
+
`localStorage.setItem('cozyclay.scenes.v5', ${JSON.stringify(scenes)})`,
|
|
168
|
+
`localStorage.setItem('cozyclay.project-session.v1', ${JSON.stringify(JSON.stringify({ name, previsMode: project.previsMode || "animation", updatedAt: Date.now() }))})`,
|
|
169
|
+
project.workflow ? `localStorage.setItem('cozyclay.workflow.v1', ${JSON.stringify(JSON.stringify(project.workflow))})` : null,
|
|
170
|
+
project.workspaceLayout ? `localStorage.setItem('cozyclay.workspace-layout.v1', ${JSON.stringify(JSON.stringify(project.workspaceLayout))})` : null,
|
|
171
|
+
].filter(Boolean);
|
|
172
|
+
const source = `${values.join("; ")};`;
|
|
173
|
+
await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source });
|
|
174
|
+
const loaded = cdp.once("Page.loadEventFired");
|
|
175
|
+
await cdp.send("Page.navigate", { url: studioUrl });
|
|
176
|
+
await loaded;
|
|
177
|
+
await waitFor("Studio canvas", async () => cdp.evaluate("Boolean(document.querySelector('canvas'))"), { timeoutMs: 60000 });
|
|
178
|
+
}
|
|
179
|
+
|
|
157
180
|
export async function terminateAll(children) {
|
|
158
181
|
await Promise.allSettled(children.splice(0).reverse().map((child) => terminateOwned(child)));
|
|
159
182
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Start the real CozyClay Studio in an owned headless Chrome process.
|
|
4
|
+
*
|
|
5
|
+
* This is intentionally a source-checkout tool. It starts the same dev server,
|
|
6
|
+
* seeds a portable .cclayproject into the browser's storage before the app
|
|
7
|
+
* loads, and leaves the live hub available to `cclay live` or an MCP client.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { spawnOwned, terminateOwned } from "./process-supervisor.mjs";
|
|
13
|
+
import { openStudioProject, startChrome, waitFor } from "./gt-render/browser.mjs";
|
|
14
|
+
import { readProjectDocument } from "../src/project.js";
|
|
15
|
+
|
|
16
|
+
const ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
17
|
+
const usage = `headless-studio - start the real Studio without a visible tab
|
|
18
|
+
|
|
19
|
+
node tools/headless-studio.mjs --project shot.cclayproject
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--project <file> portable .cclayproject to load (required)
|
|
23
|
+
--port <port> Vite/Studio port (default 5920)
|
|
24
|
+
--live-port <port> live hub port (default 5924)
|
|
25
|
+
--cdp-port <port> Chrome DevTools port (default 9320)
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
function value(argv, name, fallback = undefined) {
|
|
29
|
+
const index = argv.findIndex((arg) => arg === name || arg.startsWith(`${name}=`));
|
|
30
|
+
if (index < 0) return fallback;
|
|
31
|
+
const arg = argv[index];
|
|
32
|
+
return arg.includes("=") ? arg.slice(name.length + 1) : argv[index + 1];
|
|
33
|
+
}
|
|
34
|
+
function port(argv, name, fallback) {
|
|
35
|
+
const parsed = Number(value(argv, name, fallback));
|
|
36
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) throw new Error(`${name} must be an integer in 1..65535`);
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const argv = process.argv.slice(2);
|
|
41
|
+
if (argv.includes("--help") || argv.includes("-h")) { console.log(usage); process.exit(0); }
|
|
42
|
+
const projectPath = value(argv, "--project");
|
|
43
|
+
if (!projectPath) { console.error(usage); process.exit(2); }
|
|
44
|
+
const projectRaw = readFileSync(resolve(projectPath), "utf8");
|
|
45
|
+
const parsed = readProjectDocument(projectRaw);
|
|
46
|
+
if (!parsed.ok) throw new Error(`Cannot load project: ${parsed.reason}`);
|
|
47
|
+
const studioPort = port(argv, "--port", 5920);
|
|
48
|
+
const livePort = port(argv, "--live-port", 5924);
|
|
49
|
+
const cdpPort = port(argv, "--cdp-port", 9320);
|
|
50
|
+
const children = [];
|
|
51
|
+
const cleanups = [];
|
|
52
|
+
let cdp = null;
|
|
53
|
+
let shuttingDown = false;
|
|
54
|
+
const shutdown = async (code = 0) => {
|
|
55
|
+
if (shuttingDown) return;
|
|
56
|
+
shuttingDown = true;
|
|
57
|
+
try { cdp?.close(); } catch { /* already closed */ }
|
|
58
|
+
for (const cleanup of cleanups.splice(0).reverse()) { try { cleanup(); } catch { /* best effort */ } }
|
|
59
|
+
await Promise.allSettled(children.reverse().map((child) => terminateOwned(child)));
|
|
60
|
+
process.exitCode = code;
|
|
61
|
+
};
|
|
62
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void shutdown(128 + (signal === "SIGINT" ? 2 : 15)); });
|
|
63
|
+
|
|
64
|
+
let dev = null;
|
|
65
|
+
try {
|
|
66
|
+
dev = spawnOwned(process.execPath, [join(ROOT, "tools/dev-full.mjs"), "--host", "127.0.0.1", "--port", String(studioPort)], {
|
|
67
|
+
cwd: ROOT,
|
|
68
|
+
env: { ...process.env, COZYCLAY_LIVE_PORT: String(livePort) },
|
|
69
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
70
|
+
});
|
|
71
|
+
children.push(dev);
|
|
72
|
+
await waitFor("Studio server", async () => (await fetch(`http://127.0.0.1:${studioPort}/app/`)).ok, { child: dev });
|
|
73
|
+
cdp = await startChrome({ port: cdpPort, children, cleanups });
|
|
74
|
+
await openStudioProject(cdp, `http://127.0.0.1:${studioPort}/app/`, parsed.project);
|
|
75
|
+
console.log(JSON.stringify({ ok: true, studio: `http://127.0.0.1:${studioPort}/app/`, livePort, cdpPort, project: parsed.project.name, warnings: parsed.warnings }, null, 2));
|
|
76
|
+
await new Promise((resolve) => {
|
|
77
|
+
const done = () => resolve();
|
|
78
|
+
dev.once("exit", done);
|
|
79
|
+
if (dev.exitCode !== null || dev.signalCode !== null) done();
|
|
80
|
+
});
|
|
81
|
+
await shutdown(dev.exitCode ?? 1);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`headless Studio: ${error?.message || error}`);
|
|
84
|
+
await shutdown(1);
|
|
85
|
+
}
|
|
@@ -99,10 +99,10 @@
|
|
|
99
99
|
|
|
100
100
|
import { buildFullBodyConstraints } from "./pose-constraints.mjs";
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
const LEFT_HAND_TYPE = "left-hand";
|
|
103
|
+
const RIGHT_HAND_TYPE = "right-hand";
|
|
104
|
+
const LEFT_FOOT_TYPE = "left-foot";
|
|
105
|
+
const RIGHT_FOOT_TYPE = "right-foot";
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* IK/mid-joint track id -> Kimodo effector constraint type.
|
|
@@ -50,7 +50,7 @@ const SSH_OPTS = [
|
|
|
50
50
|
"-o", "ServerAliveCountMax=240",
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
const DEFAULT_MODEL = "Kimodo-SOMA-RP-v1.1";
|
|
54
54
|
export const KIMODO_BACKENDS = {
|
|
55
55
|
"nvidia-cuda": { repo: "$HOME/.cozyclay/kimodo", entry: ".venv/bin/kimodo_gen", mode: "cuda", promptFlag: null, durationFlag: "--duration", stepsFlag: "--diffusion_steps", outputFlag: "--output" },
|
|
56
56
|
"kimodo-mlx": { repo: "$HOME/.cozyclay/kimodo-mlx", entry: "$HOME/.cozyclay/kimodo-mlx-venv/bin/python", motion: "$HOME/.cozyclay/kimodo-mlx/models/nvidia-soma-rp-v1.1", text: "$HOME/.cozyclay/kimodo-mlx/models/llm2vec-text-bundle", mode: "mlx", promptFlag: "--prompt", framesFlag: "--frames", stepsFlag: "--steps", outputFlag: null },
|