Add HyperFrames skills

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011h6NB8JQtfPc6eRCefEzBB
This commit is contained in:
Claude committed 2026-10-14 21:51:46 +00:00
1 parent 6bd8165e6d
commit 1b2c8439f0
964 files changed
+142321

No files matched your search

@@ -0,0 +1,176 @@
// Frame packets inline the storyboard frame, blueprint and cited recipes.
// Workflow wrappers supply resource paths and workflow-specific sections.
// The role combines the shared worker contract with the workflow delta.
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { basename, join, resolve } from "node:path";
export function field(block, name) {
const match = block.match(new RegExp(`^-\\s+${name}:\\s*(.+)$`, "im"));
return match?.[1]?.trim() ?? null;
}
export function splitFrames(storyboard) {
const matches = [...storyboard.matchAll(/^## Frame\s+([^\n]+)$/gm)];
return matches.map((match, index) => {
const start = match.index;
const end = matches[index + 1]?.index ?? storyboard.length;
return {
heading: match[1].trim(),
block: storyboard.slice(start, end).trim(),
};
});
}
export function frameId(frame) {
const src = field(frame.block, "src");
if (!src) throw new Error(`${frame.heading}: missing src`);
return basename(src).replace(/\.html?$/i, "");
}
export function selectedFile(path, heading) {
if (!path || !existsSync(path)) return "";
return `\n## ${heading}\n\n${readFileSync(path, "utf8").trim()}\n`;
}
function escapeRegExp(id) {
return id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function knownRuleIds(animationDir) {
const rulesDir = join(animationDir, "rules");
if (!existsSync(rulesDir)) {
console.warn(
`frame-packets: no rules dir at ${rulesDir} — packets will inline no motion recipes`,
);
return [];
}
return readdirSync(rulesDir)
.filter((name) => name.endsWith(".md"))
.map((name) => name.replace(/\.md$/, ""));
}
export function citedRules(block, ruleIds) {
const explicit = (field(block, "rules") ?? "")
.split(/[,\s]+/)
.map((rule) => rule.trim())
.filter(Boolean);
const mentioned = ruleIds.filter((id) =>
new RegExp(`(?<![\\w-])${escapeRegExp(id)}(?![\\w-])`, "i").test(block),
);
return [...new Set([...explicit, ...mentioned])].filter((id) => ruleIds.includes(id));
}
// visual-design.md tells the author to write the blueprint as `<id> (Reproduce)`
// or `<id> (Adapt)` — the qualifier is direction for the frame worker, not part of
// the filename. Parse the field into the id it names (or null for `compose`), so
// no caller ever resolves a raw field value against the blueprints directory.
export function blueprintId(block) {
const raw = field(block, "blueprint");
if (!raw) return null;
const id = raw.replace(/\s*\([^)]*\)\s*$/, "").trim();
return id && id.toLowerCase() !== "compose" ? id : null;
}
export function resourceSections(block, { animationDir, ruleIds, frameId }) {
let sections = "";
const blueprint = blueprintId(block);
if (blueprint) {
const blueprintsDir = join(animationDir, "blueprints");
const path = join(blueprintsDir, `${blueprint}.md`);
// An installed library must contain the named blueprint.
// An absent on-demand library warns, like missing motion rules.
if (!existsSync(blueprintsDir)) {
console.warn(
`frame-packets: no blueprints dir at ${blueprintsDir} — packets will inline no blueprint`,
);
} else if (!existsSync(path)) {
throw new Error(`${frameId ?? "frame"}: blueprint "${blueprint}" has no file at ${path}`);
} else {
sections += selectedFile(path, `Selected blueprint: ${blueprint}`);
}
}
for (const rule of citedRules(block, ruleIds)) {
sections += selectedFile(
join(animationDir, "rules", `${rule}.md`),
`Selected motion rule: ${rule}`,
);
}
return sections;
}
export function buildRolePayload({ corePath, deltaPath, outDir }) {
const core = readFileSync(corePath, "utf8").trim();
const delta = readFileSync(deltaPath, "utf8").trim();
const role = `${core}\n\n---\n\n${delta}\n`;
mkdirSync(outDir, { recursive: true });
const path = join(outDir, "_role.md");
writeFileSync(path, role);
return { path, bytes: Buffer.byteLength(role) };
}
export function buildFramePackets({
projectDir,
storyboardPath = join(projectDir, "STORYBOARD.md"),
outDir = join(projectDir, ".hyperframes", "frame-packets"),
maxPacketBytes = 48_000,
animationDir,
corePath,
deltaPath,
// Per-workflow hooks (all optional):
// designTruthLine(projectDir) -> the packet's design-truth input line
// validateFrame(frame, id) -> throw to reject a frame before packing
// extraSections(block) -> extra packet sections appended after the rule recipes
designTruthLine = (dir) => `- Design tokens: ${join(resolve(dir), "frame.md")}`,
validateFrame,
extraSections,
}) {
const storyboard = readFileSync(storyboardPath, "utf8");
const frames = splitFrames(storyboard);
if (frames.length === 0) throw new Error("STORYBOARD.md has no frame blocks");
const ruleIds = knownRuleIds(animationDir);
const packets = frames.map((frame) => {
const id = frameId(frame);
if (validateFrame) validateFrame(frame, id);
const packet = `# Frame packet: ${id}\n\n## Project inputs\n\n- Project: ${resolve(projectDir)}\n${designTruthLine(projectDir)}\n- RULES_DIR: ${join(animationDir, "rules")}\n\n## Assigned storyboard block\n\n${frame.block}\n${resourceSections(frame.block, { animationDir, ruleIds, frameId: id })}${extraSections ? extraSections(frame.block) : ""}`;
const bytes = Buffer.byteLength(packet);
if (bytes > maxPacketBytes) {
throw new Error(`${id}: frame packet is ${bytes} bytes (limit ${maxPacketBytes})`);
}
return { frameId: id, path: join(outDir, `${id}.md`), bytes, packet };
});
mkdirSync(outDir, { recursive: true });
for (const { path, packet } of packets) writeFileSync(path, packet);
buildRolePayload({ corePath, deltaPath, outDir });
return packets.map(({ packet: _packet, ...result }) => result);
}
export function flag(argv, name, fallback) {
const index = argv.indexOf(`--${name}`);
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
}
export { isMainModule } from "./main-module.mjs";
export function runCli({ buildFramePackets: build, buildRolePayload: buildRole }) {
const argv = process.argv.slice(2);
const projectDir = resolve(flag(argv, "project", "."));
const outDir = resolve(flag(argv, "out-dir", join(projectDir, ".hyperframes", "frame-packets")));
try {
const packets = build({
projectDir,
storyboardPath: resolve(flag(argv, "storyboard", join(projectDir, "STORYBOARD.md"))),
outDir,
});
const role = buildRole({ outDir });
console.log(`✓ frame packets: ${packets.length} bounded packet(s)`);
for (const packet of packets)
console.log(` ${packet.frameId}: ${packet.bytes} bytes → ${packet.path}`);
console.log(` worker role: ${role.bytes} bytes → ${role.path}`);
} catch (error) {
console.error(`✗ frame packets: ${error.message}`);
process.exit(1);
}
}
@@ -0,0 +1,13 @@
import { realpathSync } from "node:fs";
import { pathToFileURL } from "node:url";
// Node resolves symlinks in the main module's import.meta.url but argv[1] keeps the invoked
// spelling (a symlinked skills dir, or /tmp vs /private/tmp on macOS), so compare real paths.
export function isMainModule(importMetaUrl) {
if (!process.argv[1]) return false;
try {
return pathToFileURL(realpathSync(process.argv[1])).href === importMetaUrl;
} catch {
return false;
}
}
@@ -0,0 +1,51 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { dirname, join, relative, resolve } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
const SKILLS = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const CLI_MEDIA_USE = resolve(SKILLS, "../packages/cli/src/media-use");
// Agents install skills by symlinking the skills dir, so each script must still run its CLI.
test("skill CLIs run when invoked through a symlinked skills dir", () => {
const dir = mkdtempSync(join(tmpdir(), "skills-link-"));
try {
symlinkSync(SKILLS, join(dir, "skills"), "dir");
for (const script of [
"faceless-explainer/scripts/captions.mjs",
"pr-to-video/scripts/captions.mjs",
"product-launch-video/scripts/captions.mjs",
"media-use/audio/scripts/heygen-voice.mjs",
]) {
const run = spawnSync(process.execPath, [join(dir, "skills", script), "not-a-command"], {
encoding: "utf8",
});
assert.notEqual(run.status, 0, `${script} exited ${run.status} without running its CLI`);
assert.match(run.stderr, /usage/, script);
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
function skillScripts(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name);
if (entry.isDirectory()) return entry.name === "node_modules" ? [] : skillScripts(path);
return /\.(mjs|js)$/.test(entry.name) && !/\.test\.mjs$/.test(entry.name) ? [path] : [];
});
}
test("skill scripts detect their CLI entry through lib/main-module.mjs only", () => {
const handRolled = [...skillScripts(SKILLS), ...skillScripts(CLI_MEDIA_USE)].filter(
(path) =>
!path.endsWith("main-module.mjs") && readFileSync(path, "utf8").includes("process.argv[1]"),
);
assert.deepEqual(
handRolled.map((path) => relative(SKILLS, path)),
[],
);
});
@@ -0,0 +1,89 @@
#!/usr/bin/env node
// Run from the user's project; locate the release from this installed file.
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { join, win32 } from "node:path";
import { fileURLToPath } from "node:url";
import { isMainModule } from "./lib/main-module.mjs";
const root = fileURLToPath(new URL("../../../", import.meta.url));
const manifests = [
"plugin.json",
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
".cursor-plugin/plugin.json",
"gemini-extension.json",
];
export function pluginVersion(pluginRoot = root) {
for (const path of manifests) {
const file = join(pluginRoot, path);
if (!existsSync(file)) continue;
const manifest = JSON.parse(readFileSync(file, "utf8"));
if (
manifest.name !== "hyperframes" ||
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version ?? "")
) {
throw new Error(`Invalid HyperFrames plugin release: ${file}`);
}
return manifest.version;
}
throw new Error(
"No HyperFrames plugin manifest found. Use the standalone skills installation instructions.",
);
}
export function invocation(
args,
{
version = pluginVersion(),
env = process.env,
platform = process.platform,
node = process.execPath,
pathExists = existsSync,
} = {},
) {
if (args[0] === "skills")
throw new Error(
"Update HyperFrames through your agent's plugin manager; bundled skills are release-managed.",
);
const childEnv = {
...env,
HYPERFRAMES_SKIP_SKILLS: "1",
HYPERFRAMES_SKILL_PKG_VERSION: version,
HYPERFRAMES_PLUGIN_VERSION: version,
HYPERFRAMES_NO_UPDATE_CHECK: "1",
};
if (args[0] === "--script") {
if (!args[1]) throw new Error("--script requires a Node script path.");
return { command: node, args: args.slice(1), env: childEnv };
}
const cliArgs = ["--yes", `hyperframes@${version}`, ...args];
if (platform !== "win32") return { command: "npx", args: cliArgs, env: childEnv };
const candidates = [
env.npm_execpath && win32.join(win32.dirname(env.npm_execpath), "npx-cli.js"),
win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npx-cli.js"),
].filter(Boolean);
const npx = candidates.find(pathExists);
if (!npx)
throw new Error(
"Cannot find npx-cli.js. Install Node.js with npm or run from an npm environment.",
);
return { command: node, args: [npx, ...cliArgs], env: childEnv };
}
if (isMainModule(import.meta.url)) {
try {
const command = invocation(process.argv.slice(2));
const result = spawnSync(command.command, command.args, {
env: command.env,
stdio: "inherit",
windowsHide: true,
});
if (result.error) throw result.error;
process.exitCode = result.status ?? 1;
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
@@ -0,0 +1,121 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { test } from "node:test";
import { invocation, pluginVersion } from "./plugin-cli.mjs";
function fixture(t, manifest = "plugin.json", version = "1.2.3") {
const root = mkdtempSync(join(tmpdir(), "hf-plugin-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(join(root, "skills/hyperframes/scripts/lib"), { recursive: true });
mkdirSync(join(root, manifest, ".."), { recursive: true });
writeFileSync(join(root, manifest), JSON.stringify({ name: "hyperframes", version }));
const launcher = join(root, "skills/hyperframes/scripts/plugin-cli.mjs");
copyFileSync(new URL("./plugin-cli.mjs", import.meta.url), launcher);
copyFileSync(
new URL("./lib/main-module.mjs", import.meta.url),
join(root, "skills/hyperframes/scripts/lib/main-module.mjs"),
);
return { root, launcher };
}
for (const manifest of [
"plugin.json",
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
".cursor-plugin/plugin.json",
"gemini-extension.json",
]) {
test(`locates ${manifest} without repository packages`, (t) => {
const { root } = fixture(t, manifest);
assert.equal(pluginVersion(root), "1.2.3");
});
}
test("rejects missing and floating plugin versions", (t) => {
const { root } = fixture(t, "plugin.json", "latest");
assert.throws(() => pluginVersion(root), /Invalid HyperFrames plugin release/);
rmSync(join(root, "plugin.json"));
assert.throws(() => pluginVersion(root), /No HyperFrames plugin manifest/);
});
test("pins CLI, suppresses standalone refresh, preserves unrelated environment", () => {
const result = invocation(["init", "project with spaces", "--non-interactive"], {
version: "1.2.3",
platform: "darwin",
env: { PATH: "/bin", HYPERFRAMES_SKIP_SKILLS: "0" },
});
assert.equal(result.command, "npx");
assert.deepEqual(result.args, [
"--yes",
"hyperframes@1.2.3",
"init",
"project with spaces",
"--non-interactive",
]);
assert.deepEqual(result.env, {
PATH: "/bin",
HYPERFRAMES_SKIP_SKILLS: "1",
HYPERFRAMES_SKILL_PKG_VERSION: "1.2.3",
HYPERFRAMES_PLUGIN_VERSION: "1.2.3",
HYPERFRAMES_NO_UPDATE_CHECK: "1",
});
assert.throws(() => invocation(["skills", "update"], { version: "1.2.3" }), /plugin manager/);
});
test("Windows launches npx through Node without shell argument interpretation", () => {
const result = invocation(["render", "a & b"], {
version: "1.2.3",
platform: "win32",
node: String.raw`C:\Program Files\nodejs\node.exe`,
env: { npm_execpath: String.raw`C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js` },
pathExists: (p) => p === String.raw`C:\Program Files\nodejs\node_modules\npm\bin\npx-cli.js`,
});
assert.equal(result.command, String.raw`C:\Program Files\nodejs\node.exe`);
assert.deepEqual(result.args, [
String.raw`C:\Program Files\nodejs\node_modules\npm\bin\npx-cli.js`,
"--yes",
"hyperframes@1.2.3",
"render",
"a & b",
]);
assert.throws(
() => invocation([], { version: "1.2.3", platform: "win32", env: {}, pathExists: () => false }),
/Cannot find npx/,
);
});
test("installed helper keeps cwd, arguments, release environment and exit code", (t) => {
const { root, launcher } = fixture(t);
const project = join(root, "user project");
mkdirSync(project);
const helper = join(root, "helper.mjs");
writeFileSync(
helper,
"console.log(JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2), skip: process.env.HYPERFRAMES_SKIP_SKILLS, version: process.env.HYPERFRAMES_SKILL_PKG_VERSION })); process.exit(7);",
);
const before = readFileSync(join(root, "plugin.json"), "utf8");
const result = spawnSync(
process.execPath,
[launcher, "--script", helper, "literal $value & spaces"],
{ cwd: project, encoding: "utf8" },
);
assert.equal(result.status, 7, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), {
cwd: realpathSync(project),
args: ["literal $value & spaces"],
skip: "1",
version: "1.2.3",
});
assert.equal(readFileSync(join(root, "plugin.json"), "utf8"), before);
});