Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1b3d845
feat(cli): move media-use engine into the CLI
miguel-heygen Sep 20, 2026
20c9528
fix(cli): replace media-use compatibility asset
miguel-heygen Sep 20, 2026
601844b
fix(cli): replace moved media-use tree
miguel-heygen Sep 20, 2026
ea3526b
fix(cli): copy moved media-use files cleanly
miguel-heygen Sep 20, 2026
597bc0e
fix(cli): copy media-use engine files individually
miguel-heygen Sep 20, 2026
b060ea8
fix(cli): keep audio helper compatibility import
miguel-heygen Sep 20, 2026
60faba1
fix(cli): clear copied skill destinations
miguel-heygen Sep 20, 2026
680d8c2
test(cli): preserve moved media-use test paths
miguel-heygen Sep 20, 2026
d8d1199
fix(cli): preserve media-use script compatibility imports
miguel-heygen Sep 20, 2026
acbd2e5
fix(cli): preserve media-use telemetry shim
miguel-heygen Sep 20, 2026
f2aaed6
fix(cli): resolve packaged media-use LUT assets
miguel-heygen Sep 20, 2026
d121af6
fix(cli): resolve packaged bundled SFX assets
miguel-heygen Sep 20, 2026
5c02f27
chore(skills): regenerate media-use manifest
miguel-heygen Sep 20, 2026
cbf2e56
fix(cli): preserve media-use parity coverage
miguel-heygen Sep 20, 2026
e621871
fix(cli): cover relocated media-use tests
miguel-heygen Sep 20, 2026
9c6cc8d
chore(skills): regenerate media-use manifest
miguel-heygen Sep 20, 2026
a03fd45
fix(cli): format bundled media provider
miguel-heygen Sep 20, 2026
532da4c
fix(ci): run relocated media-use tests with node
miguel-heygen Sep 20, 2026
a8aca38
fix(ci): preserve fallow script exemptions
miguel-heygen Sep 20, 2026
67f47eb
fix(ci): scope media-use fallow exemption
miguel-heygen Sep 20, 2026
f709ed0
refactor(cli): trim media-use essay comments
miguel-heygen Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
"packages/engine/tests/**",
"skills/**/test-corpus/**",
"skills/**/scripts/**",
// The relocated media-use .mjs files are agent-invoked scripts outside the
// import graph; keep the exemption scoped to that shipped script type.
"packages/cli/src/media-use/**/*.mjs",
// Agent-invoked motion-graphics tools co-located with their docs (run via
// `node <path>` per grounding/PROTOCOL.md / categories/maps/module.md
// prose), not import-graph reachable.
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ jobs:
# `node:` built-in imports. They aren't part of any workspace package, and
# the main `Test` job's `code` path filter excludes `skills/**`, so without
# this dedicated job they'd never run in CI. Examples:
# * skills/media-use/scripts/resolve.test.mjs
# * skills/media-use/scripts/lib/manifest.test.mjs
# * packages/cli/src/media-use/resolve.test.mjs
# * packages/cli/src/media-use/lib/manifest.test.mjs
# Several of these are regression guards (e.g. shell-injection cases), so
# the whole point is that they fire on PRs that touch skills/.
test-skills:
Expand All @@ -390,20 +390,21 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- name: Discover and run skills tests
# We expand the test list via bash so the job fails loudly when the
# matcher comes back empty, rather than silently no-op'ing (which
# would defeat the whole point of this job).
run: |
set -euo pipefail
mapfile -t SKILLS_TESTS < <(find skills -type f -name "*.test.mjs" | sort)
mapfile -t SKILLS_TESTS < <(find skills packages/cli/src/media-use -type f -name "*.test.mjs" | sort)
if [ "${#SKILLS_TESTS[@]}" -eq 0 ]; then
echo "::error::No skills/**/*.test.mjs files found. Did the layout change?"
exit 1
fi
printf 'Running %d skills test file(s):\n' "${#SKILLS_TESTS[@]}"
printf ' * %s\n' "${SKILLS_TESTS[@]}"
node --test "${SKILLS_TESTS[@]}"
bun run test:skills

test-skill-bootstrap-windows:
name: "Test: skill bootstrap (Windows, Node 24)"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"knip": "knip",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts && vitest run scripts/catalog/",
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"test:skills": "node --test 'skills/**/*.test.mjs'",
"test:skills": "node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
"package:codex-plugin": "node scripts/package-codex-plugin.mjs",
Expand Down
20 changes: 17 additions & 3 deletions packages/cli/scripts/build-copy.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Cross-platform replacement for the previous `mkdir -p … && cp -r …` shell
// chain, which failed on Windows because `cp` doesn't accept `-r` there.

import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { setTimeout as sleep } from "node:timers/promises";
Expand Down Expand Up @@ -80,15 +80,29 @@ async function main() {
// Skills bundled into the published CLI. Branches don't all carry the same
// skills/ tree (it gets restructured), so each entry is existsSync-guarded:
// a missing skill dir warns + skips instead of crashing the build.
for (const skill of ["hyperframes", "hyperframes-cli", "gsap"]) {
for (const skill of ["hyperframes", "hyperframes-cli", "gsap", "media-use"]) {
const src = join(REPO_ROOT, "skills", skill);
if (!existsSync(src)) {
console.warn(`[build-copy] skill not found, skipping: skills/${skill}`);
continue;
}
copyDir(src, join(DIST, "skills", skill));
const destination = join(DIST, "skills", skill);
rmSync(destination, { recursive: true, force: true });
copyDir(src, destination);
}

// The media-use engine lives with the CLI source, but keeps its published
// skill-relative layout so the moved .mjs tree can run without a rewrite.
const mediaEngine = join(CLI_ROOT, "src", "media-use");
const publishedMediaLib = join(DIST, "skills", "media-use", "scripts", "lib");
rmSync(publishedMediaLib, { recursive: true, force: true });
mkdirSync(publishedMediaLib, { recursive: true });
copyDirContents(join(mediaEngine, "lib"), publishedMediaLib);
cpSync(
join(mediaEngine, "resolve.mjs"),
join(DIST, "skills", "media-use", "scripts", "resolve.mjs"),
);

const dockerfile = join(CLI_ROOT, "src", "docker", "Dockerfile.render");
if (existsSync(dockerfile)) {
cpSync(dockerfile, join(DIST, "docker", "Dockerfile.render"));
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/audio/scripts
1 change: 1 addition & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const commandLoaders = {
init: () => import("./commands/init.js").then((m) => m.default),
add: () => import("./commands/add.js").then((m) => m.default),
catalog: () => import("./commands/catalog.js").then((m) => m.default),
"media-use": () => import("./commands/media-use.js").then((m) => m.default),
play: () => import("./commands/play.js").then((m) => m.default),
present: () => import("./commands/present.js").then((m) => m.default),
preview: () =>
Expand Down
78 changes: 78 additions & 0 deletions packages/cli/src/commands/media-use.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { defineCommand } from "citty";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { finishCommand } from "../utils/commandResult.js";

const MEDIA_USE_ARGS = {
type: { type: "string" },
intent: { type: "string" },
entity: { type: "string" },
project: { type: "string", alias: "p" },
adopt: { type: "boolean" },
candidates: { type: "boolean" },
doctor: { type: "boolean" },
stats: { type: "boolean" },
days: { type: "string" },
"dry-run": { type: "boolean" },
reuse: { type: "string" },
from: { type: "string" },
params: { type: "string" },
for: { type: "string" },
analyze: { type: "boolean" },
"local-only": { type: "boolean" },
provider: { type: "string" },
"avatar-id": { type: "string" },
"voice-id": { type: "string" },
json: { type: "boolean" },
help: { type: "boolean", alias: "h" },
} as const;

function enginePath(): string {
const here = dirname(fileURLToPath(import.meta.url));
const candidates = [
join(here, "..", "media-use", "resolve.mjs"),
join(here, "skills", "media-use", "scripts", "resolve.mjs"),
join(here, "..", "..", "skills", "media-use", "scripts", "resolve.mjs"),
];
const engine = candidates.find((candidate) => existsSync(candidate));
if (!engine) throw new Error("media-use engine is missing from this CLI build");
return engine;
}

function invokeEngine(verb: string): never {
const commandIndex = process.argv.indexOf("media-use");
const verbIndex = commandIndex + 1;
const passed = process.argv.slice(verbIndex + 1);
const flag = verb === "resolve" ? [] : [`--${verb}`];
const result = spawnSync(process.execPath, [enginePath(), ...flag, ...passed], {
stdio: "inherit",
});
if (result.error) throw result.error;
finishCommand(result.status ?? 1);
}

function subcommand(name: string) {
return defineCommand({
meta: { name, description: `media-use ${name}` },
args: MEDIA_USE_ARGS,
run: () => invokeEngine(name),
});
}

export default defineCommand({
meta: { name: "media-use", description: "Resolve and operate on project media" },
subCommands: {
resolve: () => subcommand("resolve"),
doctor: () => subcommand("doctor"),
stats: () => subcommand("stats"),
adopt: () => subcommand("adopt"),
candidates: () => subcommand("candidates"),
reuse: () => subcommand("reuse"),
from: () => subcommand("from"),
params: () => subcommand("params"),
analyze: () => subcommand("analyze"),
},
run: () => console.log("Run `hyperframes media-use <resolve|doctor|stats|...> --help`"),
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ import { extname, join } from "node:path";

const LIB_DIR =
process.env.HYPERFRAMES_MEDIA_USE_SFX_DIR ||
[
join(import.meta.dirname, "..", "..", "audio", "assets", "sfx"),
join(
import.meta.dirname,
"..",
"..",
"..",
"..",
"..",
"skills",
"media-use",
"audio",
"assets",
"sfx",
),
].find((candidate) => existsSync(candidate)) ||
join(import.meta.dirname, "..", "..", "audio", "assets", "sfx");

export const BUNDLED_SFX_RECOVERY_COMMAND = "npx hyperframes skills update media-use";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ import { CAPABILITIES, listModels } from "./local-models.mjs";
// test enforces the weakness→owner matrix in references/meta.md so a claim can't rot — if
// a capability's entrypoint disappears, this fails.

const SKILL = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const SKILL = join(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"..",
"..",
"..",
"skills",
"media-use",
);

test("weakness: audio-only → media-use resolves image + icon", () => {
for (const t of ["image", "icon"]) {
Expand Down Expand Up @@ -43,7 +52,10 @@ test("weakness: no media-ops → ops guidance reference exists", () => {

test("weakness: no transcript-driven cutting → cut compiler entrypoints exist", async () => {
assert.ok(existsSync(join(SKILL, "scripts", "transcript-cut.mjs")), "transcript-cut missing");
assert.ok(existsSync(join(SKILL, "scripts", "lib", "cutlist.mjs")), "cutlist lib missing");
assert.ok(
existsSync(join(dirname(fileURLToPath(import.meta.url)), "cutlist.mjs")),
"cutlist lib missing",
);
const cutlist = await import("./cutlist.mjs");
assert.equal(typeof cutlist.compileCutList, "function");
});
Expand All @@ -60,7 +72,10 @@ test("weakness: whisper.cpp is weak → better local ASR (Parakeet) entrypoint e

test("weakness: no auto-duck/loudness → duck compiler and recipes exist", async () => {
assert.ok(existsSync(join(SKILL, "scripts", "audio-duck.mjs")), "audio-duck missing");
assert.ok(existsSync(join(SKILL, "scripts", "lib", "duck.mjs")), "duck lib missing");
assert.ok(
existsSync(join(dirname(fileURLToPath(import.meta.url)), "duck.mjs")),
"duck lib missing",
);
assert.ok(existsSync(join(SKILL, "references", "operations.md")), "operations.md missing");
const duck = await import("./duck.mjs");
assert.equal(typeof duck.speechSpans, "function");
Expand Down
Loading
Loading