Files
hyperframes/skills/media-use/scripts/lib/cache.mjs
T
Miguel ÁngelandClaude Opus 4.8 73f6d3e5be feat(media-use): core infrastructure — manifest, cache, adopt, probe (#1682)
* feat(media-use): core infrastructure — manifest, cache, adopt, probe

Foundation for media-use — the media resolution layer for HyperFrames.

- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)

* fix(media-use): oxfmt formatting + cap freeze download size

Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:28:38 -04:00

115 lines
3.2 KiB
JavaScript

import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
import { join, basename } from "node:path";
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { readManifest, appendRecord } from "./manifest.mjs";
const SCHEMA_PREFIX = "mu-v1-";
const KEY_HEX_CHARS = 16;
const COMPLETE_SENTINEL = ".hf-complete";
export function globalMediaDir() {
return join(homedir(), ".media");
}
export function contentHash(filePath) {
const bytes = readFileSync(filePath);
return createHash("sha256").update(bytes).digest("hex");
}
function cacheEntryDir(rootDir, sha) {
return join(rootDir, SCHEMA_PREFIX + sha.slice(0, KEY_HEX_CHARS));
}
function isComplete(entryDir) {
return existsSync(join(entryDir, COMPLETE_SENTINEL));
}
function markComplete(entryDir) {
writeFileSync(join(entryDir, COMPLETE_SENTINEL), "", "utf8");
}
function readGlobalManifest() {
return readManifest(globalMediaDir());
}
function validateCacheHit(match) {
if (!match?.sha) return null;
return isComplete(cacheEntryDir(globalMediaDir(), match.sha)) ? match : null;
}
export function cacheGet(prompt, type) {
return validateCacheHit(
readGlobalManifest().find(
(r) => r.reusable && r.provenance?.prompt === prompt && (type == null || r.type === type),
),
);
}
export function cacheGetByEntity(entity) {
const lower = entity.toLowerCase();
return validateCacheHit(
readGlobalManifest().find((r) => r.reusable && r.entity && r.entity.toLowerCase() === lower),
);
}
export function cachePut(filePath, record) {
const sha = contentHash(filePath);
const dir = globalMediaDir();
const entryDir = cacheEntryDir(dir, sha);
mkdirSync(entryDir, { recursive: true });
const dest = join(entryDir, basename(filePath));
copyFileSync(filePath, dest);
markComplete(entryDir);
const globalRecord = {
...record,
sha,
reusable: true,
cached_path: dest,
};
appendRecord(globalMediaDir(), globalRecord);
return { sha, cached_path: dest };
}
export function importFromCache(cacheRecord, projectDir, localId, localPath) {
const sha = cacheRecord.sha;
const entryDir = cacheEntryDir(globalMediaDir(), sha);
if (!isComplete(entryDir)) return null;
const cachedFile = cacheRecord.cached_path;
if (!cachedFile || !existsSync(cachedFile)) return null;
mkdirSync(join(projectDir, ".media"), { recursive: true });
const fullDest = join(projectDir, localPath);
mkdirSync(join(fullDest, ".."), { recursive: true });
copyFileSync(cachedFile, fullDest);
const projectRecord = {
...cacheRecord,
id: localId,
path: localPath,
provenance: {
...cacheRecord.provenance,
imported_from: sha,
},
};
delete projectRecord.sha;
delete projectRecord.reusable;
delete projectRecord.cached_path;
return projectRecord;
}
export function promote(projectDir, id) {
const records = readManifest(projectDir);
const record = records.find((r) => r.id === id);
if (!record) throw new Error(`asset not found in project manifest: ${id}`);
const filePath = join(projectDir, record.path);
if (!existsSync(filePath)) throw new Error(`asset file not found: ${filePath}`);
return cachePut(filePath, record);
}