Files
hyperframes/scripts/check-core-skills.mjs
WaterrrForeverandMiguel Angel Simon Sierra f2358315e5 feat(skills): re-land reverse any video on the HyperFrames plan, with clearer credit wording (#6833)
* fix(cli,skills): say a step's credits are its total, and keep cost out of retries

An out-of-credits refusal named what the step needs, and an agent read
that as the shortfall. It now says the number is the step's total.

After its last try, an agent told the person what was spent and what
another try would cost. The HeyGen reference now says not to.

* fix(cli): plan avatar credits from the HyperFrames price rows

An avatar's credits came from its old API price, about five times what
the plan charges, so --max-credits refused calls that fit and a top-up
asked for too much. They now use the Avatar IV photo row plus the
general avatar video row, 950 credits a minute.

* fix(cli): pin the avatar price test and keep pricing under the comment share

* fix(cli): plan avatar seconds on the Avatar IV row alone

The general avatar row is only checked at admission and billed on
seconds left after Avatar IV, so adding it to every second made a 15 s
script plan at 238 instead of 200 and a 220 limit refuse a call that fits.

* fix(cli): the credits link opens the web plan page, which is live

* test(cli): pin the production credits link too

* chore(skills): regenerate the skills manifest after merging main

* docs(cli): keep the person measure's doc comments to four lines

* docs(cli): keep the measure doc comments to four lines

* docs(cli): keep the measure comment lines to 120 characters

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-31 00:47:54 +00:00

101 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
// Holds the core skills to their size budgets and checks what their entry files name: every
// `npx hyperframes <command>` against the commands packages/cli/src/cli.ts registers, and every
// path in backticks against the skill folder it is relative to.
import { existsSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
// Every installed skill's description is listed on every turn; an entry is read in full when it opens.
export const DESCRIPTION_MAX = 320;
export const CORE_SKILLS = {
hyperframes: 7168,
"hyperframes-cli": 7168,
"hyperframes-registry": 5120,
"media-use": 5120,
figma: 7168,
slideshow: 5120,
"remotion-to-hyperframes": 5120,
"reverse-any-video": 5120,
};
/** @param {string} skill */
const skillFile = (skill, root = REPO_ROOT) => join(root, "skills", skill, "SKILL.md");
/** @param {string} content */
export function descriptionOf(content) {
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(content)?.[1] ?? "";
return /^description:[ \t]*(.*)$/m.exec(frontmatter)?.[1].trim() ?? "";
}
/** The top-level command names the CLI registers. @param {string} cliSource */
export function registeredCommands(cliSource) {
const block = /const commandLoaders = \{([\s\S]*?)\n\};/.exec(cliSource)?.[1] ?? "";
return new Set([...block.matchAll(/^\s+"?([a-z][a-z-]*)"?:/gm)].map((m) => m[1]));
}
/** @param {string} content @param {Set<string>} commands */
export function unknownCommands(content, commands) {
const named = [...content.matchAll(/npx hyperframes(?:@[\w.]+)? ([a-z][a-z-]*)/g)].map(
(m) => m[1],
);
return [...new Set(named)].filter((name) => !commands.has(name));
}
// A backtick span that is a relative path to a file this skill ships, such as one under references/.
const PATH_SPAN = /`((?:\.\.\/)*[a-z0-9][\w.-]*(?:\/[\w.-]+)+\.(?:md|html|json))(?:#[^`]*)?`/g;
/** @param {string} content @param {string} skillDir */
export function missingPaths(content, skillDir) {
const paths = [...content.matchAll(PATH_SPAN)].map((m) => m[1]);
return [...new Set(paths)].filter((p) => !p.includes("<") && !existsSync(join(skillDir, p)));
}
/** Size problems: the entry over its budget, the description over the per-skill cap. */
function sizeProblems(skill, content, maxBytes) {
const bytes = Buffer.byteLength(content);
const description = Buffer.byteLength(descriptionOf(content));
return [
...(maxBytes !== null && bytes > maxBytes
? [`${skill}/SKILL.md is ${bytes} B, over its ${maxBytes} B budget`]
: []),
...(description > DESCRIPTION_MAX
? [`${skill} description is ${description} B, over ${DESCRIPTION_MAX} B`]
: []),
];
}
/** Names the entry teaches that do not exist: CLI commands and shipped paths. */
function nameProblems(skill, content, skillDir, commands) {
return [
...unknownCommands(content, commands).map(
(name) =>
`${skill}/SKILL.md names \`npx hyperframes ${name}\`, which the CLI does not register`,
),
...missingPaths(content, skillDir).map(
(path) => `${skill}/SKILL.md points at \`${path}\`, which does not exist`,
),
];
}
/** @param {string} root */
export function checkCoreSkills(root = REPO_ROOT) {
const commands = registeredCommands(readFileSync(join(root, "packages/cli/src/cli.ts"), "utf8"));
return Object.entries(CORE_SKILLS).flatMap(([skill, maxBytes]) => {
const file = skillFile(skill, root);
const content = readFileSync(file, "utf8");
const names = maxBytes === null ? [] : nameProblems(skill, content, dirname(file), commands);
return [...sizeProblems(skill, content, maxBytes), ...names];
});
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const problems = checkCoreSkills();
for (const problem of problems) console.error(`✗ ${problem}`);
if (problems.length > 0) process.exit(1);
console.log(`✓ core skills within budget; commands and paths resolve`);
}