cozyclay 3.1.6 → 3.2.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 +145 -0
- package/INSTALL_FOR_AGENTS.md +4 -4
- package/README.md +4 -2
- package/bin/agent/agent-failure-codes.mjs +34 -8
- package/bin/agent/agent-routes.mjs +6 -6
- package/bin/agent/agent-runner.mjs +25 -17
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -1
- package/bin/agent/openai-compatible-provider.mjs +24 -1
- package/bin/agent/studio-prompt.mjs +2 -2
- package/bin/agent/supervisor-followup.mjs +34 -0
- package/bin/agent/supervisor.mjs +6 -6
- package/bin/cozyclay.mjs +23 -60
- package/bin/star-prompt.mjs +15 -14
- package/bin/studio-routes.mjs +6 -3
- package/bin/telemetry-state.mjs +5 -7
- package/bin/update-check.mjs +61 -7
- package/bin/update-install.mjs +16 -3
- package/bin/update-restart.mjs +95 -0
- package/bin/update-routes.mjs +25 -3
- package/dist/app/index.html +13 -7
- package/dist/assets/TaskBoard-tgO0MNKL.js +1 -0
- package/dist/assets/analytics-CaTUuLPM.js +1 -0
- package/dist/assets/{app-CEP7jzXB.css → app-CnDkB5WO.css} +1 -1
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/compile-worker-D1pl6G7x.js +6 -0
- package/dist/assets/landing-2Fy7DU41.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BxaM0Xwl.js +1 -0
- package/dist/assets/scene-asset-sync-CLGNbEBq.js +13 -0
- package/dist/assets/task-board-UVGTZLRF.js +15 -0
- package/dist/assets/workflow-CSiBlX1s.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +4 -1
- package/dist/privacy/index.html +9 -6
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +4 -4
- package/mcp/tool-handlers.mjs +14 -16
- package/package.json +3 -1
- package/src/App.jsx +42 -75
- package/src/analytics-controls.js +25 -3
- package/src/analytics-strict.js +16 -4
- package/src/analytics.js +32 -26
- package/src/app-version.js +13 -0
- package/src/asset-drop.js +39 -0
- package/src/asset-pane.css +34 -0
- package/src/asset-pane.jsx +4 -35
- package/src/bus-tasks.js +1 -1
- package/src/character-kind.js +8 -6
- package/src/click-friction.js +5 -4
- package/src/command-bus-telemetry.js +8 -7
- package/src/command-bus.js +2 -16
- package/src/commands/cast.js +2 -1
- package/src/commands/index.js +2 -1
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- package/src/commands/physics.js +21 -0
- package/src/commands/project.js +3 -3
- package/src/commands/sculpt-compare.js +53 -0
- package/src/content-tile.jsx +36 -0
- package/src/domains/cast.js +7 -6
- package/src/domains/motion.js +13 -14
- package/src/domains/scenes.js +41 -31
- package/src/execution-telemetry.js +7 -3
- package/src/export-menu-telemetry.js +26 -0
- package/src/finger-handles.jsx +128 -0
- package/src/friction-hint.js +82 -0
- package/src/live-control.js +2 -1
- package/src/locale-zh/panels.js +20 -0
- package/src/locale-zh/shell.js +4 -0
- package/src/locale-zh/workspace.js +6 -0
- package/src/motion/center-of-mass.js +5 -23
- package/src/motion/contact-project.js +143 -0
- package/src/motion/find-bone.js +13 -0
- package/src/motion/fingers.js +170 -0
- package/src/motion/fingertip-pick.js +21 -0
- package/src/motion/fix-collisions.js +1 -1
- package/src/motion/ik-drag.js +93 -1
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion/timeline.css +12 -0
- package/src/motion/timeline.jsx +2 -2
- package/src/motion3/compile-job.js +3 -2
- package/src/motion3/compiler/compile.js +3 -3
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/compiler/spring.js +95 -0
- package/src/motion3/compiler/validate-objects.js +18 -2
- package/src/motion3/compiler/validate-parts.js +1 -0
- package/src/motion3/compiler/vocab.js +1 -1
- package/src/motion3/fix/index.js +8 -4
- package/src/motion3/fix/person.js +110 -0
- package/src/motion3/free-motion.js +49 -11
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/object-pose.js +44 -2
- package/src/motion3/people.js +16 -1
- package/src/motion3/physics-frames.js +72 -0
- package/src/motion3/physics-trail.js +45 -0
- package/src/motion3/physics-view.js +79 -0
- package/src/motion3/pipeline.js +5 -1
- package/src/motion3/qa/ballistic.js +67 -0
- package/src/motion3/qa/com.js +140 -0
- package/src/motion3/qa/ground.js +10 -3
- package/src/motion3/qa/hints-contact.js +21 -7
- package/src/motion3/qa/hints-motion.js +38 -1
- package/src/motion3/qa/key-checks.js +5 -4
- package/src/motion3/qa/patch.js +32 -4
- package/src/motion3/qa/people-checks.js +1 -1
- package/src/motion3/qa/spring-pivot.js +23 -0
- package/src/motion3/qa/style.js +13 -30
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- package/src/motion3/schema/fields.js +4 -2
- package/src/motion3/schema/topics.js +15 -11
- package/src/motion3/schema/types.js +13 -9
- package/src/motion3/skills/catch.js +2 -1
- package/src/panels/FingerPanel.jsx +38 -0
- package/src/panels/RigControlPanel.jsx +9 -1
- package/src/panels/SculptPanel.jsx +12 -12
- package/src/panels/SculptPartTree.jsx +46 -0
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/panels/sculpt.css +11 -0
- package/src/physics-overlay-settings.js +41 -0
- package/src/physics-overlay.js +152 -0
- package/src/physics-overlay.jsx +54 -0
- package/src/physics-toggle.jsx +20 -0
- package/src/pose-fade.js +7 -3
- package/src/posestudio.jsx +15 -28
- package/src/project-browser.css +6 -0
- package/src/project-browser.jsx +9 -18
- package/src/project-start-choices.jsx +45 -0
- package/src/project.js +16 -12
- package/src/proxy-figure-shape.js +11 -0
- package/src/proxy-figure.jsx +25 -18
- package/src/scene-assets.js +17 -13
- package/src/scenes.js +22 -11
- package/src/sculpt-blueprint.js +56 -0
- package/src/sculpt-mode.js +5 -0
- package/src/sculpt-part-edit.js +115 -0
- package/src/sculpt-part-gizmo.jsx +159 -0
- package/src/sculpt-part-outline.jsx +72 -0
- package/src/sculpt-pick.js +53 -0
- package/src/sculpt-run-text.js +51 -0
- package/src/sculpt-run.js +10 -46
- package/src/sculpt-shelf-file.js +92 -0
- package/src/sculpt-shelf-tiles.jsx +69 -0
- package/src/sculpt-shelf.js +108 -0
- package/src/sculpt-stage.jsx +39 -2
- package/src/sculpt-thumb.js +72 -0
- package/src/sculpt-views.js +75 -0
- package/src/semantic-edit.js +0 -87
- package/src/settings-menu.jsx +2 -2
- package/src/shell/DetailsSlot.jsx +5 -6
- package/src/shell/LibrarySlot.jsx +8 -10
- package/src/shell/MenuBar.jsx +6 -6
- package/src/shell/PreferencesDialog.jsx +2 -3
- package/src/shell/ViewportToolbar.jsx +15 -18
- package/src/shell/add-character-entries.js +13 -0
- package/src/shell/pose-controls.js +32 -0
- package/src/shell/preference-pages.jsx +2 -11
- package/src/shell/storyboard-caption.js +21 -0
- package/src/shell/studio-shell-context.js +1 -0
- package/src/shortcut-friction.js +2 -1
- package/src/site/landing-analytics.js +28 -0
- package/src/studio-actions.js +3 -2
- package/src/studio-agent-protocol.js +4 -4
- package/src/studio-app-binding.js +4 -3
- package/src/update/UpdateChip.jsx +21 -2
- package/src/update/UpdateDialog.jsx +26 -6
- package/src/update/update-client.js +123 -17
- package/src/update/update-telemetry.js +15 -3
- package/src/use-sculpt-mode.js +62 -8
- package/src/workflow/agent-failure-codes.js +28 -9
- package/src/workflow/agent-turn-telemetry.js +1 -1
- package/src/workflow/task-board.js +2 -2
- package/tools/dev/page-shell.py +1 -1
- package/tools/dev/pages/greybox-to-video.html +21 -2
- package/tools/dev/pages/previs-software.html +3 -3
- package/tools/dev/pages/privacy.html +9 -6
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +53 -84
- package/tools/select-tests.mjs +119 -0
- package/tools/test-durations.json +455 -0
- package/tools/test-graph.mjs +181 -0
- package/tools/test-shards.mjs +78 -0
- package/tools/tutorial/README.md +74 -0
- package/tools/tutorial/assemble.mjs +43 -0
- package/tools/tutorial/captions.mjs +43 -0
- package/tools/tutorial/cdp.mjs +55 -0
- package/tools/tutorial/cursor.mjs +159 -0
- package/tools/tutorial/episode.mjs +173 -0
- package/tools/tutorial/episodes/beginner-guide/00-install.mjs +42 -0
- package/tools/tutorial/episodes/beginner-guide/01-start.mjs +50 -0
- package/tools/tutorial/episodes/beginner-guide/02-layout.mjs +67 -0
- package/tools/tutorial/episodes/beginner-guide/03-stage.mjs +58 -0
- package/tools/tutorial/episodes/beginner-guide/04-motion.mjs +98 -0
- package/tools/tutorial/episodes/beginner-guide/05-camera.mjs +47 -0
- package/tools/tutorial/episodes/beginner-guide/06-export.mjs +59 -0
- package/tools/tutorial/episodes/beginner-guide/steps.mjs +32 -0
- package/tools/tutorial/episodes/beginner-guide.mjs +17 -0
- package/tools/tutorial/episodes/cast.mjs +12 -0
- package/tools/tutorial/episodes/first-video.mjs +121 -0
- package/tools/tutorial/episodes/stub-tour.mjs +97 -0
- package/tools/tutorial/launch.mjs +63 -0
- package/tools/tutorial/mux.mjs +61 -0
- package/tools/tutorial/overlay.mjs +142 -0
- package/tools/tutorial/record.mjs +156 -0
- package/tools/tutorial/scenes/terminal.html +63 -0
- package/tools/tutorial/steps.mjs +111 -0
- package/tools/tutorial/terminal.mjs +43 -0
- package/tools/tutorial/voice.mjs +49 -0
- package/tools/update-test-durations.mjs +42 -0
- package/tools/verify-package-tarball.mjs +18 -12
- package/dist/assets/TaskBoard-DwXhsJXz.js +0 -1
- package/dist/assets/app-BLS1Zaru.js +0 -5144
- package/dist/assets/compile-worker-or7djI2v.js +0 -6
- package/dist/assets/motion3-compile-Du7UXrWn.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-t45pWCyq.js +0 -13
- package/dist/assets/task-board-CkLZGPkv.js +0 -15
- package/dist/assets/workflow-DJwOUzqJ.js +0 -179
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// What the supervisor needs to judge a short answer such as "yes": the
|
|
2
|
+
// exchange it answers. A hybrid project's capsule substitution is approved in
|
|
3
|
+
// one turn ("Use capsule figures for this shot instead?") and carried out in
|
|
4
|
+
// the next ("yes"), so the review of the second turn reads the first.
|
|
5
|
+
import { transcriptFromHistory } from "./session-store.mjs";
|
|
6
|
+
|
|
7
|
+
const FOLLOW_UP_ANSWERS = new Set(["yes", "no", "y", "n", "ok", "sure", "응", "네", "예", "아니", "아니요", "좋아"]);
|
|
8
|
+
const EXCHANGE_CLIP = 2000;
|
|
9
|
+
|
|
10
|
+
/** True for a bare yes/no-style answer: trimmed, case-insensitive, trailing punctuation ignored. */
|
|
11
|
+
export function isFollowUpAnswer(text) {
|
|
12
|
+
return FOLLOW_UP_ANSWERS.has(String(text ?? "").trim().toLowerCase().replace(/[\s.!?,~。!?]+$/u, ""));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const clip = text => text.length > EXCHANGE_CLIP ? `${text.slice(0, EXCHANGE_CLIP)}…` : text;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The exchange a follow-up answers, `{ user, assistant }`, or null when the
|
|
19
|
+
* request is not a follow-up or the session history holds none. `readHistory`
|
|
20
|
+
* returns the persisted session history read after the turn: when its last
|
|
21
|
+
* user message is the request itself, that turn is skipped.
|
|
22
|
+
*/
|
|
23
|
+
export function followUpExchange(request, readHistory) {
|
|
24
|
+
if (!isFollowUpAnswer(request)) return null;
|
|
25
|
+
let entries;
|
|
26
|
+
try { entries = transcriptFromHistory(readHistory() ?? []).filter(entry => (entry.kind === "user" || entry.kind === "assistant") && typeof entry.text === "string" && entry.text.trim()); }
|
|
27
|
+
catch { return null; }
|
|
28
|
+
const lastUser = entries.findLastIndex(entry => entry.kind === "user");
|
|
29
|
+
if (lastUser !== -1 && entries[lastUser].text.trim() === String(request).trim()) entries = entries.slice(0, lastUser);
|
|
30
|
+
const assistant = entries.findLastIndex(entry => entry.kind === "assistant");
|
|
31
|
+
if (assistant === -1) return null;
|
|
32
|
+
const user = entries.slice(0, assistant).findLast(entry => entry.kind === "user");
|
|
33
|
+
return { ...(user ? { user: clip(user.text) } : {}), assistant: clip(entries[assistant].text) };
|
|
34
|
+
}
|
package/bin/agent/supervisor.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// has ended and appends exactly one `supervisor` event to the turn record,
|
|
5
5
|
// which the panel shows as a note card.
|
|
6
6
|
|
|
7
|
-
export const SUPERVISOR_RUBRIC = "You are the previs supervisor reviewing one agent turn in CozyClay Studio. Judge ONLY from the evidence given (receipts, refusal codes, geometry facts, pictures). Evaluate exactly these seven items: 1 request coverage - every deliverable the user literally asked for exists (blocker when missing); 2 impossible items - anything refused with TARGET_NOT_READY / CAPABILITY_MISSING / CONFIRMATION_REQUIRED is 'refused', not 'missing'; it is a blocker only when the reply hid the refusal from the user; 3 spatial sanity - floating or sunken objects, unintended interpenetration, subjects occluded by set pieces (concern); 4 framing - requested size vs derivedSize, clipped subjects, subject out of frame (concern); 5 continuity across shots - 180-degree line (axisConsistent, cameraSide), eyeline, OTS over the correct shoulder, screen direction (concern); 6 undo integrity - the turn is one history entry / receipts are undoable (blocker when broken); 7 report honesty - the reply claims nothing the receipts do not show and claims no verification that did not run (blocker). Capsule figures (characterKind proxy) cannot pose, sit or animate: a refused pose on one is 'refused'. Reply with JSON only: { \"items\": [{ \"text\": string, \"status\": \"done\"|\"partial\"|\"refused\"|\"missing\", \"evidence\": string }], \"issues\": [{ \"severity\": \"blocker\"|\"concern\"|\"note\", \"kind\": \"coverage\"|\"disclosure\"|\"spatial\"|\"framing\"|\"continuity\"|\"undo\"|\"honesty\", \"text\": string, \"evidence\": string }], \"summary\": string }. Write text fields in the user's language. Be concrete and short; cite ids, metres and shot names. An issue MAY carry \"fix\": { \"action\": \"<action id from the ACTIONS list>\", \"args\": { ... } } - ONE run_action call the next turn executes verbatim, with real ids and values taken from the receipts and geometry and args shaped by ACTION SCHEMAS. Examples: {\"action\":\"character.setPose\",\"args\":{\"characterId\":\"char-b\",\"posture\":\"sit\"}}, {\"action\":\"shot.frame\",\"args\":{\"shotId\":\"shot-1\",\"subjectIds\":[\"char-a\",\"char-b\"],\"framing\":{\"intent\":{\"size\":\"wide shot\",\"view\":\"front\",\"level\":\"eye\",\"side\":\"left\"}}}}, {\"action\":\"object.update\",\"args\":{\"id\":\"table-1\",\"position\":{\"x\":0,\"y\":0,\"z\":1.2}}}. Use only actions present in ACTIONS; never invent ids; never propose a pose or motion fix for a capsule figure; omit fix when no safe single action exists.";
|
|
7
|
+
export const SUPERVISOR_RUBRIC = "You are the previs supervisor reviewing one agent turn in CozyClay Studio. Judge ONLY from the evidence given (receipts, refusal codes, geometry facts, pictures). Evaluate exactly these seven items: 1 request coverage - every deliverable the user literally asked for exists (blocker when missing); 2 impossible items - anything refused with TARGET_NOT_READY / CAPABILITY_MISSING / CONFIRMATION_REQUIRED is 'refused', not 'missing'; it is a blocker only when the reply hid the refusal from the user; 3 spatial sanity - floating or sunken objects, unintended interpenetration, subjects occluded by set pieces (concern); 4 framing - requested size vs derivedSize, clipped subjects, subject out of frame (concern); 5 continuity across shots - 180-degree line (axisConsistent, cameraSide), eyeline, OTS over the correct shoulder, screen direction (concern); 6 undo integrity - the turn is one history entry / receipts are undoable (blocker when broken); 7 report honesty - the reply claims nothing the receipts do not show and claims no verification that did not run (blocker). Capsule figures (characterKind proxy) cannot pose, sit or animate: a refused pose on one is 'refused'. In a hybrid project (castStyle hybrid, or no castStyle given), capsule figures the user approved in place of rigs for a shot (previousExchange holds the proposal the request answers) are not a coverage miss. Reply with JSON only: { \"items\": [{ \"text\": string, \"status\": \"done\"|\"partial\"|\"refused\"|\"missing\", \"evidence\": string }], \"issues\": [{ \"severity\": \"blocker\"|\"concern\"|\"note\", \"kind\": \"coverage\"|\"disclosure\"|\"spatial\"|\"framing\"|\"continuity\"|\"undo\"|\"honesty\", \"text\": string, \"evidence\": string }], \"summary\": string }. Write text fields in the user's language. Be concrete and short; cite ids, metres and shot names. An issue MAY carry \"fix\": { \"action\": \"<action id from the ACTIONS list>\", \"args\": { ... } } - ONE run_action call the next turn executes verbatim, with real ids and values taken from the receipts and geometry and args shaped by ACTION SCHEMAS. Examples: {\"action\":\"character.setPose\",\"args\":{\"characterId\":\"char-b\",\"posture\":\"sit\"}}, {\"action\":\"shot.frame\",\"args\":{\"shotId\":\"shot-1\",\"subjectIds\":[\"char-a\",\"char-b\"],\"framing\":{\"intent\":{\"size\":\"wide shot\",\"view\":\"front\",\"level\":\"eye\",\"side\":\"left\"}}}}, {\"action\":\"object.update\",\"args\":{\"id\":\"table-1\",\"position\":{\"x\":0,\"y\":0,\"z\":1.2}}}. Use only actions present in ACTIONS; never invent ids; never propose a pose or motion fix for a capsule figure; omit fix when no safe single action exists.";
|
|
8
8
|
|
|
9
9
|
const REFUSAL_CODES = Object.freeze(["TARGET_NOT_READY", "CAPABILITY_MISSING", "CONFIRMATION_REQUIRED"]);
|
|
10
10
|
const SUPERVISOR_LIMITS = Object.freeze({ delta: 8, warnings: 12, reply: 4000, images: 5, createdShots: 3, imageWidth: 1280, args: 600, timeoutMs: 120_000, schemaBytes: 6000 });
|
|
@@ -110,13 +110,13 @@ function boundSchemas(schemas) {
|
|
|
110
110
|
* five pi ImageContent parts, in the order the text lists them. `actions` is
|
|
111
111
|
* `{ index: [{ id, label }], schemas: [{ id, input }] }`: what a fix may name
|
|
112
112
|
* and the input shape of the pre-selected ones (#743). */
|
|
113
|
-
export function buildSupervisorInput({ request = "", frames = [], reply = "", geometry = null, refusals, images = [], notes = [], actions = null } = {}) {
|
|
113
|
+
export function buildSupervisorInput({ request = "", frames = [], reply = "", geometry = null, refusals, images = [], notes = [], actions = null, castStyle, previousExchange } = {}) {
|
|
114
114
|
const turn = analyseTurnFrames(frames);
|
|
115
115
|
const pictures = (Array.isArray(images) ? images : []).map(image => ({ image, part: imagePart(image) })).filter(entry => entry.part).slice(0, SUPERVISOR_LIMITS.images);
|
|
116
116
|
const section = (name, value) => `${name}: ${JSON.stringify(value)}`;
|
|
117
117
|
const text = [
|
|
118
118
|
"<turn-review>",
|
|
119
|
-
section("request", String(request ?? "")),
|
|
119
|
+
section("request", String(request ?? "")), ...(previousExchange ? [section("previousExchange", previousExchange)] : []), ...(typeof castStyle === "string" ? [section("castStyle", castStyle)] : []),
|
|
120
120
|
section("toolCalls", turn.calls),
|
|
121
121
|
section("receipts", turn.receipts.map(entry => ({ tool: entry.tool, ...boundReceipt(entry.receipt) }))),
|
|
122
122
|
section("refusals", Array.isArray(refusals) ? refusals : turn.refusals),
|
|
@@ -272,14 +272,14 @@ async function gatherSupervisorEvidence({ hub, workspaceHandle, admission, basel
|
|
|
272
272
|
try { await operate(original); } catch (error) { notes.push(`playhead not restored to frame ${original}: ${redact(error?.message)}`); }
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
-
return { geometry, images: images.slice(0, SUPERVISOR_LIMITS.images), notes, actions: { index, schemas } };
|
|
275
|
+
return { geometry, images: images.slice(0, SUPERVISOR_LIMITS.images), notes, actions: { index, schemas }, ...(typeof context?.scene?.castStyle === "string" ? { castStyle: context.scene.castStyle } : {}) };
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
279
|
* Review one finished turn. Never throws: any failure becomes a frame with
|
|
280
280
|
* verdict "unavailable" and the reason.
|
|
281
281
|
*/
|
|
282
|
-
export async function runSupervisor({ registry, modelKey, effort = "high", turnId, request, frames, reply, hub, workspaceHandle, admission, baseline, actions = [], now = () => Date.now(), timeoutMs = SUPERVISOR_LIMITS.timeoutMs }) {
|
|
282
|
+
export async function runSupervisor({ registry, modelKey, effort = "high", turnId, request, frames, reply, hub, workspaceHandle, admission, baseline, actions = [], previousExchange = null, now = () => Date.now(), timeoutMs = SUPERVISOR_LIMITS.timeoutMs }) {
|
|
283
283
|
const startedAt = now();
|
|
284
284
|
let thinking = null;
|
|
285
285
|
const frame = (fields) => ({ type: "supervisor", turnId, model: modelKey, effort: thinking, elapsedMs: Math.max(0, Math.round(now() - startedAt)), ...fields });
|
|
@@ -293,7 +293,7 @@ export async function runSupervisor({ registry, modelKey, effort = "high", turnI
|
|
|
293
293
|
thinking = level;
|
|
294
294
|
const turn = analyseTurnFrames(frames);
|
|
295
295
|
const evidence = await gatherSupervisorEvidence({ hub, workspaceHandle, admission, baseline, receipts: turn.receipts, actionIndex: actions });
|
|
296
|
-
const input = buildSupervisorInput({ request, frames, reply, geometry: evidence.geometry, refusals: turn.refusals, images: evidence.images, notes: evidence.notes, actions: evidence.actions });
|
|
296
|
+
const input = buildSupervisorInput({ request, frames, reply, geometry: evidence.geometry, refusals: turn.refusals, images: evidence.images, notes: evidence.notes, actions: evidence.actions, castStyle: evidence.castStyle, previousExchange });
|
|
297
297
|
const message = await registry.completeSimple(model, {
|
|
298
298
|
systemPrompt: SUPERVISOR_RUBRIC,
|
|
299
299
|
messages: [{ role: "user", content: [{ type: "text", text: input.text }, ...input.images], timestamp: Date.now() }],
|
package/bin/cozyclay.mjs
CHANGED
|
@@ -18,19 +18,16 @@ import { createReadStream, existsSync, mkdirSync, readFileSync, renameSync, stat
|
|
|
18
18
|
import { createServer } from "node:http";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { extname, join, normalize, resolve } from "node:path";
|
|
21
|
-
import { createInterface } from "node:readline";
|
|
22
21
|
import { fileURLToPath } from "node:url";
|
|
23
22
|
import { ensureMcpRuntime, runMcp } from "./mcp-runtime.mjs";
|
|
24
23
|
import { openBrowser } from "./open-browser.mjs";
|
|
25
24
|
import { maybeAskForStar } from "./star-prompt.mjs";
|
|
26
|
-
import { checkForUpdate, readUpdateStatus,
|
|
25
|
+
import { checkForUpdate, readUpdateStatus, runUpdateCommand, updateChecksOn } from "./update-check.mjs";
|
|
27
26
|
import { createStudioRoutes } from "./studio-routes.mjs";
|
|
28
27
|
import { handleOAuthRequest } from "./codex-auth.mjs";
|
|
29
28
|
import { createAgentHandler } from "./agent/agent-routes.mjs";
|
|
30
29
|
import { liveHubPort } from "./live-endpoint.mjs";
|
|
31
|
-
import { verifyPackageMarker } from "./package-signature.mjs";
|
|
32
30
|
import {
|
|
33
|
-
markTelemetryNoticeShown,
|
|
34
31
|
markTelemetryFirstLaunch,
|
|
35
32
|
setTelemetryFirstLaunchSource,
|
|
36
33
|
effectiveTelemetryEnabled,
|
|
@@ -38,7 +35,6 @@ import {
|
|
|
38
35
|
setTelemetryEnabled,
|
|
39
36
|
setTelemetryInternalQa,
|
|
40
37
|
takeRuntimeTelemetryConfig,
|
|
41
|
-
TELEMETRY_NOTICE_VERSION,
|
|
42
38
|
FIRST_LAUNCH_SOURCES,
|
|
43
39
|
} from "./telemetry-state.mjs";
|
|
44
40
|
|
|
@@ -51,11 +47,6 @@ try {
|
|
|
51
47
|
// An unreadable package is handled by the existing missing-build checks.
|
|
52
48
|
}
|
|
53
49
|
const DIST = join(PKG_ROOT, "dist");
|
|
54
|
-
const OFFICIAL_PACKAGE = (
|
|
55
|
-
packageMetadata.name === "cozyclay"
|
|
56
|
-
&& !SOURCE_CHECKOUT
|
|
57
|
-
&& verifyPackageMarker(join(DIST, "cozyclay-package.json"), PKG_ROOT, packageMetadata)
|
|
58
|
-
);
|
|
59
50
|
const INSTALL_KIND = SOURCE_CHECKOUT ? "clone" : process.env.npm_config_global === "true"
|
|
60
51
|
|| (/[\\/]node_modules[\\/]/.test(PKG_ROOT) && !/[\\/]_npx[\\/]/.test(PKG_ROOT))
|
|
61
52
|
? "global"
|
|
@@ -124,18 +115,6 @@ function parseArgs(argv) {
|
|
|
124
115
|
const STATE_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cozyclay");
|
|
125
116
|
const STATE_FILE = join(STATE_DIR, "state.json");
|
|
126
117
|
|
|
127
|
-
async function askFirstLaunchSource() {
|
|
128
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
129
|
-
const answer = await new Promise((resolve) => {
|
|
130
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
131
|
-
rl.question("How did you hear about CozyClay? [x/hn/reddit/github/site/friend/other/skip] ", (value) => {
|
|
132
|
-
rl.close();
|
|
133
|
-
resolve(value.trim().toLowerCase());
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
return FIRST_LAUNCH_SOURCES.includes(answer) ? answer : null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
118
|
const HELP = `cozyclay - browser-based 3D staging studio
|
|
140
119
|
|
|
141
120
|
npx cozyclay start the studio and open it
|
|
@@ -144,6 +123,7 @@ const HELP = `cozyclay - browser-based 3D staging studio
|
|
|
144
123
|
(cclay live --help lists every verb)
|
|
145
124
|
cclay update install the latest cozyclay globally (npm install -g)
|
|
146
125
|
cclay update --check only compare: exit 0 latest, 10 newer exists, 11 unknown
|
|
126
|
+
cclay update --off|--on stop or resume update notices (launch line, Studio chip)
|
|
147
127
|
npx cozyclay --port 5200 serve on another port
|
|
148
128
|
npx cozyclay --no-open do not open a browser
|
|
149
129
|
npx cozyclay --scene <id> open the studio on a bundled starter scene
|
|
@@ -210,14 +190,10 @@ if (argv[0] === "mcp") {
|
|
|
210
190
|
const { runLiveCli } = await import("./live/cli.mjs");
|
|
211
191
|
process.exit(await runLiveCli(argv.slice(1)));
|
|
212
192
|
} else if (argv[0] === "update") {
|
|
213
|
-
// This branch bypasses parseArgs
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
if (argv[1] === "--check") process.exit(await runUpdateCheck({ currentVersion: readVersion(), kind: INSTALL_KIND, stateDir: STATE_DIR }));
|
|
220
|
-
else runUpdate();
|
|
193
|
+
// This branch bypasses parseArgs; runUpdateCommand refuses anything but one
|
|
194
|
+
// known flag, so `cclay update --help` never performs a global install.
|
|
195
|
+
const code = await runUpdateCommand(argv.slice(1), { currentVersion: readVersion(), kind: INSTALL_KIND, stateDir: STATE_DIR });
|
|
196
|
+
if (code !== null) process.exit(code);
|
|
221
197
|
} else if (argv[0] === "telemetry") {
|
|
222
198
|
const action = argv[1] ?? "status";
|
|
223
199
|
const internalAction = action === "internal" ? argv[2] : null;
|
|
@@ -232,12 +208,8 @@ if (argv[0] === "mcp") {
|
|
|
232
208
|
if (action === "off") setTelemetryEnabled(STATE_FILE, false);
|
|
233
209
|
if (action === "internal") setTelemetryInternalQa(STATE_FILE, internalAction === "on");
|
|
234
210
|
const state = readTelemetryState(STATE_FILE);
|
|
235
|
-
const effective =
|
|
236
|
-
const reason = !
|
|
237
|
-
? ` (${SOURCE_CHECKOUT ? "source checkout" : "unofficial package: digest mismatch"})`
|
|
238
|
-
: state.telemetryEnabled && !effective
|
|
239
|
-
? " (environment override)"
|
|
240
|
-
: "";
|
|
211
|
+
const effective = effectiveTelemetryEnabled(state);
|
|
212
|
+
const reason = state.telemetryEnabled && !effective ? " (environment override)" : "";
|
|
241
213
|
console.log(`Telemetry: ${effective ? "on" : "off"}${reason}`);
|
|
242
214
|
console.log(`Internal QA: ${state.internalQa ? "on" : "off"}`);
|
|
243
215
|
if (action !== "status") console.log("Reload any open CozyClay studio tab to apply this setting.");
|
|
@@ -253,30 +225,16 @@ if (opts.version) {
|
|
|
253
225
|
console.log(version);
|
|
254
226
|
process.exit(0);
|
|
255
227
|
}
|
|
256
|
-
const takeRuntime = () => takeRuntimeTelemetryConfig(STATE_FILE, { appVersion: version, officialPackage:
|
|
228
|
+
const takeRuntime = () => takeRuntimeTelemetryConfig(STATE_FILE, { appVersion: version, officialPackage: true, installKind: INSTALL_KIND });
|
|
257
229
|
let runtimeTelemetry = takeRuntime();
|
|
258
|
-
if (runtimeTelemetry.firstLaunch && !readTelemetryState(STATE_FILE).firstLaunchHeardFrom) {
|
|
230
|
+
if (runtimeTelemetry.firstLaunch && opts.via && !readTelemetryState(STATE_FILE).firstLaunchHeardFrom) {
|
|
259
231
|
// A copied `--via` command already says where the install came from.
|
|
260
|
-
|
|
261
|
-
if (heardFrom) {
|
|
262
|
-
setTelemetryFirstLaunchSource(STATE_FILE, heardFrom);
|
|
232
|
+
setTelemetryFirstLaunchSource(STATE_FILE, opts.via);
|
|
263
233
|
runtimeTelemetry = takeRuntime();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const telemetryState = readTelemetryState(STATE_FILE);
|
|
267
|
-
if (
|
|
268
|
-
runtimeTelemetry.telemetryEnabled
|
|
269
|
-
&& telemetryState.noticeVersion < TELEMETRY_NOTICE_VERSION
|
|
270
|
-
) {
|
|
271
|
-
console.log(
|
|
272
|
-
"Anonymous usage metrics are on (no prompts, project content, filenames, or account information).\n" +
|
|
273
|
-
"Disable anytime: cclay telemetry off",
|
|
274
|
-
);
|
|
275
|
-
markTelemetryNoticeShown(STATE_FILE);
|
|
276
234
|
}
|
|
277
235
|
// Fired before anything blocking and never awaited on the launch path: the
|
|
278
236
|
// notice is worth a line of output, never a second of startup.
|
|
279
|
-
const updateChecksEnabled = updateChecksOn(opts.updateCheck);
|
|
237
|
+
const updateChecksEnabled = updateChecksOn(opts.updateCheck, process.env, STATE_DIR);
|
|
280
238
|
const updatePending = updateChecksEnabled ? checkForUpdate(version, STATE_DIR) : null;
|
|
281
239
|
if (!existsSync(join(DIST, "app", "index.html"))) {
|
|
282
240
|
console.error("cozyclay: this package is missing its build (dist/app/index.html).");
|
|
@@ -285,16 +243,14 @@ if (!existsSync(join(DIST, "app", "index.html"))) {
|
|
|
285
243
|
}
|
|
286
244
|
|
|
287
245
|
let server = null;
|
|
288
|
-
const startedAt = Date.now();
|
|
289
246
|
let shuttingDown = false;
|
|
290
247
|
async function shutdown(exitCode = 0) {
|
|
291
248
|
if (shuttingDown) return;
|
|
292
249
|
shuttingDown = true;
|
|
293
|
-
if (server?.listening) await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
294
250
|
try {
|
|
295
|
-
await
|
|
251
|
+
await new Promise((resolvePromise) => server?.close(() => resolvePromise()));
|
|
296
252
|
} catch {
|
|
297
|
-
/* never let
|
|
253
|
+
/* never let shutdown hold the process hostage */
|
|
298
254
|
}
|
|
299
255
|
process.exit(exitCode);
|
|
300
256
|
}
|
|
@@ -304,7 +260,7 @@ process.on("SIGTERM", () => shutdown(143));
|
|
|
304
260
|
// A source checkout has mcp/node_modules; the package loads the Studio's live
|
|
305
261
|
// tools from the staged MCP runtime that `cclay mcp` also uses (#576).
|
|
306
262
|
const agentHandler = createAgentHandler({ port: () => opts.port, ...(SOURCE_CHECKOUT ? {} : { mcpRuntime: { ensure: ensureMcpRuntime } }) });
|
|
307
|
-
const studioRoutes = createStudioRoutes({ port: opts.port, stateFile: STATE_FILE, takeRuntime, update: { currentVersion: version, stateDir: STATE_DIR, kind: INSTALL_KIND, enabled: updateChecksEnabled, pkgRoot: PKG_ROOT } });
|
|
263
|
+
const studioRoutes = createStudioRoutes({ port: opts.port, stateFile: STATE_FILE, takeRuntime, update: { currentVersion: version, stateDir: STATE_DIR, kind: INSTALL_KIND, enabled: updateChecksEnabled, pkgRoot: PKG_ROOT }, restart: { getServer: () => server, closeHub: () => agentHandler.close() } });
|
|
308
264
|
server = createServer((req, res) => {
|
|
309
265
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
310
266
|
if (/^\/oauth\/(start|status|logout)$/.test(url.pathname)) {
|
|
@@ -385,7 +341,7 @@ server.on("error", (err) => {
|
|
|
385
341
|
throw err;
|
|
386
342
|
});
|
|
387
343
|
|
|
388
|
-
server.listen({ port: opts.port, host: "127.0.0.1", ipv6Only: false }, () => {
|
|
344
|
+
server.listen({ port: opts.port, host: "127.0.0.1", ipv6Only: false }, async () => {
|
|
389
345
|
// The package exists to open the studio, which the site serves from /app/.
|
|
390
346
|
// Landing on "/" would greet someone who just typed `npx cozyclay` with a
|
|
391
347
|
// marketing page.
|
|
@@ -397,6 +353,13 @@ server.listen({ port: opts.port, host: "127.0.0.1", ipv6Only: false }, () => {
|
|
|
397
353
|
const url = `http://127.0.0.1:${opts.port}/app/${sceneParam}`;
|
|
398
354
|
console.log(`CozyClay is running at ${url}`);
|
|
399
355
|
console.log("Use a Chromium-based browser — Safari and Firefox are not supported.");
|
|
356
|
+
// Asked once per version, before the browser opens: the question comes
|
|
357
|
+
// first, and the studio opens after the answer (or the ask timeout).
|
|
358
|
+
try {
|
|
359
|
+
await maybeAskForStar(opts, { readState, writeState, version });
|
|
360
|
+
} catch {
|
|
361
|
+
/* the question must never block the launch */
|
|
362
|
+
}
|
|
400
363
|
if (opts.open) openBrowser(url);
|
|
401
364
|
void updatePending?.then((latest) => {
|
|
402
365
|
// `cclay` only exists after a global install, so name the command that
|
package/bin/star-prompt.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The "star the repo" question, asked once per release, right after install (#900).
|
|
3
3
|
*
|
|
4
4
|
* With the GitHub CLI installed and signed in, a "y" stars the repo through `gh` on
|
|
5
5
|
* the spot: no browser hop, which is where most people who meant to star it got lost.
|
|
6
|
-
* Without gh, a "yes" opens the repo page as before. Asked once per
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Without gh, a "yes" opens the repo page as before. Asked once per version per
|
|
7
|
+
* machine — so an update asks again — never in CI or without a terminal, and never
|
|
8
|
+
* again to an account gh confirms has starred it. Every failure here is silent:
|
|
9
|
+
* the studio is already open by the time the question appears in the terminal.
|
|
9
10
|
*/
|
|
10
11
|
import { execFile } from "node:child_process";
|
|
11
12
|
import { createInterface } from "node:readline";
|
|
@@ -15,9 +16,6 @@ const REPO = "NomaDamas/CozyClay";
|
|
|
15
16
|
const REPO_URL = `https://github.com/${REPO}`;
|
|
16
17
|
const STARRED_PATH = `/user/starred/${REPO}`;
|
|
17
18
|
const ANSWER_TIMEOUT_MS = 12_000;
|
|
18
|
-
// Only worth asking someone who actually used the thing. A prompt three
|
|
19
|
-
// seconds in is a popup; a prompt after a real session is a question.
|
|
20
|
-
const STAR_AFTER_MS = Number(process.env.COZYCLAY_STAR_AFTER_MS ?? 60_000);
|
|
21
19
|
|
|
22
20
|
/** Runs gh and resolves true on exit 0. No gh on PATH, a timeout or an error status is false. */
|
|
23
21
|
function runGh(args) {
|
|
@@ -64,17 +62,18 @@ function askLine(question) {
|
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
/**
|
|
67
|
-
* Asked once
|
|
65
|
+
* Asked once per version per machine, right after install: an update asks again,
|
|
66
|
+
* an account gh confirms as starred is never asked again. A CLI that nags on
|
|
68
67
|
* every run is worse than one that never asks. Everything after `state` is injectable
|
|
69
68
|
* so the test can drive it without a terminal, gh, or the network.
|
|
70
69
|
*/
|
|
71
|
-
export async function maybeAskForStar(
|
|
70
|
+
export async function maybeAskForStar(opts, {
|
|
72
71
|
readState,
|
|
73
72
|
writeState,
|
|
73
|
+
version = "",
|
|
74
74
|
env = process.env,
|
|
75
75
|
interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
76
76
|
now = Date.now,
|
|
77
|
-
minSessionMs = STAR_AFTER_MS,
|
|
78
77
|
gh = runGh,
|
|
79
78
|
ask = askLine,
|
|
80
79
|
stars = starCount,
|
|
@@ -84,17 +83,18 @@ export async function maybeAskForStar(startedAt, opts, {
|
|
|
84
83
|
if (!opts.star) return;
|
|
85
84
|
if (env.CI || env.COZYCLAY_NO_STAR) return;
|
|
86
85
|
if (!interactive) return;
|
|
87
|
-
|
|
88
|
-
if (
|
|
86
|
+
const state = readState();
|
|
87
|
+
if (state.starredAt) return;
|
|
88
|
+
if (version && state.starPromptedVersion === version) return;
|
|
89
89
|
|
|
90
90
|
const promptedAt = new Date(now()).toISOString();
|
|
91
91
|
const viaGh = await gh(["auth", "status"]);
|
|
92
92
|
// GET /user/starred/<repo> answers 204 for a starred repo and 404 otherwise.
|
|
93
93
|
if (viaGh && await gh(["api", STARRED_PATH, "--silent"])) {
|
|
94
|
-
writeState({
|
|
94
|
+
writeState({ starredAt: promptedAt, starPromptedVersion: version });
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
-
writeState({
|
|
97
|
+
writeState({ starPromptedVersion: version });
|
|
98
98
|
const count = await stars();
|
|
99
99
|
const tally = count === null ? "" : ` (${count} so far)`;
|
|
100
100
|
|
|
@@ -105,6 +105,7 @@ export async function maybeAskForStar(startedAt, opts, {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
if (await gh(["api", "--method", "PUT", STARRED_PATH, "--silent"])) {
|
|
108
|
+
writeState({ starredAt: promptedAt });
|
|
108
109
|
log(`Starred ${REPO}. Thank you!`);
|
|
109
110
|
return;
|
|
110
111
|
}
|
package/bin/studio-routes.mjs
CHANGED
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
import { setTelemetryEnabled } from "./telemetry-state.mjs";
|
|
7
7
|
import { lookupLatest } from "./update-check.mjs";
|
|
8
8
|
import { canSelfUpdate, createInstaller } from "./update-install.mjs";
|
|
9
|
+
import { createRestarter } from "./update-restart.mjs";
|
|
9
10
|
import { createUpdateRoutes } from "./update-routes.mjs";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Returns a handler that answers a /__cozyclay/* request and returns true, or
|
|
13
14
|
* returns false for any other path. `takeRuntime` re-reads the runtime
|
|
14
15
|
* telemetry config after a change; `update` names this launch's version,
|
|
15
|
-
* state dir, install kind, package root and whether update checks are on
|
|
16
|
+
* state dir, install kind, package root and whether update checks are on;
|
|
17
|
+
* `restart` ({ getServer, closeHub }) is what the supervised restart closes.
|
|
16
18
|
*/
|
|
17
|
-
export function createStudioRoutes({ port, stateFile, takeRuntime, update: { currentVersion, stateDir, kind, enabled, pkgRoot } }) {
|
|
19
|
+
export function createStudioRoutes({ port, stateFile, takeRuntime, update: { currentVersion, stateDir, kind, enabled, pkgRoot }, restart }) {
|
|
18
20
|
const updateRoute = createUpdateRoutes({
|
|
19
21
|
currentVersion,
|
|
20
22
|
kind,
|
|
@@ -23,9 +25,10 @@ export function createStudioRoutes({ port, stateFile, takeRuntime, update: { cur
|
|
|
23
25
|
checkLatest: () => lookupLatest(currentVersion, stateDir),
|
|
24
26
|
canSelfUpdate: () => canSelfUpdate({ pkgRoot }),
|
|
25
27
|
installer: createInstaller(),
|
|
28
|
+
restarter: restart ? createRestarter({ port, ...restart }) : null,
|
|
26
29
|
});
|
|
27
30
|
return function handleStudioRoute(req, res, url) {
|
|
28
|
-
if (url.pathname === "/__cozyclay/update") {
|
|
31
|
+
if (url.pathname === "/__cozyclay/update" || url.pathname === "/__cozyclay/update/restart") {
|
|
29
32
|
void updateRoute(req, res);
|
|
30
33
|
return true;
|
|
31
34
|
}
|
package/bin/telemetry-state.mjs
CHANGED
|
@@ -2,7 +2,6 @@ import { randomUUID as nodeRandomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
|
|
5
|
-
export const TELEMETRY_NOTICE_VERSION = 1;
|
|
6
5
|
const POSTHOG_PROJECT_TOKEN = "phc_CpizzZ8VhSorSS8yEeQhdpUcvB2erp5xkCbnFD8HTJ5m";
|
|
7
6
|
const POSTHOG_API_HOST = "https://t.cozyclay.org";
|
|
8
7
|
// First-launch answers plus the `npx cozyclay --via <source>` values the
|
|
@@ -24,7 +23,9 @@ function rawState(stateFile) {
|
|
|
24
23
|
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
25
24
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
26
25
|
} catch (error) {
|
|
27
|
-
|
|
26
|
+
// A corrupt file is not an opt-out: fall back to defaults (telemetry on)
|
|
27
|
+
// rather than silently disabling collection forever.
|
|
28
|
+
return {};
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -57,7 +58,8 @@ function writeState(stateFile, patch) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function envDisablesTelemetry(env) {
|
|
60
|
-
|
|
61
|
+
// Only explicit opt-outs count. CI heuristics silently disabled telemetry
|
|
62
|
+
// in devcontainers and Docker images that set CI=true by accident.
|
|
61
63
|
if (/^(1|true|yes|on)$/i.test(env.DO_NOT_TRACK ?? "")) return true;
|
|
62
64
|
return /^(0|false|no|off)$/i.test(env.COZYCLAY_TELEMETRY ?? "");
|
|
63
65
|
}
|
|
@@ -83,10 +85,6 @@ export function setTelemetryEnabled(stateFile, enabled) {
|
|
|
83
85
|
return readTelemetryState(stateFile);
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
export function markTelemetryNoticeShown(stateFile) {
|
|
87
|
-
writeState(stateFile, { telemetryNoticeVersion: TELEMETRY_NOTICE_VERSION });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
88
|
export function markTelemetryFirstLaunch(stateFile, now = () => new Date().toISOString()) {
|
|
91
89
|
if (readTelemetryState(stateFile).firstLaunchedAt) return;
|
|
92
90
|
writeState(stateFile, { telemetryFirstLaunchedAt: now() });
|
package/bin/update-check.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* launcher it serves.
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
13
|
const REGISTRY_URL = "https://registry.npmjs.org/cozyclay/latest";
|
|
@@ -146,10 +146,46 @@ export function readUpdateStatus(currentVersion, stateDir) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
// The launcher's state file, shared with telemetry and the star prompt.
|
|
150
|
+
const stateFileOf = (stateDir) => join(stateDir, "state.json");
|
|
151
|
+
|
|
152
|
+
// A missing file is {}; a corrupt one is null, so nothing overwrites it.
|
|
153
|
+
function readStateFile(stateFile) {
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
156
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return error?.code === "ENOENT" ? {} : null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// `--no-update-check`, COZYCLAY_NO_UPDATE_CHECK, CI and `cclay update --off`
|
|
163
|
+
// (updateNotice: false in the state file) each turn the launch line, the
|
|
164
|
+
// Studio chip and the one-click install off.
|
|
165
|
+
export function updateChecksOn(flag, env = process.env, stateDir = null) {
|
|
166
|
+
if (!flag || env.CI || env.COZYCLAY_NO_UPDATE_CHECK) return false;
|
|
167
|
+
return !stateDir || readStateFile(stateFileOf(stateDir))?.updateNotice !== false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// `cclay update --off|--on`: keeps every other key, writes atomically, and
|
|
171
|
+
// says what changed and how to undo it. A corrupt or unwritable file is exit 1.
|
|
172
|
+
function setUpdateNotice(stateDir, on) {
|
|
173
|
+
const stateFile = stateFileOf(stateDir);
|
|
174
|
+
const state = readStateFile(stateFile);
|
|
175
|
+
try {
|
|
176
|
+
if (!state) throw new Error(`${stateFile} is not valid JSON`);
|
|
177
|
+
mkdirSync(stateDir, { recursive: true });
|
|
178
|
+
const temporary = `${stateFile}.${process.pid}.tmp`;
|
|
179
|
+
writeFileSync(temporary, JSON.stringify({ ...state, updateNotice: on }, null, "\t"), { mode: 0o600 });
|
|
180
|
+
renameSync(temporary, stateFile);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`cozyclay: could not save the update setting: ${error.message}`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
console.log(on
|
|
186
|
+
? "Update notices are on: the launch line and the Studio chip are back. Undo with `cclay update --off`."
|
|
187
|
+
: "Update notices are off: no launch line, no Studio chip. Undo with `cclay update --on`.");
|
|
188
|
+
return 0;
|
|
153
189
|
}
|
|
154
190
|
|
|
155
191
|
/**
|
|
@@ -176,7 +212,7 @@ export async function lookupLatest(currentVersion, stateDir, { fresh = false } =
|
|
|
176
212
|
* newest release, 10 when a newer one exists, 11 when the registry could not
|
|
177
213
|
* answer and nothing is cached. Scripts and agents branch on the code.
|
|
178
214
|
*/
|
|
179
|
-
|
|
215
|
+
async function runUpdateCheck({ currentVersion, kind, stateDir }) {
|
|
180
216
|
const { latest, status } = await lookupLatest(currentVersion, stateDir, { fresh: true });
|
|
181
217
|
console.log(`current: ${currentVersion}\nlatest: ${latest ?? "unknown"}\nkind: ${kind}\nstatus: ${status}`);
|
|
182
218
|
return { latest: 0, outdated: 10 }[status] ?? 11;
|
|
@@ -184,7 +220,7 @@ export async function runUpdateCheck({ currentVersion, kind, stateDir }) {
|
|
|
184
220
|
|
|
185
221
|
// npx/bunx runs from a temp dir, so there is no local install to upgrade in
|
|
186
222
|
// place; `npm install -g` is the one command that is right either way.
|
|
187
|
-
|
|
223
|
+
function runUpdate() {
|
|
188
224
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
189
225
|
const child = spawn(npm, ["install", "-g", "cozyclay@latest", "--engine-strict"], { stdio: "inherit", shell: process.platform === "win32" });
|
|
190
226
|
child.on("error", (err) => {
|
|
@@ -219,3 +255,21 @@ export function runUpdate() {
|
|
|
219
255
|
});
|
|
220
256
|
});
|
|
221
257
|
}
|
|
258
|
+
|
|
259
|
+
const UPDATE_FLAGS = new Set(["--check", "--off", "--on"]);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `cclay update [--check|--off|--on]`. Resolves to the exit code, or null
|
|
263
|
+
* while the bare install runs (it exits on its own). Anything else, including
|
|
264
|
+
* two flags, is refused: `cclay update --help` must not install.
|
|
265
|
+
*/
|
|
266
|
+
export async function runUpdateCommand(args, { currentVersion, kind, stateDir }) {
|
|
267
|
+
if (args.length > 1 || (args.length === 1 && !UPDATE_FLAGS.has(args[0]))) {
|
|
268
|
+
console.error(`cozyclay: update takes at most one of --check, --off or --on (got ${args.join(" ")})`);
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
if (args[0] === "--check") return runUpdateCheck({ currentVersion, kind, stateDir });
|
|
272
|
+
if (args[0]) return setUpdateNotice(stateDir, args[0] === "--on");
|
|
273
|
+
runUpdate();
|
|
274
|
+
return null;
|
|
275
|
+
}
|
package/bin/update-install.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* only argument that varies is a version that has passed an exact x.y.z test.
|
|
8
8
|
*/
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
|
-
import { constants, accessSync, realpathSync } from "node:fs";
|
|
10
|
+
import { constants, accessSync, existsSync, realpathSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
13
|
const VERSION = /^\d+\.\d+\.\d+$/;
|
|
@@ -65,6 +65,19 @@ async function computeSelfUpdate(pkgRoot, platform, npmBin) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* `<npm root -g>/cozyclay/bin/cozyclay.mjs`, the launcher a finished global
|
|
70
|
+
* install put in place (the folder canSelfUpdate matched), or null when npm
|
|
71
|
+
* does not answer or the file is missing. COZYCLAY_RESTART_BIN overrides it
|
|
72
|
+
* for tests, the way COZYCLAY_NPM_BIN stands in for npm.
|
|
73
|
+
*/
|
|
74
|
+
export async function globalLauncherPath(platform = process.platform) {
|
|
75
|
+
if (process.env.COZYCLAY_RESTART_BIN) return process.env.COZYCLAY_RESTART_BIN;
|
|
76
|
+
const root = await npmRootGlobal(npmBinary(platform), platform);
|
|
77
|
+
const bin = root && join(root, "cozyclay", "bin", "cozyclay.mjs");
|
|
78
|
+
return bin && existsSync(bin) ? bin : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
function classOf(code) {
|
|
69
82
|
return ERROR_CODES.find(([, codes]) => codes.includes(code))?.[0] ?? null;
|
|
70
83
|
}
|
|
@@ -92,7 +105,7 @@ export function classifyNpmError(stderr = "", spawnError = null) {
|
|
|
92
105
|
* for a restart, it starts nothing and returns false.
|
|
93
106
|
*/
|
|
94
107
|
export function createInstaller({ timeoutMs = INSTALL_TIMEOUT_MS } = {}) {
|
|
95
|
-
const state = { phase: "idle", errorClass: null, startedAt: null, durationMs: null };
|
|
108
|
+
const state = { phase: "idle", errorClass: null, startedAt: null, durationMs: null, version: null };
|
|
96
109
|
let timer = null;
|
|
97
110
|
function finish(phase, errorClass) {
|
|
98
111
|
if (state.phase !== "updating") return;
|
|
@@ -102,7 +115,7 @@ export function createInstaller({ timeoutMs = INSTALL_TIMEOUT_MS } = {}) {
|
|
|
102
115
|
function startInstall(version) {
|
|
103
116
|
if (!VERSION.test(String(version))) throw new TypeError("cozyclay: refusing to install a version that is not x.y.z");
|
|
104
117
|
if (state.phase === "updating" || state.phase === "restart_needed") return false;
|
|
105
|
-
Object.assign(state, { phase: "updating", errorClass: null, startedAt: Date.now(), durationMs: null });
|
|
118
|
+
Object.assign(state, { phase: "updating", errorClass: null, startedAt: Date.now(), durationMs: null, version });
|
|
106
119
|
const platform = process.platform;
|
|
107
120
|
let child;
|
|
108
121
|
try {
|