Files
PaperClipAI/server/src/services/skill-repository-package.ts
T
86c965ce7a fix(skills): expire unused version caches and restore saved contents (#16433)
Expire idle skill-version caches with durable runtime pins, bounded discovery, conservative retention, and database-backed restoration.

Co-Authored-By: Dotta <cryppadotta@users.noreply.github.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-15 12:20:57 -05:00

54 lines
3.8 KiB
TypeScript

import path from 'node:path';
import type { CompanySkillVersionFileInventoryEntry, SkillRepositoryPackage } from '@paperclipai/shared';
import { z } from 'zod';
import { assertSkillSnapshotPath, skillFileBytes } from './skill-snapshot.js';
export const REPOSITORY_CONTENT_DIR = '.paperclip-repository';
export const MAX_REPOSITORY_FILES = 9_990;
export const MAX_REPOSITORY_BYTES = 61 * 1024 * 1024;
export function repositorySkillFrontmatter(content: string): string | undefined {
return content.match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/)?.[0];
}
const manifestSchema = z.object({
version: z.literal(1),
skills: z.array(z.string().min(1).max(4096)).min(1).max(1000),
requirements: z.string().max(4000).optional(),
}).strict();
/** Declarative metadata only: importing never invokes a repository's installer. */
export function inspectRepositoryManifest(files: CompanySkillVersionFileInventoryEntry[], skillPaths: string[]) {
const file = files.find(file => file.path === 'paperclip.skills.json');
if (!file) return {};
const manifest = manifestSchema.parse(JSON.parse(skillFileBytes(file).toString('utf8')));
const declared = [...new Set(manifest.skills.map(assertSkillSnapshotPath))];
if (declared.some(value => !skillPaths.includes(value))) throw new Error('Manifest skills must name existing SKILL.md entrypoints.');
return { skillPaths: declared, requirements: manifest.requirements ?? null };
}
export function repositoryPackageInspection(files: CompanySkillVersionFileInventoryEntry[], skillPaths: string[], warnings: string[], error: string | null): SkillRepositoryPackage {
let manifest: ReturnType<typeof inspectRepositoryManifest> = {};
try { manifest = inspectRepositoryManifest(files, skillPaths); }
catch { warnings = [...warnings, 'paperclip.skills.json — Invalid manifest; skills were discovered automatically.']; }
return { files: files.map(file => ({ path: file.path, kind: file.kind, sizeBytes: skillFileBytes(file).length,
encoding: file.encoding ?? 'utf8', executable: file.executable ?? false })), requirements: null, ...manifest, error, warnings };
}
/** A portable discovery entrypoint directs the agent to the untouched original.
* The full tree travels inside the skill on every existing adapter and sandbox
* transport. Never rewrite authored relative links or use host-only symlinks.
*/
export function repositorySkillProjection(files: CompanySkillVersionFileInventoryEntry[], skillPath: string): CompanySkillVersionFileInventoryEntry[] {
assertSkillSnapshotPath(skillPath);
const skill = files.find(file => file.path === skillPath);
if (!skill || skill.encoding === 'base64') throw new Error('Repository skill entrypoint is missing or not text.');
const frontmatter = repositorySkillFrontmatter(skill.content);
if (!frontmatter) throw new Error('Repository skill entrypoint has no frontmatter.');
const target = `${REPOSITORY_CONTENT_DIR}/${skillPath}`;
const wrapper = `${frontmatter.trimEnd()}\n\nRead and follow the complete skill instructions in [${skillPath}](${target.split('/').map(encodeURIComponent).join('/')}). Resolve the original skill's relative file references from its directory inside \`${REPOSITORY_CONTENT_DIR}/\`. That directory contains the included pinned repository files, including supporting skills and shared files. Repository-root paths are relative to that directory.\n`;
// Host-specific discovery policy belongs beside the generated entrypoint too.
const policyPath = path.posix.join(path.posix.dirname(skillPath), 'agents/openai.yaml');
return [{ path: 'SKILL.md', kind: 'skill', content: wrapper },
...files.filter(file => file.path === policyPath).map(file => ({ ...file, path: 'agents/openai.yaml' })),
...files.map(file => ({ ...file, path: `${REPOSITORY_CONTENT_DIR}/${assertSkillSnapshotPath(file.path)}` }))];
}