From 836619caff14198e33c1af05923df571f213b0b9 Mon Sep 17 00:00:00 2001 From: iamcipherdev <268238676+iamcipherdev@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:45:39 +0000 Subject: [PATCH 1/2] fix(media-use): honor telemetryEnabled: false from shared config in optedOut The CLI's 'hyperframes telemetry disable' writes telemetryEnabled: false to ~/.hyperframes/config.json, but the media-use scripts' optedOut() only checked env vars, so transcribe.mjs, transcript-cut.mjs and audio-duck.mjs kept sending PostHog events for users who had opted out through the CLI. optedOut() now also returns true when the shared config has telemetryEnabled === false, reusing the readSharedConfig() helper already in the same file. Applied byte-identically to both copies of telemetry.mjs (skills/media-use/scripts/lib and packages/cli/src/media-use/lib, per the copy-parity rule), with tests in both test files. Closes #5465 --- packages/cli/src/media-use/lib/telemetry.mjs | 5 +- .../cli/src/media-use/lib/telemetry.test.mjs | 20 ++++++ skills/media-use/scripts/lib/telemetry.mjs | 5 +- .../media-use/scripts/lib/telemetry.test.mjs | 64 +++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 skills/media-use/scripts/lib/telemetry.test.mjs diff --git a/packages/cli/src/media-use/lib/telemetry.mjs b/packages/cli/src/media-use/lib/telemetry.mjs index 8ceb7739637..182e3505b5f 100644 --- a/packages/cli/src/media-use/lib/telemetry.mjs +++ b/packages/cli/src/media-use/lib/telemetry.mjs @@ -35,9 +35,12 @@ function posthogHost() { return override || POSTHOG_HOST; } -/** True when telemetry must NOT be sent (opt-out envs, CI, dev). */ +/** True when telemetry must NOT be sent (opt-out envs, CI, dev, shared config). */ export function optedOut() { return ( + // `hyperframes telemetry disable` writes telemetryEnabled: false to the + // shared config; one opt-out must cover the CLI and the media-use scripts. + readSharedConfig().telemetryEnabled === false || process.env.HYPERFRAMES_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1" || process.env.CI === "true" || diff --git a/packages/cli/src/media-use/lib/telemetry.test.mjs b/packages/cli/src/media-use/lib/telemetry.test.mjs index f534210ea11..19db85a4cb0 100644 --- a/packages/cli/src/media-use/lib/telemetry.test.mjs +++ b/packages/cli/src/media-use/lib/telemetry.test.mjs @@ -57,6 +57,26 @@ test("optedOut respects DO_NOT_TRACK / HYPERFRAMES_NO_TELEMETRY / CI", () => { } }); +test("optedOut honors telemetryEnabled: false from the shared config", () => { + const savedEnv = { ...process.env }; + const { root, home } = sandbox(); + try { + withoutTelemetryOptOut(); + const configPath = join(home, ".hyperframes", "config.json"); + assert.equal(optedOut(), false, "no config: tracking allowed"); + mkdirSync(join(home, ".hyperframes"), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ telemetryEnabled: false })); + assert.equal(optedOut(), true, "telemetryEnabled: false opts out"); + writeFileSync(configPath, JSON.stringify({ telemetryEnabled: true })); + assert.equal(optedOut(), false, "telemetryEnabled: true allows tracking"); + writeFileSync(configPath, "{not json"); + assert.equal(optedOut(), false, "unreadable config: tracking allowed"); + } finally { + restoreEnv(savedEnv); + rmSync(root, { recursive: true, force: true }); + } +}); + test("track is a no-op (no network, resolves) when opted out", async () => { const savedEnv = { ...process.env }; const originalFetch = globalThis.fetch; diff --git a/skills/media-use/scripts/lib/telemetry.mjs b/skills/media-use/scripts/lib/telemetry.mjs index 8ceb7739637..182e3505b5f 100644 --- a/skills/media-use/scripts/lib/telemetry.mjs +++ b/skills/media-use/scripts/lib/telemetry.mjs @@ -35,9 +35,12 @@ function posthogHost() { return override || POSTHOG_HOST; } -/** True when telemetry must NOT be sent (opt-out envs, CI, dev). */ +/** True when telemetry must NOT be sent (opt-out envs, CI, dev, shared config). */ export function optedOut() { return ( + // `hyperframes telemetry disable` writes telemetryEnabled: false to the + // shared config; one opt-out must cover the CLI and the media-use scripts. + readSharedConfig().telemetryEnabled === false || process.env.HYPERFRAMES_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1" || process.env.CI === "true" || diff --git a/skills/media-use/scripts/lib/telemetry.test.mjs b/skills/media-use/scripts/lib/telemetry.test.mjs new file mode 100644 index 00000000000..ea2be0ad6fc --- /dev/null +++ b/skills/media-use/scripts/lib/telemetry.test.mjs @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { optedOut } from "./telemetry.mjs"; + +// optedOut() also reads env vars; pin them to a neutral state for the whole +// file and restore everything (env + shared config) afterwards. +const ENV_KEYS = ["HYPERFRAMES_NO_TELEMETRY", "DO_NOT_TRACK", "CI", "NODE_ENV"]; +const savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); +const configPath = join(homedir(), ".hyperframes", "config.json"); +const savedConfig = existsSync(configPath) ? readFileSync(configPath, "utf8") : null; + +function writeConfig(contents) { + if (contents === null) { + rmSync(configPath, { force: true }); + } else { + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, contents); + } +} + +test.before(() => { + for (const key of ENV_KEYS) delete process.env[key]; +}); + +test.after(() => { + for (const key of ENV_KEYS) { + if (savedEnv[key] === undefined) delete process.env[key]; + else process.env[key] = savedEnv[key]; + } + writeConfig(savedConfig); +}); + +test("optedOut is false with no config and no opt-out env", () => { + writeConfig(null); + assert.equal(optedOut(), false); +}); + +test("optedOut honors telemetryEnabled: false from the shared config", () => { + writeConfig(JSON.stringify({ telemetryEnabled: false })); + assert.equal(optedOut(), true); +}); + +test("optedOut is false when the shared config enables telemetry", () => { + writeConfig(JSON.stringify({ telemetryEnabled: true })); + assert.equal(optedOut(), false); +}); + +test("optedOut treats an unreadable shared config as no opt-out", () => { + writeConfig("{not json"); + assert.equal(optedOut(), false); +}); + +test("optedOut still honors the env opt-outs", () => { + writeConfig(JSON.stringify({ telemetryEnabled: true })); + process.env.DO_NOT_TRACK = "1"; + try { + assert.equal(optedOut(), true); + } finally { + delete process.env.DO_NOT_TRACK; + } +}); From 649aef15906d1605ccc90a5a9128b298739723ae Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Oct 2026 20:54:32 +0000 Subject: [PATCH 2/2] test(media-use): sandbox HOME in the telemetry opt-out test and regenerate the skills manifest Co-Authored-By: Claude Opus 5.5 (1M context) --- skills-manifest.json | 4 +-- .../media-use/scripts/lib/telemetry.test.mjs | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 4655e215d69..32f60f5910c 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -54,8 +54,8 @@ "files": 1 }, "media-use": { - "hash": "d0875c7f8bc28d51", - "files": 109 + "hash": "216735f7a450bb69", + "files": 110 }, "motion-graphics": { "hash": "6db77031ad839f6a", diff --git a/skills/media-use/scripts/lib/telemetry.test.mjs b/skills/media-use/scripts/lib/telemetry.test.mjs index ea2be0ad6fc..4a0c541ee1c 100644 --- a/skills/media-use/scripts/lib/telemetry.test.mjs +++ b/skills/media-use/scripts/lib/telemetry.test.mjs @@ -1,16 +1,24 @@ import assert from "node:assert/strict"; -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { test } from "node:test"; import { optedOut } from "./telemetry.mjs"; -// optedOut() also reads env vars; pin them to a neutral state for the whole -// file and restore everything (env + shared config) afterwards. -const ENV_KEYS = ["HYPERFRAMES_NO_TELEMETRY", "DO_NOT_TRACK", "CI", "NODE_ENV"]; +// optedOut() reads ~/.hyperframes/config.json and env vars. Point HOME at a temp +// dir so the run never touches the real config, and pin the env opt-outs to a +// neutral state; restore both afterwards. +const ENV_KEYS = [ + "HOME", + "USERPROFILE", + "HYPERFRAMES_NO_TELEMETRY", + "DO_NOT_TRACK", + "CI", + "NODE_ENV", +]; const savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); -const configPath = join(homedir(), ".hyperframes", "config.json"); -const savedConfig = existsSync(configPath) ? readFileSync(configPath, "utf8") : null; +const home = mkdtempSync(join(tmpdir(), "mu-telemetry-")); +const configPath = join(home, ".hyperframes", "config.json"); function writeConfig(contents) { if (contents === null) { @@ -23,6 +31,7 @@ function writeConfig(contents) { test.before(() => { for (const key of ENV_KEYS) delete process.env[key]; + process.env.HOME = process.env.USERPROFILE = home; }); test.after(() => { @@ -30,7 +39,7 @@ test.after(() => { if (savedEnv[key] === undefined) delete process.env[key]; else process.env[key] = savedEnv[key]; } - writeConfig(savedConfig); + rmSync(home, { recursive: true, force: true }); }); test("optedOut is false with no config and no opt-out env", () => {