cozyclay 3.1.3 → 3.1.5
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 +98 -0
- package/INSTALL_FOR_AGENTS.md +3 -0
- package/README.md +5 -0
- package/bin/agent/agent-failure-codes.mjs +31 -0
- package/bin/agent/agent-routes.mjs +5 -10
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/cozyclay.mjs +18 -112
- package/bin/star-prompt.mjs +123 -0
- package/bin/studio-routes.mjs +69 -0
- package/bin/update-check.mjs +53 -5
- package/bin/update-install.mjs +136 -0
- package/bin/update-routes.mjs +80 -0
- package/dist/ai-camera-control/index.html +1 -1
- package/dist/app/index.html +5 -6
- package/dist/assets/{TaskBoard-U1Yl-4F1.js → TaskBoard-DwXhsJXz.js} +1 -1
- package/dist/assets/app-CEP7jzXB.css +1 -0
- package/dist/assets/app-CTKcyPM-.js +5144 -0
- package/dist/assets/compile-worker-or7djI2v.js +6 -0
- package/dist/assets/{motion3-compile-DDG3wklD.js → motion3-compile-Du7UXrWn.js} +1 -1
- package/dist/assets/scene-asset-sync-t45pWCyq.js +13 -0
- package/dist/assets/task-board-CkLZGPkv.js +15 -0
- package/dist/assets/workflow-DJwOUzqJ.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +3 -3
- package/dist/index.html +533 -264
- package/dist/media/cozyclay-mark.png +0 -0
- package/dist/media/cozyclay-truck-demo-poster.jpg +0 -0
- package/dist/media/step-move-poster.jpg +0 -0
- package/dist/media/step-shoot-poster.jpg +0 -0
- package/dist/media/step-stage-poster.jpg +0 -0
- package/dist/media/truck-greybox-poster.jpg +0 -0
- package/dist/media/truck-seedance-poster.jpg +0 -0
- package/dist/previs-software/index.html +4 -4
- package/dist/privacy/index.html +46 -19
- package/dist/seedance-camera-control/index.html +3 -3
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +3 -4
- package/package.json +1 -1
- package/src/App.jsx +59 -70
- package/src/analytics-controls.js +17 -0
- package/src/analytics-friction.js +124 -0
- package/src/analytics-strict.js +139 -0
- package/src/analytics-values.js +49 -0
- package/src/analytics.js +37 -133
- package/src/asset-pane.jsx +4 -5
- package/src/click-friction.js +101 -0
- package/src/command-bus-telemetry.js +155 -0
- package/src/command-bus.js +10 -8
- package/src/commands/motion3.js +7 -2
- package/src/controls.jsx +3 -2
- package/src/crash-telemetry.js +45 -0
- package/src/domains/cast.js +2 -11
- package/src/domains/motion.js +49 -38
- package/src/domains/objects.js +3 -0
- package/src/domains/scene-undo.js +27 -0
- package/src/domains/scenes.js +1 -1
- package/src/error-boundary.jsx +2 -0
- package/src/export-attempt.js +100 -0
- package/src/export-workspace.js +12 -1
- package/src/first-success-guide.jsx +11 -7
- package/src/guide-telemetry.js +60 -0
- package/src/hierarchy-panel.jsx +2 -1
- package/src/live-control.js +2 -9
- package/src/locale-zh/domains.js +1 -0
- package/src/main.jsx +5 -0
- package/src/motion/timeline.jsx +9 -5
- package/src/motion-draft-telemetry.js +62 -0
- package/src/motion3/compiler/channels.js +7 -5
- package/src/motion3/compiler/compile.js +6 -2
- package/src/motion3/compiler/finger-close.js +84 -7
- package/src/motion3/compiler/head-lead.js +70 -0
- package/src/motion3/compiler/head-steady.js +1 -1
- package/src/motion3/compiler/limbs.js +10 -3
- package/src/motion3/compiler/pose.js +1 -1
- package/src/motion3/compiler/validate.js +5 -3
- package/src/motion3/pipeline.js +4 -0
- package/src/motion3/qa/explain.js +2 -1
- package/src/motion3/qa/finger-checks.js +80 -0
- package/src/motion3/qa/gait-observe.js +88 -0
- package/src/motion3/qa/grasp-checks.js +3 -0
- package/src/motion3/qa/hints-grasp.js +8 -0
- package/src/motion3/qa/index.js +2 -1
- package/src/motion3/qa/motion-life.js +26 -3
- package/src/motion3/schema/topics.js +1 -1
- package/src/playback-telemetry.js +107 -0
- package/src/post-turn-edits.js +62 -0
- package/src/search-friction.js +49 -0
- package/src/semantic-edit.js +19 -6
- package/src/shell/BoardSlot.jsx +3 -3
- package/src/shell/BottomDock.jsx +1 -1
- package/src/shell/DetailsSlot.jsx +1 -1
- package/src/shell/MenuBar.jsx +13 -13
- package/src/shell/StudioShell.jsx +7 -1
- package/src/shell/TopBar.jsx +2 -0
- package/src/shell/ViewportToolbar.jsx +2 -2
- package/src/shortcut-friction.js +122 -0
- package/src/stage-canvas-rigs.jsx +3 -4
- package/src/studio-telemetry.js +97 -0
- package/src/thrash-friction.js +90 -0
- package/src/update/UpdateChip.jsx +86 -0
- package/src/update/UpdateDialog.jsx +147 -0
- package/src/update/update-client.js +188 -0
- package/src/update/update-telemetry.js +78 -0
- package/src/update/update.css +94 -0
- package/src/use-webgl-available.js +35 -0
- package/src/workflow/AgentPanel.jsx +29 -30
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/LoginPanel.jsx +15 -5
- package/src/workflow/agent-client.js +10 -91
- package/src/workflow/agent-failure-codes.js +47 -0
- package/src/workflow/agent-turn-telemetry.js +222 -0
- package/src/workflow/connect-telemetry.js +86 -0
- package/src/workflow/prompt-telemetry.js +105 -0
- package/tools/dev/pages/privacy.html +44 -17
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +8 -8
- package/dist/assets/app-BdRlMf4J.css +0 -1
- package/dist/assets/app-DwETmoQZ.js +0 -5144
- package/dist/assets/compile-worker-oy_P9HHh.js +0 -6
- package/dist/assets/landing-y3EqDgSZ.js +0 -1
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +0 -1
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +0 -13
- package/dist/assets/task-board-DFfNa0xg.js +0 -15
- package/dist/assets/workflow-BMJGd_gZ.js +0 -179
- package/dist/media/playground-poster.jpg +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,104 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 3.1.5 - 2026-10-25
|
|
6
|
+
|
|
7
|
+
The Studio tells you when a new release is out and installs it in one click, Claude models work in the agent panel again, IK edits stay on their own character, turns and walks look more natural, and anonymous analytics show where the Studio gets in the way.
|
|
8
|
+
|
|
9
|
+
Agent
|
|
10
|
+
|
|
11
|
+
- Agent turns on Claude models no longer fail with a 400 error about a
|
|
12
|
+
deferred placeholder tool.
|
|
13
|
+
- When a turn is already running, the failure chip reads `busy` instead of
|
|
14
|
+
`upstream`. The message text is unchanged.
|
|
15
|
+
|
|
16
|
+
Motion
|
|
17
|
+
|
|
18
|
+
- The head turns first and leads the body into a turn, by about 5 frames at
|
|
19
|
+
24 fps, the way a person's does. `flow.headLead` sets the frames (0 to
|
|
20
|
+
12), and `false` turns it off. A keyed head look still wins.
|
|
21
|
+
- Takes that walk report `observations.gait`: cadence, step length, speed,
|
|
22
|
+
stance and pelvis motion, with warnings outside a person's range. It is a
|
|
23
|
+
reading only and never changes the QA result.
|
|
24
|
+
- Grasp QA reads each finger: `FINGER_ROM` (a joint past its range),
|
|
25
|
+
`FINGER_SELF` (two fingers inside each other) and `THUMB_OFF` (a held
|
|
26
|
+
object without the thumb on it).
|
|
27
|
+
- A carried ball keeps the thumb on it. A finger that misses a held object
|
|
28
|
+
turns toward it at the knuckle, and the ring and little fingers curl
|
|
29
|
+
together. Stacked hand presets no longer over-curl.
|
|
30
|
+
|
|
31
|
+
Studio
|
|
32
|
+
|
|
33
|
+
- An npm studio shows a small **Update** chip in the top bar when a newer
|
|
34
|
+
release is out, once per version. A global install whose folder npm can
|
|
35
|
+
write gets **Update now**, which installs the release and then asks you
|
|
36
|
+
to restart `cclay`. Other installs get the command to copy, and npx gets
|
|
37
|
+
"run `npx cozyclay` again". **Update now** waits while an agent turn,
|
|
38
|
+
an export or a recording runs. A failed install names the cause
|
|
39
|
+
(permissions, network, Node version, npm missing) and keeps the command.
|
|
40
|
+
- An IK edit stays with its own character. Selecting another character
|
|
41
|
+
mid-drag no longer poses it, and undo no longer reverts or deletes the
|
|
42
|
+
other character. Switching characters applies a pending IK edit and shows
|
|
43
|
+
a notice.
|
|
44
|
+
- Ctrl+Z after an IK drag puts the pose back, with or without a take
|
|
45
|
+
loaded.
|
|
46
|
+
- On a browser without WebGL, the unavailable notice appears one frame
|
|
47
|
+
after the Studio first draws instead of during it.
|
|
48
|
+
|
|
49
|
+
CLI
|
|
50
|
+
|
|
51
|
+
- `cclay update --check` prints the current and latest version without
|
|
52
|
+
installing, and exits 0 (up to date), 10 (update available) or 11
|
|
53
|
+
(unknown). `cclay update` now refuses a release your Node.js cannot run.
|
|
54
|
+
- A source checkout reports itself as `clone`, and a Windows global install
|
|
55
|
+
is no longer mistaken for npx.
|
|
56
|
+
|
|
57
|
+
Site
|
|
58
|
+
|
|
59
|
+
- A redesigned cozyclay.org: a black first screen with a greybox-to-video
|
|
60
|
+
reel, how-it-works clips from the tutorial, and Try it opens the Studio.
|
|
61
|
+
|
|
62
|
+
Analytics
|
|
63
|
+
|
|
64
|
+
- The anonymous usage analytics now cover where the Studio gets in the way:
|
|
65
|
+
- the agent loop: refused commands, a summary per agent turn, each motion
|
|
66
|
+
draft and its QA result, and each prompt sent, so retry loops and
|
|
67
|
+
hesitation show
|
|
68
|
+
- model connect: the sign-in steps and where they stop, and model changes
|
|
69
|
+
- exports: retries, the stage a failed or stopped export reached, and
|
|
70
|
+
saved files. Storyboard PNG, render pass and OTIO exports are now
|
|
71
|
+
counted.
|
|
72
|
+
- clicks, shortcuts and search: repeated or dead clicks on the main
|
|
73
|
+
controls, shortcuts that did nothing, searches with no result, and
|
|
74
|
+
switching panels or orbiting the camera back and forth
|
|
75
|
+
- playback, boot and crashes: time to the first frame, playback runs and
|
|
76
|
+
late frames, the first-success guide, render crashes and lost WebGL
|
|
77
|
+
- the update notice: when it shows, which button was pressed, and whether
|
|
78
|
+
the install worked
|
|
79
|
+
- one friction summary per session
|
|
80
|
+
- Every value is a closed bucket or a fixed id. No text, names, paths,
|
|
81
|
+
object ids, click coordinates or npm output are sent. Prompt text never
|
|
82
|
+
leaves the browser; only a length bucket and a similarity grade to the
|
|
83
|
+
last prompt do.
|
|
84
|
+
- First-edit events count only your own edits, not the agent's, and report
|
|
85
|
+
`definition_version` 2.
|
|
86
|
+
- Events that nothing sent any more are removed: the old motion-generation
|
|
87
|
+
events and `sample:played`.
|
|
88
|
+
- The privacy page lists every event and property. `cclay telemetry off`
|
|
89
|
+
still turns it all off.
|
|
90
|
+
|
|
91
|
+
## 3.1.4 - 2026-10-24
|
|
92
|
+
|
|
93
|
+
Starring CozyClay from the terminal takes one key when the GitHub CLI is signed in.
|
|
94
|
+
|
|
95
|
+
CLI
|
|
96
|
+
|
|
97
|
+
- The one-time star question on exit stars NomaDamas/CozyClay through `gh`
|
|
98
|
+
when `gh` is installed and signed in: answer `y` and it is done, with no
|
|
99
|
+
browser. An account that already starred the repo is not asked. Without
|
|
100
|
+
`gh` the question opens the repo page as before, and `--no-star`,
|
|
101
|
+
`COZYCLAY_NO_STAR` and CI still turn it off.
|
|
102
|
+
|
|
5
103
|
## 3.1.3 - 2026-10-24
|
|
6
104
|
|
|
7
105
|
More ways to connect a model (Claude Pro/Max, OpenAI-compatible servers, MiniMax M3.1 Flash), a Simplified Chinese UI, and easier project opening.
|
package/INSTALL_FOR_AGENTS.md
CHANGED
|
@@ -32,6 +32,9 @@ npx -y cozyclay --no-star
|
|
|
32
32
|
|
|
33
33
|
For a permanent install, run `npm install -g cozyclay`. Then `cclay` and `cozyclay` are the same
|
|
34
34
|
command. `cclay update` installs the latest release.
|
|
35
|
+
`cclay update --check` installs nothing. It prints the current version, the latest version and
|
|
36
|
+
the install kind, then exits `0` when up to date, `10` when an update is available, and `11` when
|
|
37
|
+
it cannot tell (offline, or checks turned off).
|
|
35
38
|
|
|
36
39
|
Tell the user:
|
|
37
40
|
|
package/README.md
CHANGED
|
@@ -58,6 +58,11 @@ npx cozyclay
|
|
|
58
58
|
That is the whole install. You need Node.js 22.19 or newer and a Chromium browser. You do not need
|
|
59
59
|
a GPU, Python or a model download. The studio opens at `http://127.0.0.1:5180/app/`.
|
|
60
60
|
|
|
61
|
+
**Updating.** `npx cozyclay` fetches the newest release each time you run it. After
|
|
62
|
+
`npm install -g cozyclay`, the studio shows a small **Update** chip in the top bar when a newer
|
|
63
|
+
release is out. **Update now** installs it, and then you restart `cclay`. `cclay update` does the
|
|
64
|
+
same from the terminal. `--no-update-check` or `COZYCLAY_NO_UPDATE_CHECK=1` turns the check off.
|
|
65
|
+
|
|
61
66
|
> **Asking an AI agent to set it up?** Give it this repository link. Its instructions are in
|
|
62
67
|
> [`INSTALL_FOR_AGENTS.md`](INSTALL_FOR_AGENTS.md).
|
|
63
68
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The closed `agent:turn_failed.failure_code` vocabulary, sidecar side. Kept
|
|
2
|
+
// self-contained (minimal sidecar installs omit src/); it gives the same answers
|
|
3
|
+
// as src/workflow/agent-failure-codes.js, whose header defines each code, and
|
|
4
|
+
// test/verify-agent-execution.mjs holds the two tables to that. Only the
|
|
5
|
+
// structured code and HTTP status are read, never a message.
|
|
6
|
+
const BY_CODE = {
|
|
7
|
+
unauthorized: "key_refused", auth: "auth", AUTH_REQUIRED: "auth", rate_limit: "rate_limited", RATE_LIMITED: "rate_limited",
|
|
8
|
+
STALE_SCENE: "stale_scene", STALE_TARGET: "stale_scene", STALE_ENVIRONMENT: "stale_scene",
|
|
9
|
+
CAPABILITY_MISSING: "capability_missing", LIVE_HUB_UNAVAILABLE: "capability_missing",
|
|
10
|
+
busy: "busy", TARGET_BUSY: "busy", SCOPE_BUSY: "busy", TARGET_NOT_READY: "busy",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** `{ code, status }` (a thrown error or a runner `error` frame) -> failure code.
|
|
14
|
+
* The runner's `overloaded`/`upstream`/`truncated` and every 5xx read as upstream. */
|
|
15
|
+
export function failureCodeOf(error, fallback = "upstream") {
|
|
16
|
+
try {
|
|
17
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
18
|
+
const status = error?.status;
|
|
19
|
+
if (Object.hasOwn(BY_CODE, code)) return BY_CODE[code];
|
|
20
|
+
if (code.startsWith("INVALID_")) return "invalid_request";
|
|
21
|
+
if (status === 401 || status === 403) return "auth";
|
|
22
|
+
if (status === 429) return "rate_limited";
|
|
23
|
+
return Number.isInteger(status) && status >= 400 && status < 500 ? "invalid_request" : fallback;
|
|
24
|
+
} catch { return fallback; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A turn that threw: an abort wins, then the error's own code, then a tool failure. */
|
|
28
|
+
export const agentFailureCode = (error, signal, tool = false) => {
|
|
29
|
+
try { if (signal.aborted || error?.name === "AbortError") return "aborted"; } catch { /* foreign getter */ }
|
|
30
|
+
return failureCodeOf(error, tool ? "tool_failed" : "upstream");
|
|
31
|
+
};
|
|
@@ -13,6 +13,7 @@ import { createAgentRunner } from "./agent-runner.mjs";
|
|
|
13
13
|
import { createProviderRoutes } from "./provider-routes.mjs";
|
|
14
14
|
import { STUDIO_SYSTEM_PROMPT_STORYBOARD } from "./studio-prompt.mjs";
|
|
15
15
|
import { studioTransaction } from "./studio-turn-transaction.mjs";
|
|
16
|
+
import { agentFailureCode, failureCodeOf } from "./agent-failure-codes.mjs";
|
|
16
17
|
|
|
17
18
|
// Values the codex backend accepts for reasoning.effort (its own 400 lists them).
|
|
18
19
|
export const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
@@ -77,12 +78,6 @@ const agentToolCategory = (name) => {
|
|
|
77
78
|
if (name === "place_image_in_scene" || name === "add_reference_node") return "scene_write";
|
|
78
79
|
return "other";
|
|
79
80
|
};
|
|
80
|
-
const agentFailureCode = (error, signal, tool = false) => advisory(() => {
|
|
81
|
-
if (signal.aborted || error?.name === "AbortError") return "aborted";
|
|
82
|
-
if (error?.status === 401 || error?.code === "unauthorized") return "auth";
|
|
83
|
-
if (error?.status === 429) return "rate_limited";
|
|
84
|
-
return tool ? "tool_failed" : "upstream";
|
|
85
|
-
}, tool ? "tool_failed" : "upstream");
|
|
86
81
|
// These existing canvas commands only return a node/edge after publishing a
|
|
87
82
|
// new insertion. Read/focus, generic accepted responses, update no-ops and run
|
|
88
83
|
// outputs (which can echo old values) are deliberately not application proof.
|
|
@@ -579,7 +574,7 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
|
|
|
579
574
|
}
|
|
580
575
|
if (frame.type === "error") {
|
|
581
576
|
turnOutcome = controller.signal.aborted || frame.code === "aborted" ? "cancelled" : "failed";
|
|
582
|
-
turnFailureCode = controller.signal.aborted ? "aborted" : frame
|
|
577
|
+
turnFailureCode = controller.signal.aborted || frame.code === "aborted" ? "aborted" : failureCodeOf(frame);
|
|
583
578
|
return send(frame);
|
|
584
579
|
}
|
|
585
580
|
if (frame.type === "done") {
|
|
@@ -822,7 +817,7 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
|
|
|
822
817
|
const send = (event) => { if (!res.writableEnded && !res.destroyed) res.write(`data: ${JSON.stringify(event)}\n\n`); };
|
|
823
818
|
let session = sessions.get(value.sessionId);
|
|
824
819
|
if (session?.running) {
|
|
825
|
-
send({ type: "error", code: "
|
|
820
|
+
send({ type: "error", code: "busy", message: "busy" }); send({ type: "done" }); res.end(); return true;
|
|
826
821
|
}
|
|
827
822
|
session ||= { images: new Map(), history: [] };
|
|
828
823
|
sessions.set(value.sessionId, session);
|
|
@@ -870,7 +865,7 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
|
|
|
870
865
|
return;
|
|
871
866
|
}
|
|
872
867
|
turnOutcome = signal.aborted || frame.code === "aborted" ? "cancelled" : "failed";
|
|
873
|
-
turnFailureCode = signal.aborted ? "aborted" : frame
|
|
868
|
+
turnFailureCode = signal.aborted || frame.code === "aborted" ? "aborted" : failureCodeOf(frame);
|
|
874
869
|
return send(frame);
|
|
875
870
|
}
|
|
876
871
|
if (frame.type === "done") {
|
|
@@ -903,7 +898,7 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
|
|
|
903
898
|
catch (error) {
|
|
904
899
|
turnOutcome = signal.aborted ? "cancelled" : "failed";
|
|
905
900
|
turnFailureCode = signal.aborted ? "aborted" : agentFailureCode(error, signal, toolFailed);
|
|
906
|
-
if (!signal.aborted)
|
|
901
|
+
if (!signal.aborted) send({ type: "error", ...errorInfo(error, quota) });
|
|
907
902
|
if (!res.writableEnded) emitFrame({ type: "done" });
|
|
908
903
|
}
|
|
909
904
|
finally {
|
|
@@ -220,8 +220,10 @@ export function withAnthropicCacheBreakpoints(payload, model) {
|
|
|
220
220
|
next = { ...next, system: [...system.slice(0, -1), { ...system.at(-1), cache_control: cacheControl }] };
|
|
221
221
|
count += 1;
|
|
222
222
|
}
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
// pi-ai 1.4 appends a defer_loading placeholder tool; the API rejects cache_control on deferred tools.
|
|
224
|
+
const lastTool = tools.findLastIndex((tool) => !tool?.defer_loading);
|
|
225
|
+
if (lastTool !== -1 && !marked(tools[lastTool]) && model?.compat?.supportsCacheControlOnTools !== false && count < 4) {
|
|
226
|
+
next = { ...next, tools: tools.map((tool, index) => index === lastTool ? { ...tool, cache_control: cacheControl } : tool) };
|
|
225
227
|
}
|
|
226
228
|
return next === payload ? undefined : next;
|
|
227
229
|
}
|
|
@@ -511,7 +513,7 @@ export function createAgentRunner({ models: suppliedModels, sessionStore, tools
|
|
|
511
513
|
|
|
512
514
|
const start = async function* (input = {}) {
|
|
513
515
|
if (state.active) {
|
|
514
|
-
yield { type: "error", code: "
|
|
516
|
+
yield { type: "error", code: "busy", message: "busy" };
|
|
515
517
|
yield { type: "done" };
|
|
516
518
|
return;
|
|
517
519
|
}
|
package/bin/cozyclay.mjs
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* that needs an install step before it can serve a prebuilt app is a
|
|
15
15
|
* launcher that will break.
|
|
16
16
|
*/
|
|
17
|
-
import { spawn } from "node:child_process";
|
|
18
17
|
import { createReadStream, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
19
18
|
import { createServer } from "node:http";
|
|
20
19
|
import { homedir } from "node:os";
|
|
@@ -23,7 +22,9 @@ import { createInterface } from "node:readline";
|
|
|
23
22
|
import { fileURLToPath } from "node:url";
|
|
24
23
|
import { ensureMcpRuntime, runMcp } from "./mcp-runtime.mjs";
|
|
25
24
|
import { openBrowser } from "./open-browser.mjs";
|
|
26
|
-
import {
|
|
25
|
+
import { maybeAskForStar } from "./star-prompt.mjs";
|
|
26
|
+
import { checkForUpdate, readUpdateStatus, runUpdate, runUpdateCheck, updateChecksOn } from "./update-check.mjs";
|
|
27
|
+
import { createStudioRoutes } from "./studio-routes.mjs";
|
|
27
28
|
import { handleOAuthRequest } from "./codex-auth.mjs";
|
|
28
29
|
import { createAgentHandler } from "./agent/agent-routes.mjs";
|
|
29
30
|
import { liveHubPort } from "./live-endpoint.mjs";
|
|
@@ -55,8 +56,8 @@ const OFFICIAL_PACKAGE = (
|
|
|
55
56
|
&& !SOURCE_CHECKOUT
|
|
56
57
|
&& verifyPackageMarker(join(DIST, "cozyclay-package.json"), PKG_ROOT, packageMetadata)
|
|
57
58
|
);
|
|
58
|
-
const INSTALL_KIND = process.env.npm_config_global === "true"
|
|
59
|
-
|| (
|
|
59
|
+
const INSTALL_KIND = SOURCE_CHECKOUT ? "clone" : process.env.npm_config_global === "true"
|
|
60
|
+
|| (/[\\/]node_modules[\\/]/.test(PKG_ROOT) && !/[\\/]_npx[\\/]/.test(PKG_ROOT))
|
|
60
61
|
? "global"
|
|
61
62
|
: "npx";
|
|
62
63
|
|
|
@@ -120,12 +121,8 @@ function parseArgs(argv) {
|
|
|
120
121
|
return opts;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
const REPO_URL = "https://github.com/NomaDamas/CozyClay";
|
|
124
124
|
const STATE_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cozyclay");
|
|
125
125
|
const STATE_FILE = join(STATE_DIR, "state.json");
|
|
126
|
-
// Only worth asking someone who actually used the thing. A prompt three
|
|
127
|
-
// seconds in is a popup; a prompt after a real session is a question.
|
|
128
|
-
const STAR_AFTER_MS = Number(process.env.COZYCLAY_STAR_AFTER_MS ?? 60_000);
|
|
129
126
|
|
|
130
127
|
async function askFirstLaunchSource() {
|
|
131
128
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
@@ -146,6 +143,7 @@ const HELP = `cozyclay - browser-based 3D staging studio
|
|
|
146
143
|
cclay live status drive a running studio from the terminal
|
|
147
144
|
(cclay live --help lists every verb)
|
|
148
145
|
cclay update install the latest cozyclay globally (npm install -g)
|
|
146
|
+
cclay update --check only compare: exit 0 latest, 10 newer exists, 11 unknown
|
|
149
147
|
npx cozyclay --port 5200 serve on another port
|
|
150
148
|
npx cozyclay --no-open do not open a browser
|
|
151
149
|
npx cozyclay --scene <id> open the studio on a bundled starter scene
|
|
@@ -197,49 +195,6 @@ function writeState(patch) {
|
|
|
197
195
|
}
|
|
198
196
|
}
|
|
199
197
|
|
|
200
|
-
async function starCount() {
|
|
201
|
-
try {
|
|
202
|
-
const res = await fetch("https://api.github.com/repos/NomaDamas/CozyClay", {
|
|
203
|
-
headers: { accept: "application/vnd.github+json" },
|
|
204
|
-
signal: AbortSignal.timeout(2500),
|
|
205
|
-
});
|
|
206
|
-
if (!res.ok) return null;
|
|
207
|
-
const body = await res.json();
|
|
208
|
-
return Number.isInteger(body?.stargazers_count) ? body.stargazers_count : null;
|
|
209
|
-
} catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Asked once, on the way out, and never again on this machine. A CLI that
|
|
215
|
-
// nags on every run is worse than one that never asks.
|
|
216
|
-
async function maybeAskForStar(startedAt, opts) {
|
|
217
|
-
if (!opts.star) return;
|
|
218
|
-
if (process.env.CI || process.env.COZYCLAY_NO_STAR) return;
|
|
219
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
220
|
-
if (Date.now() - startedAt < STAR_AFTER_MS) return;
|
|
221
|
-
if (readState().starPromptedAt) return;
|
|
222
|
-
|
|
223
|
-
writeState({ starPromptedAt: new Date().toISOString() });
|
|
224
|
-
const stars = await starCount();
|
|
225
|
-
const tally = stars === null ? "" : ` (${stars} so far)`;
|
|
226
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
227
|
-
const answer = await new Promise((done) => {
|
|
228
|
-
const timer = setTimeout(() => done(""), 12_000);
|
|
229
|
-
rl.question(`\nWas CozyClay any good? A star helps people find it${tally}. Open GitHub? [Y/n] `, (a) => {
|
|
230
|
-
clearTimeout(timer);
|
|
231
|
-
done(a);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
rl.close();
|
|
235
|
-
if (/^(y|yes|)$/i.test(answer.trim())) {
|
|
236
|
-
openBrowser(REPO_URL);
|
|
237
|
-
console.log("Thanks. Opening the repo.");
|
|
238
|
-
} else {
|
|
239
|
-
console.log(`Fair enough. ${REPO_URL} if you change your mind.`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
198
|
// Read on every launch now, so it must not be able to take the launcher down:
|
|
244
199
|
// the version is cosmetic here, and "0.0.0" just suppresses a bogus notice.
|
|
245
200
|
function readVersion() {
|
|
@@ -257,11 +212,12 @@ if (argv[0] === "mcp") {
|
|
|
257
212
|
} else if (argv[0] === "update") {
|
|
258
213
|
// This branch bypasses parseArgs, so anything trailing would be swallowed
|
|
259
214
|
// and `cclay update --help` would perform an unrequested global install.
|
|
260
|
-
if (argv.length > 1) {
|
|
261
|
-
console.error(`cozyclay: update takes no arguments (got ${argv.slice(1).join(" ")})`);
|
|
215
|
+
if (argv.length > 1 && !(argv.length === 2 && argv[1] === "--check")) {
|
|
216
|
+
console.error(`cozyclay: update takes no arguments except --check (got ${argv.slice(1).join(" ")})`);
|
|
262
217
|
process.exit(1);
|
|
263
218
|
}
|
|
264
|
-
|
|
219
|
+
if (argv[1] === "--check") process.exit(await runUpdateCheck({ currentVersion: readVersion(), kind: INSTALL_KIND, stateDir: STATE_DIR }));
|
|
220
|
+
else runUpdate();
|
|
265
221
|
} else if (argv[0] === "telemetry") {
|
|
266
222
|
const action = argv[1] ?? "status";
|
|
267
223
|
const internalAction = action === "internal" ? argv[2] : null;
|
|
@@ -297,21 +253,14 @@ if (opts.version) {
|
|
|
297
253
|
console.log(version);
|
|
298
254
|
process.exit(0);
|
|
299
255
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
officialPackage: OFFICIAL_PACKAGE,
|
|
303
|
-
installKind: INSTALL_KIND,
|
|
304
|
-
});
|
|
256
|
+
const takeRuntime = () => takeRuntimeTelemetryConfig(STATE_FILE, { appVersion: version, officialPackage: OFFICIAL_PACKAGE, installKind: INSTALL_KIND });
|
|
257
|
+
let runtimeTelemetry = takeRuntime();
|
|
305
258
|
if (runtimeTelemetry.firstLaunch && !readTelemetryState(STATE_FILE).firstLaunchHeardFrom) {
|
|
306
259
|
// A copied `--via` command already says where the install came from.
|
|
307
260
|
const heardFrom = opts.via || await askFirstLaunchSource();
|
|
308
261
|
if (heardFrom) {
|
|
309
262
|
setTelemetryFirstLaunchSource(STATE_FILE, heardFrom);
|
|
310
|
-
runtimeTelemetry =
|
|
311
|
-
appVersion: version,
|
|
312
|
-
officialPackage: OFFICIAL_PACKAGE,
|
|
313
|
-
installKind: INSTALL_KIND,
|
|
314
|
-
});
|
|
263
|
+
runtimeTelemetry = takeRuntime();
|
|
315
264
|
}
|
|
316
265
|
}
|
|
317
266
|
const telemetryState = readTelemetryState(STATE_FILE);
|
|
@@ -327,7 +276,7 @@ if (
|
|
|
327
276
|
}
|
|
328
277
|
// Fired before anything blocking and never awaited on the launch path: the
|
|
329
278
|
// notice is worth a line of output, never a second of startup.
|
|
330
|
-
const updateChecksEnabled =
|
|
279
|
+
const updateChecksEnabled = updateChecksOn(opts.updateCheck);
|
|
331
280
|
const updatePending = updateChecksEnabled ? checkForUpdate(version, STATE_DIR) : null;
|
|
332
281
|
if (!existsSync(join(DIST, "app", "index.html"))) {
|
|
333
282
|
console.error("cozyclay: this package is missing its build (dist/app/index.html).");
|
|
@@ -343,7 +292,7 @@ async function shutdown(exitCode = 0) {
|
|
|
343
292
|
shuttingDown = true;
|
|
344
293
|
if (server?.listening) await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
345
294
|
try {
|
|
346
|
-
await maybeAskForStar(startedAt, opts);
|
|
295
|
+
await maybeAskForStar(startedAt, opts, { readState, writeState });
|
|
347
296
|
} catch {
|
|
348
297
|
/* never let the goodbye prompt hold the process hostage */
|
|
349
298
|
}
|
|
@@ -355,6 +304,7 @@ process.on("SIGTERM", () => shutdown(143));
|
|
|
355
304
|
// A source checkout has mcp/node_modules; the package loads the Studio's live
|
|
356
305
|
// tools from the staged MCP runtime that `cclay mcp` also uses (#576).
|
|
357
306
|
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 } });
|
|
358
308
|
server = createServer((req, res) => {
|
|
359
309
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
360
310
|
if (/^\/oauth\/(start|status|logout)$/.test(url.pathname)) {
|
|
@@ -374,47 +324,7 @@ server = createServer((req, res) => {
|
|
|
374
324
|
});
|
|
375
325
|
return;
|
|
376
326
|
}
|
|
377
|
-
if (
|
|
378
|
-
if (req.method === "POST") {
|
|
379
|
-
const origin = req.headers.origin;
|
|
380
|
-
const expectedOrigin = `http://127.0.0.1:${opts.port}`;
|
|
381
|
-
if (origin !== expectedOrigin) {
|
|
382
|
-
res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
|
|
383
|
-
res.end(JSON.stringify({ error: "forbidden origin" }));
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
let body = "";
|
|
387
|
-
req.setEncoding("utf8");
|
|
388
|
-
req.on("data", (chunk) => {
|
|
389
|
-
body += chunk;
|
|
390
|
-
if (body.length > 1024) req.destroy();
|
|
391
|
-
});
|
|
392
|
-
req.on("end", () => {
|
|
393
|
-
try {
|
|
394
|
-
const value = JSON.parse(body);
|
|
395
|
-
if (typeof value.enabled !== "boolean") throw new TypeError("enabled must be boolean");
|
|
396
|
-
setTelemetryEnabled(STATE_FILE, value.enabled);
|
|
397
|
-
runtimeTelemetry = takeRuntimeTelemetryConfig(STATE_FILE, {
|
|
398
|
-
appVersion: version,
|
|
399
|
-
officialPackage: OFFICIAL_PACKAGE,
|
|
400
|
-
installKind: INSTALL_KIND,
|
|
401
|
-
});
|
|
402
|
-
res.writeHead(200, {
|
|
403
|
-
"content-type": "application/json; charset=utf-8",
|
|
404
|
-
"cache-control": "no-store",
|
|
405
|
-
});
|
|
406
|
-
res.end(JSON.stringify(runtimeTelemetry));
|
|
407
|
-
} catch {
|
|
408
|
-
res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
|
|
409
|
-
res.end(JSON.stringify({ error: "invalid telemetry setting" }));
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
res.writeHead(405, { allow: "POST", "content-type": "application/json; charset=utf-8" });
|
|
415
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
327
|
+
if (studioRoutes(req, res, url)) return;
|
|
418
328
|
let rel;
|
|
419
329
|
try {
|
|
420
330
|
rel = decodeURIComponent(url.pathname);
|
|
@@ -442,11 +352,7 @@ server = createServer((req, res) => {
|
|
|
442
352
|
return;
|
|
443
353
|
}
|
|
444
354
|
if (target === join(DIST, "app", "index.html")) {
|
|
445
|
-
runtimeTelemetry =
|
|
446
|
-
appVersion: version,
|
|
447
|
-
officialPackage: OFFICIAL_PACKAGE,
|
|
448
|
-
installKind: INSTALL_KIND,
|
|
449
|
-
});
|
|
355
|
+
runtimeTelemetry = takeRuntime();
|
|
450
356
|
// Whether this launch is the newest release, from the registry answer the
|
|
451
357
|
// update check already cached (#466). Never a request of its own.
|
|
452
358
|
const runtime = { ...runtimeTelemetry, updateStatus: updateChecksEnabled ? readUpdateStatus(version, STATE_DIR) : "unknown" };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-time "star the repo" question on the way out of `npx cozyclay` (#900).
|
|
3
|
+
*
|
|
4
|
+
* With the GitHub CLI installed and signed in, a "y" stars the repo through `gh` on
|
|
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 machine, after a
|
|
7
|
+
* real session, never in CI or without a terminal, and never to an account that has
|
|
8
|
+
* already starred it. Every failure here is silent: this runs while the launcher exits.
|
|
9
|
+
*/
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
import { openBrowser } from "./open-browser.mjs";
|
|
13
|
+
|
|
14
|
+
const REPO = "NomaDamas/CozyClay";
|
|
15
|
+
const REPO_URL = `https://github.com/${REPO}`;
|
|
16
|
+
const STARRED_PATH = `/user/starred/${REPO}`;
|
|
17
|
+
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
|
+
|
|
22
|
+
/** Runs gh and resolves true on exit 0. No gh on PATH, a timeout or an error status is false. */
|
|
23
|
+
function runGh(args) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
try {
|
|
26
|
+
execFile("gh", args, {
|
|
27
|
+
timeout: 4000,
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1" },
|
|
30
|
+
}, (error) => resolve(!error));
|
|
31
|
+
} catch {
|
|
32
|
+
resolve(false);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function starCount() {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
|
|
40
|
+
headers: { accept: "application/vnd.github+json" },
|
|
41
|
+
signal: AbortSignal.timeout(2500),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) return null;
|
|
44
|
+
const body = await res.json();
|
|
45
|
+
return Number.isInteger(body?.stargazers_count) ? body.stargazers_count : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function askLine(question) {
|
|
52
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
return new Promise((done) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
rl.close();
|
|
56
|
+
done("");
|
|
57
|
+
}, ANSWER_TIMEOUT_MS);
|
|
58
|
+
rl.question(question, (answer) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
rl.close();
|
|
61
|
+
done(answer);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Asked once, on the way out, and never again on this machine. A CLI that nags on
|
|
68
|
+
* every run is worse than one that never asks. Everything after `state` is injectable
|
|
69
|
+
* so the test can drive it without a terminal, gh, or the network.
|
|
70
|
+
*/
|
|
71
|
+
export async function maybeAskForStar(startedAt, opts, {
|
|
72
|
+
readState,
|
|
73
|
+
writeState,
|
|
74
|
+
env = process.env,
|
|
75
|
+
interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
76
|
+
now = Date.now,
|
|
77
|
+
minSessionMs = STAR_AFTER_MS,
|
|
78
|
+
gh = runGh,
|
|
79
|
+
ask = askLine,
|
|
80
|
+
stars = starCount,
|
|
81
|
+
open = openBrowser,
|
|
82
|
+
log = console.log,
|
|
83
|
+
}) {
|
|
84
|
+
if (!opts.star) return;
|
|
85
|
+
if (env.CI || env.COZYCLAY_NO_STAR) return;
|
|
86
|
+
if (!interactive) return;
|
|
87
|
+
if (now() - startedAt < minSessionMs) return;
|
|
88
|
+
if (readState().starPromptedAt) return;
|
|
89
|
+
|
|
90
|
+
const promptedAt = new Date(now()).toISOString();
|
|
91
|
+
const viaGh = await gh(["auth", "status"]);
|
|
92
|
+
// GET /user/starred/<repo> answers 204 for a starred repo and 404 otherwise.
|
|
93
|
+
if (viaGh && await gh(["api", STARRED_PATH, "--silent"])) {
|
|
94
|
+
writeState({ starPromptedAt: promptedAt });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
writeState({ starPromptedAt: promptedAt });
|
|
98
|
+
const count = await stars();
|
|
99
|
+
const tally = count === null ? "" : ` (${count} so far)`;
|
|
100
|
+
|
|
101
|
+
if (viaGh) {
|
|
102
|
+
const answer = await ask(`\nWas CozyClay any good? A star helps people find it${tally}. Star ${REPO} with your gh account? [y/N] `);
|
|
103
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
104
|
+
log(`No problem. ${REPO_URL} if you change your mind.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (await gh(["api", "--method", "PUT", STARRED_PATH, "--silent"])) {
|
|
108
|
+
log(`Starred ${REPO}. Thank you!`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
log("gh could not star it, so opening the repo instead.");
|
|
112
|
+
open(REPO_URL);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const answer = await ask(`\nWas CozyClay any good? A star helps people find it${tally}. Open GitHub? [Y/n] `);
|
|
117
|
+
if (/^(y|yes|)$/i.test(answer.trim())) {
|
|
118
|
+
open(REPO_URL);
|
|
119
|
+
log("Thanks. Opening the repo.");
|
|
120
|
+
} else {
|
|
121
|
+
log(`Fair enough. ${REPO_URL} if you change your mind.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launcher's own /__cozyclay/* routes, kept out of bin/cozyclay.mjs:
|
|
3
|
+
* the telemetry toggle the Studio's privacy setting posts to, and the update
|
|
4
|
+
* routes behind the Studio's update chip.
|
|
5
|
+
*/
|
|
6
|
+
import { setTelemetryEnabled } from "./telemetry-state.mjs";
|
|
7
|
+
import { lookupLatest } from "./update-check.mjs";
|
|
8
|
+
import { canSelfUpdate, createInstaller } from "./update-install.mjs";
|
|
9
|
+
import { createUpdateRoutes } from "./update-routes.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns a handler that answers a /__cozyclay/* request and returns true, or
|
|
13
|
+
* returns false for any other path. `takeRuntime` re-reads the runtime
|
|
14
|
+
* 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
|
+
*/
|
|
17
|
+
export function createStudioRoutes({ port, stateFile, takeRuntime, update: { currentVersion, stateDir, kind, enabled, pkgRoot } }) {
|
|
18
|
+
const updateRoute = createUpdateRoutes({
|
|
19
|
+
currentVersion,
|
|
20
|
+
kind,
|
|
21
|
+
enabled,
|
|
22
|
+
port,
|
|
23
|
+
checkLatest: () => lookupLatest(currentVersion, stateDir),
|
|
24
|
+
canSelfUpdate: () => canSelfUpdate({ pkgRoot }),
|
|
25
|
+
installer: createInstaller(),
|
|
26
|
+
});
|
|
27
|
+
return function handleStudioRoute(req, res, url) {
|
|
28
|
+
if (url.pathname === "/__cozyclay/update") {
|
|
29
|
+
void updateRoute(req, res);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (url.pathname !== "/__cozyclay/telemetry") return false;
|
|
33
|
+
if (req.method === "POST") {
|
|
34
|
+
const origin = req.headers.origin;
|
|
35
|
+
const expectedOrigin = `http://127.0.0.1:${port}`;
|
|
36
|
+
if (origin !== expectedOrigin) {
|
|
37
|
+
res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
|
|
38
|
+
res.end(JSON.stringify({ error: "forbidden origin" }));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
let body = "";
|
|
42
|
+
req.setEncoding("utf8");
|
|
43
|
+
req.on("data", (chunk) => {
|
|
44
|
+
body += chunk;
|
|
45
|
+
if (body.length > 1024) req.destroy();
|
|
46
|
+
});
|
|
47
|
+
req.on("end", () => {
|
|
48
|
+
try {
|
|
49
|
+
const value = JSON.parse(body);
|
|
50
|
+
if (typeof value.enabled !== "boolean") throw new TypeError("enabled must be boolean");
|
|
51
|
+
setTelemetryEnabled(stateFile, value.enabled);
|
|
52
|
+
const runtimeTelemetry = takeRuntime();
|
|
53
|
+
res.writeHead(200, {
|
|
54
|
+
"content-type": "application/json; charset=utf-8",
|
|
55
|
+
"cache-control": "no-store",
|
|
56
|
+
});
|
|
57
|
+
res.end(JSON.stringify(runtimeTelemetry));
|
|
58
|
+
} catch {
|
|
59
|
+
res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
|
|
60
|
+
res.end(JSON.stringify({ error: "invalid telemetry setting" }));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
res.writeHead(405, { allow: "POST", "content-type": "application/json; charset=utf-8" });
|
|
66
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
}
|