diff --git a/README.md b/README.md index 9ff11d3..66bbd69 100644 --- a/README.md +++ b/README.md @@ -448,3 +448,6 @@ Contributions to documentation, the MCP server, and exploration tooling are welc ## Disclaimer This repository archives a source snapshot reportedly exposed via Anthropic's npm distribution on **2026-03-31**. It is provided for research, documentation, and exploratory tooling around the snapshot. The original Claude Code source remains the property of [Anthropic](https://www.anthropic.com), this is not an official release, and no rights to Anthropic's original code are granted by this repository. If you choose to use or redistribute any of the archived material, you are responsible for assessing the legal implications yourself. Contact [nichxbt](https://www.x.com/nichxbt) for any comments. + +## Red Cross Remo CLI +This repo now includes a Remotion-first multi-agent scaffold branded as **Red Cross Remo** with coordinator, skills, plugins, MCP tools, and prompt packs for video generation. diff --git a/mcp-server/index.ts b/mcp-server/index.ts new file mode 100644 index 0000000..e9fd02a --- /dev/null +++ b/mcp-server/index.ts @@ -0,0 +1,10 @@ +import { z } from 'zod/v4' + +export const mcpToolSchema = z.object({ name: z.string(), description: z.string(), endpoint: z.string().url() }) +export type McpTool = z.infer + +export const builtInMcpTools: McpTool[] = [ + { name: 'stock-footage-search', description: 'Search stock media providers', endpoint: 'https://mcp.video.local/stock' }, + { name: 'audio-library', description: 'Discover royalty free music and fx', endpoint: 'https://mcp.video.local/audio' }, + { name: 'caption-generator', description: 'Generate subtitles and captions', endpoint: 'https://mcp.video.local/captions' }, +] diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index b49f61c..1915de6 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -1,24 +1,8 @@ -#!/usr/bin/env node -/** - * STDIO entrypoint — for local use with Claude Desktop, Claude Code, etc. - * - * Usage: - * node dist/index.js - * CLAUDE_CODE_SRC_ROOT=/path/to/src node dist/index.js - */ - -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createServer, validateSrcRoot, SRC_ROOT } from "./server.js"; - -async function main() { - await validateSrcRoot(); - const server = createServer(); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`Claude Code Explorer MCP (stdio) started — src: ${SRC_ROOT}`); -} - -main().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); -}); +export * from './tools/list-compositions.js' +export * from './tools/read-composition.js' +export * from './tools/write-composition.js' +export * from './tools/list-assets.js' +export * from './tools/get-render-status.js' +export * from './tools/search-pexels.js' +export * from './tools/run-render.js' +export * from './tools/get-project-config.js' diff --git a/mcp-server/src/tools/get-project-config.ts b/mcp-server/src/tools/get-project-config.ts new file mode 100644 index 0000000..9e17736 --- /dev/null +++ b/mcp-server/src/tools/get-project-config.ts @@ -0,0 +1 @@ +export const get_project_config = { name: 'get-project-config', description: 'get-project-config MCP tool' } diff --git a/mcp-server/src/tools/get-render-status.ts b/mcp-server/src/tools/get-render-status.ts new file mode 100644 index 0000000..05d3109 --- /dev/null +++ b/mcp-server/src/tools/get-render-status.ts @@ -0,0 +1 @@ +export const get_render_status = { name: 'get-render-status', description: 'get-render-status MCP tool' } diff --git a/mcp-server/src/tools/list-assets.ts b/mcp-server/src/tools/list-assets.ts new file mode 100644 index 0000000..1c91f29 --- /dev/null +++ b/mcp-server/src/tools/list-assets.ts @@ -0,0 +1 @@ +export const list_assets = { name: 'list-assets', description: 'list-assets MCP tool' } diff --git a/mcp-server/src/tools/list-compositions.ts b/mcp-server/src/tools/list-compositions.ts new file mode 100644 index 0000000..4824c50 --- /dev/null +++ b/mcp-server/src/tools/list-compositions.ts @@ -0,0 +1 @@ +export const list_compositions = { name: 'list-compositions', description: 'list-compositions MCP tool' } diff --git a/mcp-server/src/tools/read-composition.ts b/mcp-server/src/tools/read-composition.ts new file mode 100644 index 0000000..c16e1f4 --- /dev/null +++ b/mcp-server/src/tools/read-composition.ts @@ -0,0 +1 @@ +export const read_composition = { name: 'read-composition', description: 'read-composition MCP tool' } diff --git a/mcp-server/src/tools/run-render.ts b/mcp-server/src/tools/run-render.ts new file mode 100644 index 0000000..0ad8d41 --- /dev/null +++ b/mcp-server/src/tools/run-render.ts @@ -0,0 +1 @@ +export const run_render = { name: 'run-render', description: 'run-render MCP tool' } diff --git a/mcp-server/src/tools/search-pexels.ts b/mcp-server/src/tools/search-pexels.ts new file mode 100644 index 0000000..95b48a6 --- /dev/null +++ b/mcp-server/src/tools/search-pexels.ts @@ -0,0 +1 @@ +export const search_pexels = { name: 'search-pexels', description: 'search-pexels MCP tool' } diff --git a/mcp-server/src/tools/write-composition.ts b/mcp-server/src/tools/write-composition.ts new file mode 100644 index 0000000..e4496d4 --- /dev/null +++ b/mcp-server/src/tools/write-composition.ts @@ -0,0 +1 @@ +export const write_composition = { name: 'write-composition', description: 'write-composition MCP tool' } diff --git a/mcp.config.json b/mcp.config.json new file mode 100644 index 0000000..159a3d1 --- /dev/null +++ b/mcp.config.json @@ -0,0 +1,11 @@ +{ + "servers": { + "remotion-video": { + "type": "stdio", + "command": "node", + "args": ["./mcp-server/dist/index.js"], + "description": "Built-in Remotion video MCP server" + } + }, + "userServers": {} +} diff --git a/prompts/animation-helper.md b/prompts/animation-helper.md new file mode 100644 index 0000000..d11a543 --- /dev/null +++ b/prompts/animation-helper.md @@ -0,0 +1,4 @@ +Role: Animation helper for Remotion timing. +Output: short JSON snippets with interpolate/spring configs. +Variables: {{animation_goal}}, {{duration_frames}}, {{easing_style}}. +Edge case: if duration too small, return conservative defaults. diff --git a/prompts/asset-suggester.md b/prompts/asset-suggester.md new file mode 100644 index 0000000..437399d --- /dev/null +++ b/prompts/asset-suggester.md @@ -0,0 +1,4 @@ +Role: Asset query suggester for Pexels. +Output: JSON array of search queries per scene. +Variables: {{scene_prompt}}, {{asset_type}}, {{orientation}}, {{quality}}. +Include attribution reminder for every selected result. diff --git a/prompts/composition-generator.md b/prompts/composition-generator.md new file mode 100644 index 0000000..21a7c5d --- /dev/null +++ b/prompts/composition-generator.md @@ -0,0 +1,5 @@ +Role: Remotion TypeScript composition generator. +Return ONLY complete .tsx code. +Must use: useCurrentFrame(), useVideoConfig(), interpolate(), spring(), , . +Variables: {{name}}, {{prompt}}, {{durationInSeconds}}, {{fps}}, {{width}}, {{height}}. +Example Output: valid TSX component exported for Remotion Root registration. diff --git a/prompts/coordinator-orchestrator.md b/prompts/coordinator-orchestrator.md new file mode 100644 index 0000000..fe9ded3 --- /dev/null +++ b/prompts/coordinator-orchestrator.md @@ -0,0 +1,4 @@ +Role: Multi-agent coordinator policy. +Output: task graph JSON with dependencies and parallel groups. +Variables: {{scene_plan}}, {{max_parallel_agents}}, {{enabled_skills}}, {{enabled_plugins}}. +Edge cases: agent timeout, retryOnFailure, fallback scene regeneration. diff --git a/prompts/skill-builder.md b/prompts/skill-builder.md new file mode 100644 index 0000000..6dd4530 --- /dev/null +++ b/prompts/skill-builder.md @@ -0,0 +1,4 @@ +Role: Skill code generator. +Output: a single .skill.ts file string only. +Variables: {{skill_name}}, {{skill_description}}, {{input_schema}}, {{behavior_notes}}. +Must include zod inputs, description, version, and execute() with JSDoc. diff --git a/prompts/storyboard.md b/prompts/storyboard.md new file mode 100644 index 0000000..0741d67 --- /dev/null +++ b/prompts/storyboard.md @@ -0,0 +1,4 @@ +Role: Storyboard planner. +Output strictly JSON: {"scenes":[{"id":"...","prompt":"...","durationSeconds":5}]} +Variables: {{video_idea}}, {{target_duration_seconds}}, {{tone}}, {{platform}}. +Example: 30s video -> six 5s scenes with transition hints. diff --git a/prompts/system-prompt.md b/prompts/system-prompt.md new file mode 100644 index 0000000..f6cdd7c --- /dev/null +++ b/prompts/system-prompt.md @@ -0,0 +1,6 @@ +Role: You are Red Cross Remo, an AI video-creation CLI assistant. +Output: concise operator steps + tool call intents. +Variables: {{user_request}}, {{project_context}}, {{available_tools}}. +Example Input: {{user_request}}="launch teaser". +Example Output: plan JSON with storyboard->assets->composition->review->render. +Edge cases: missing API keys, empty project, render errors. diff --git a/src/commands.ts b/src/commands.ts index 10f03b2..5b4a17b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -185,6 +185,7 @@ import rateLimitOptions from './commands/rate-limit-options/index.js' import statusline from './commands/statusline.js' import effort from './commands/effort/index.js' import stats from './commands/stats/index.js' +import remotionCommands from './commands/remotion/index.js' // insights.ts is 113KB (3200 lines, includes diffLines/html rendering). Lazy // shim defers the heavy module until /insights is actually invoked. const usageReport: Command = { diff --git a/src/commands/remotion/composition.ts b/src/commands/remotion/composition.ts new file mode 100644 index 0000000..4b5a176 --- /dev/null +++ b/src/commands/remotion/composition.ts @@ -0,0 +1,2 @@ +import type { Command } from '../../types/command.js' +export default { type:'local-jsx', name:'composition', description:'Create/list compositions', userFacingName:()=>'/composition', isEnabled:()=>true, async call(){ return null } } satisfies Command diff --git a/src/commands/remotion/export.ts b/src/commands/remotion/export.ts new file mode 100644 index 0000000..03722a7 --- /dev/null +++ b/src/commands/remotion/export.ts @@ -0,0 +1,2 @@ +import type { Command } from '../../types/command.js' +export default { type:'local-jsx', name:'video-export', description:'Export video with preset', userFacingName:()=>'/export', isEnabled:()=>true, async call(){ return null } } satisfies Command diff --git a/src/commands/remotion/index.ts b/src/commands/remotion/index.ts new file mode 100644 index 0000000..e31d799 --- /dev/null +++ b/src/commands/remotion/index.ts @@ -0,0 +1,7 @@ +import composition from './composition.js' +import preview from './preview.js' +import render from './render.js' +import storyboard from './storyboard.js' +import videoExport from './export.js' + +export default [render, preview, composition, videoExport, storyboard] diff --git a/src/commands/remotion/preview.ts b/src/commands/remotion/preview.ts new file mode 100644 index 0000000..8acd2f4 --- /dev/null +++ b/src/commands/remotion/preview.ts @@ -0,0 +1,2 @@ +import type { Command } from '../../types/command.js' +export default { type:'local-jsx', name:'preview', description:'Preview Remotion Studio', userFacingName:()=>'/preview', isEnabled:()=>true, async call(){ return null } } satisfies Command diff --git a/src/commands/remotion/render.ts b/src/commands/remotion/render.ts new file mode 100644 index 0000000..a1d018b --- /dev/null +++ b/src/commands/remotion/render.ts @@ -0,0 +1,2 @@ +import type { Command } from '../../types/command.js' +export default { type:'local-jsx', name:'render', description:'Render a Remotion video', userFacingName:()=>'/render', isEnabled:()=>true, async call(){ return null } } satisfies Command diff --git a/src/commands/remotion/storyboard.ts b/src/commands/remotion/storyboard.ts new file mode 100644 index 0000000..689efd5 --- /dev/null +++ b/src/commands/remotion/storyboard.ts @@ -0,0 +1,2 @@ +import type { Command } from '../../types/command.js' +export default { type:'local-jsx', name:'storyboard', description:'Run storyboard coordinator workflow', userFacingName:()=>'/storyboard', isEnabled:()=>true, async call(){ return null } } satisfies Command diff --git a/src/coordinator/AgentStatusUI.tsx b/src/coordinator/AgentStatusUI.tsx new file mode 100644 index 0000000..da0cab9 --- /dev/null +++ b/src/coordinator/AgentStatusUI.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import { Box, Text } from 'ink' +import type { AgentStatus } from './agents/types.js' + +export function AgentStatusUI({ statuses }: { statuses: AgentStatus[] }) { + return ✚ Red Cross Remo — Agent Activity{statuses.map(s => {s.emoji} {s.name}: {s.task})} +} diff --git a/src/coordinator/agents/AssetAgent.ts b/src/coordinator/agents/AssetAgent.ts new file mode 100644 index 0000000..e976698 --- /dev/null +++ b/src/coordinator/agents/AssetAgent.ts @@ -0,0 +1,2 @@ +import type { Scene } from './types.js' +export class AssetAgent { description = 'Searches/downloads Pexels assets.'; /** Run asset search for a scene. */ async run(scene: Scene) { return { sceneId: scene.id, assets: [] as string[] } } } diff --git a/src/coordinator/agents/CompositionAgent.ts b/src/coordinator/agents/CompositionAgent.ts new file mode 100644 index 0000000..d93847b --- /dev/null +++ b/src/coordinator/agents/CompositionAgent.ts @@ -0,0 +1,2 @@ +import type { Scene } from './types.js' +export class CompositionAgent { description = 'Generates Remotion TSX scenes.'; /** Run composition generation for a scene. */ async run(scene: Scene) { return { sceneId: scene.id, file: `${scene.id}.tsx` } } } diff --git a/src/coordinator/agents/CoordinatorAgent.ts b/src/coordinator/agents/CoordinatorAgent.ts new file mode 100644 index 0000000..e693daa --- /dev/null +++ b/src/coordinator/agents/CoordinatorAgent.ts @@ -0,0 +1,16 @@ +import { AssetAgent } from './AssetAgent.js' +import { CompositionAgent } from './CompositionAgent.js' +import { RenderAgent } from './RenderAgent.js' +import { ReviewAgent } from './ReviewAgent.js' +import { StoryboardAgent } from './StoryboardAgent.js' + +export class CoordinatorAgent { + description = 'Coordinates storyboard, assets, composition, review, and render agents.' + /** Run the full multi-agent video workflow. */ + async run(prompt: string) { + const storyboard = await new StoryboardAgent().run(prompt) + const pairs = await Promise.all(storyboard.map(async scene => ({ assets: await new AssetAgent().run(scene), composition: await new CompositionAgent().run(scene) }))) + await new ReviewAgent().run(pairs) + return new RenderAgent().run(pairs) + } +} diff --git a/src/coordinator/agents/RenderAgent.ts b/src/coordinator/agents/RenderAgent.ts new file mode 100644 index 0000000..6b0ed94 --- /dev/null +++ b/src/coordinator/agents/RenderAgent.ts @@ -0,0 +1 @@ +export class RenderAgent { description = 'Assembles and renders final video.'; /** Run render queue execution for approved scenes. */ async run(_input: unknown) { return { outputPath: './out/final.mp4' } } } diff --git a/src/coordinator/agents/ReviewAgent.ts b/src/coordinator/agents/ReviewAgent.ts new file mode 100644 index 0000000..696f201 --- /dev/null +++ b/src/coordinator/agents/ReviewAgent.ts @@ -0,0 +1 @@ +export class ReviewAgent { description = 'Validates generated compositions.'; /** Run validation for generated scene outputs. */ async run(_input: unknown) { return { ok: true } } } diff --git a/src/coordinator/agents/StoryboardAgent.ts b/src/coordinator/agents/StoryboardAgent.ts new file mode 100644 index 0000000..d06be89 --- /dev/null +++ b/src/coordinator/agents/StoryboardAgent.ts @@ -0,0 +1,2 @@ +import type { Scene } from './types.js' +export class StoryboardAgent { description = 'Plans scenes, durations, and transitions.'; /** Run storyboard planning. */ async run(request: string): Promise { return [{ id: 'scene-1', prompt: request, durationSeconds: 5 }] } } diff --git a/src/coordinator/agents/types.ts b/src/coordinator/agents/types.ts new file mode 100644 index 0000000..01c84ae --- /dev/null +++ b/src/coordinator/agents/types.ts @@ -0,0 +1,2 @@ +export type Scene = { id: string; prompt: string; durationSeconds: number } +export type AgentStatus = { name: string; task: string; emoji: string } diff --git a/src/coordinator/coordinator.config.ts b/src/coordinator/coordinator.config.ts new file mode 100644 index 0000000..e9756c6 --- /dev/null +++ b/src/coordinator/coordinator.config.ts @@ -0,0 +1,7 @@ +export const coordinatorConfig = { + maxParallelAgents: 3, + agentTimeout: 60000, + retryOnFailure: true, + maxRetries: 2, + showAgentLogs: true, +} diff --git a/src/coordinator/index.ts b/src/coordinator/index.ts new file mode 100644 index 0000000..2f83f37 --- /dev/null +++ b/src/coordinator/index.ts @@ -0,0 +1,6 @@ +export * from './agents/CoordinatorAgent.js' +export * from './agents/StoryboardAgent.js' +export * from './agents/AssetAgent.js' +export * from './agents/CompositionAgent.js' +export * from './agents/ReviewAgent.js' +export * from './agents/RenderAgent.js' diff --git a/src/plugins/built-in/aspect-ratio.plugin.ts b/src/plugins/built-in/aspect-ratio.plugin.ts new file mode 100644 index 0000000..539d3b7 --- /dev/null +++ b/src/plugins/built-in/aspect-ratio.plugin.ts @@ -0,0 +1,18 @@ +import type { VideoPlugin } from '../index.js' + +export const AspectRatioPlugin: VideoPlugin = { + name: 'aspect-ratio', + description: 'aspect-ratio plugin for Red Cross Remo CLI', + version: '1.0.0', + enabled: true, + hooks: { + /** Run aspect-ratio startup checks. */ + onStartup: async () => {}, + /** Run aspect-ratio before render hook. */ + beforeRender: async () => {}, + /** Run aspect-ratio after render hook. */ + afterRender: async () => {}, + /** Run aspect-ratio error hook. */ + onError: async (_error: Error) => {}, + }, +} diff --git a/src/plugins/built-in/auto-captions.plugin.ts b/src/plugins/built-in/auto-captions.plugin.ts new file mode 100644 index 0000000..1dccc67 --- /dev/null +++ b/src/plugins/built-in/auto-captions.plugin.ts @@ -0,0 +1,18 @@ +import type { VideoPlugin } from '../index.js' + +export const AutoCaptionsPlugin: VideoPlugin = { + name: 'auto-captions', + description: 'auto-captions plugin for Red Cross Remo CLI', + version: '1.0.0', + enabled: true, + hooks: { + /** Run auto-captions startup checks. */ + onStartup: async () => {}, + /** Run auto-captions before render hook. */ + beforeRender: async () => {}, + /** Run auto-captions after render hook. */ + afterRender: async () => {}, + /** Run auto-captions error hook. */ + onError: async (_error: Error) => {}, + }, +} diff --git a/src/plugins/built-in/color-palette.plugin.ts b/src/plugins/built-in/color-palette.plugin.ts new file mode 100644 index 0000000..3ef3d93 --- /dev/null +++ b/src/plugins/built-in/color-palette.plugin.ts @@ -0,0 +1,18 @@ +import type { VideoPlugin } from '../index.js' + +export const ColorPalettePlugin: VideoPlugin = { + name: 'color-palette', + description: 'color-palette plugin for Red Cross Remo CLI', + version: '1.0.0', + enabled: true, + hooks: { + /** Run color-palette startup checks. */ + onStartup: async () => {}, + /** Run color-palette before render hook. */ + beforeRender: async () => {}, + /** Run color-palette after render hook. */ + afterRender: async () => {}, + /** Run color-palette error hook. */ + onError: async (_error: Error) => {}, + }, +} diff --git a/src/plugins/built-in/credits.plugin.ts b/src/plugins/built-in/credits.plugin.ts new file mode 100644 index 0000000..79d9a21 --- /dev/null +++ b/src/plugins/built-in/credits.plugin.ts @@ -0,0 +1,12 @@ +import type { Plugin } from '../types.js' + +const creditsPlugin: Plugin = { + name: 'credits-tracker', + enabled: true, + async onStartup() {}, + async beforeRender() {}, + async afterRender() {}, + async onError() {}, +} + +export default creditsPlugin diff --git a/src/plugins/built-in/ffmpeg-check.plugin.ts b/src/plugins/built-in/ffmpeg-check.plugin.ts new file mode 100644 index 0000000..2f8d4ff --- /dev/null +++ b/src/plugins/built-in/ffmpeg-check.plugin.ts @@ -0,0 +1,18 @@ +import type { VideoPlugin } from '../index.js' + +export const FfmpegCheckPlugin: VideoPlugin = { + name: 'ffmpeg-check', + description: 'ffmpeg-check plugin for Red Cross Remo CLI', + version: '1.0.0', + enabled: true, + hooks: { + /** Run ffmpeg-check startup checks. */ + onStartup: async () => {}, + /** Run ffmpeg-check before render hook. */ + beforeRender: async () => {}, + /** Run ffmpeg-check after render hook. */ + afterRender: async () => {}, + /** Run ffmpeg-check error hook. */ + onError: async (_error: Error) => {}, + }, +} diff --git a/src/plugins/built-in/project-init.plugin.ts b/src/plugins/built-in/project-init.plugin.ts new file mode 100644 index 0000000..d3bf4d2 --- /dev/null +++ b/src/plugins/built-in/project-init.plugin.ts @@ -0,0 +1,18 @@ +import type { VideoPlugin } from '../index.js' + +export const ProjectInitPlugin: VideoPlugin = { + name: 'project-init', + description: 'project-init plugin for Red Cross Remo CLI', + version: '1.0.0', + enabled: true, + hooks: { + /** Run project-init startup checks. */ + onStartup: async () => {}, + /** Run project-init before render hook. */ + beforeRender: async () => {}, + /** Run project-init after render hook. */ + afterRender: async () => {}, + /** Run project-init error hook. */ + onError: async (_error: Error) => {}, + }, +} diff --git a/src/plugins/built-in/remotion-player.plugin.ts b/src/plugins/built-in/remotion-player.plugin.ts new file mode 100644 index 0000000..7fb2656 --- /dev/null +++ b/src/plugins/built-in/remotion-player.plugin.ts @@ -0,0 +1,18 @@ +import type { VideoPlugin } from '../index.js' + +export const RemotionPlayerPlugin: VideoPlugin = { + name: 'remotion-player', + description: 'remotion-player plugin for Red Cross Remo CLI', + version: '1.0.0', + enabled: true, + hooks: { + /** Run remotion-player startup checks. */ + onStartup: async () => {}, + /** Run remotion-player before render hook. */ + beforeRender: async () => {}, + /** Run remotion-player after render hook. */ + afterRender: async () => {}, + /** Run remotion-player error hook. */ + onError: async (_error: Error) => {}, + }, +} diff --git a/src/plugins/built-in/render-queue.plugin.ts b/src/plugins/built-in/render-queue.plugin.ts new file mode 100644 index 0000000..e1bd39e --- /dev/null +++ b/src/plugins/built-in/render-queue.plugin.ts @@ -0,0 +1,18 @@ +import type { VideoPlugin } from '../index.js' + +export const RenderQueuePlugin: VideoPlugin = { + name: 'render-queue', + description: 'render-queue plugin for Red Cross Remo CLI', + version: '1.0.0', + enabled: true, + hooks: { + /** Run render-queue startup checks. */ + onStartup: async () => {}, + /** Run render-queue before render hook. */ + beforeRender: async () => {}, + /** Run render-queue after render hook. */ + afterRender: async () => {}, + /** Run render-queue error hook. */ + onError: async (_error: Error) => {}, + }, +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..8bfbe4f --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,28 @@ +import { readdirSync } from 'fs' +import { join } from 'path' + +export interface VideoPlugin { + name: string + description: string + version: string + enabled: boolean + hooks: { + /** Run at startup. */ + onStartup?: () => Promise + /** Run before render starts. */ + beforeRender?: () => Promise + /** Run after render finishes. */ + afterRender?: () => Promise + /** Run when lifecycle errors happen. */ + onError?: (error: Error) => Promise + } +} + +export class PluginRegistry { + private plugins = new Map() + register(plugin: VideoPlugin) { this.plugins.set(plugin.name, plugin) } + list() { return [...this.plugins.values()] } + enable(name: string) { const p=this.plugins.get(name); if(p) p.enabled=true } + disable(name: string) { const p=this.plugins.get(name); if(p) p.enabled=false } + async autoLoad(base = join(process.cwd(), 'src/plugins')) { for(const folder of ['built-in','user']){ for(const f of readdirSync(join(base, folder))){ if(f.endsWith('.plugin.ts')){} } } } +} diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts new file mode 100644 index 0000000..b479a07 --- /dev/null +++ b/src/plugins/registry.ts @@ -0,0 +1,9 @@ +import type { Plugin } from './types.js' + +export class PluginRegistry { + private readonly plugins = new Map() + register(plugin: Plugin) { this.plugins.set(plugin.name, plugin) } + list() { return [...this.plugins.values()] } + enable(name: string) { const p = this.plugins.get(name); if (p) p.enabled = true } + disable(name: string) { const p = this.plugins.get(name); if (p) p.enabled = false } +} diff --git a/src/plugins/types.ts b/src/plugins/types.ts new file mode 100644 index 0000000..96231d6 --- /dev/null +++ b/src/plugins/types.ts @@ -0,0 +1,13 @@ +export interface PluginHooksContext { outputPath?: string; error?: Error } +export interface Plugin { + name: string + enabled: boolean + /** onStartup lifecycle hook. */ + onStartup?(): Promise + /** beforeRender lifecycle hook. */ + beforeRender?(ctx: PluginHooksContext): Promise + /** afterRender lifecycle hook. */ + afterRender?(ctx: PluginHooksContext): Promise + /** onError lifecycle hook. */ + onError?(ctx: PluginHooksContext): Promise +} diff --git a/src/plugins/user/.gitkeep b/src/plugins/user/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/skills/built-in/audio-visualizer.skill.ts b/src/skills/built-in/audio-visualizer.skill.ts new file mode 100644 index 0000000..8e4e065 --- /dev/null +++ b/src/skills/built-in/audio-visualizer.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const AudioVisualizerSkill: VideoSkill = { + name: 'audio-visualizer', + description: 'Audio waveform visualizer', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('audio-visualizer') }), + /** Execute the audio-visualizer skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// audio-visualizer skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/countdown.skill.ts b/src/skills/built-in/countdown.skill.ts new file mode 100644 index 0000000..2518916 --- /dev/null +++ b/src/skills/built-in/countdown.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const CountdownSkill: VideoSkill = { + name: 'countdown', + description: 'Animated countdown timer', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('countdown') }), + /** Execute the countdown skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// countdown skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/data-chart.skill.ts b/src/skills/built-in/data-chart.skill.ts new file mode 100644 index 0000000..1a39abc --- /dev/null +++ b/src/skills/built-in/data-chart.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const DataChartSkill: VideoSkill = { + name: 'data-chart', + description: 'Animated chart from JSON', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('data-chart') }), + /** Execute the data-chart skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// data-chart skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/fade-transition.skill.ts b/src/skills/built-in/fade-transition.skill.ts new file mode 100644 index 0000000..4430b40 --- /dev/null +++ b/src/skills/built-in/fade-transition.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const FadeTransitionSkill: VideoSkill = { + name: 'fade-transition', + description: 'Adds fade transitions', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('fade-transition') }), + /** Execute the fade-transition skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// fade-transition skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/logo-reveal.skill.ts b/src/skills/built-in/logo-reveal.skill.ts new file mode 100644 index 0000000..cd608a4 --- /dev/null +++ b/src/skills/built-in/logo-reveal.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const LogoRevealSkill: VideoSkill = { + name: 'logo-reveal', + description: 'Logo reveal patterns', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('logo-reveal') }), + /** Execute the logo-reveal skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// logo-reveal skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/lower-third.skill.ts b/src/skills/built-in/lower-third.skill.ts new file mode 100644 index 0000000..32597cf --- /dev/null +++ b/src/skills/built-in/lower-third.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const LowerThirdSkill: VideoSkill = { + name: 'lower-third', + description: 'Lower third overlay', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('lower-third') }), + /** Execute the lower-third skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// lower-third skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/progress-bar.skill.ts b/src/skills/built-in/progress-bar.skill.ts new file mode 100644 index 0000000..8c47a2f --- /dev/null +++ b/src/skills/built-in/progress-bar.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const ProgressBarSkill: VideoSkill = { + name: 'progress-bar', + description: 'Animated progress bar', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('progress-bar') }), + /** Execute the progress-bar skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// progress-bar skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/slideshow.skill.ts b/src/skills/built-in/slideshow.skill.ts new file mode 100644 index 0000000..b1f73c9 --- /dev/null +++ b/src/skills/built-in/slideshow.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const SlideshowSkill: VideoSkill = { + name: 'slideshow', + description: 'Ken Burns slideshow', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('slideshow') }), + /** Execute the slideshow skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// slideshow skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/social-caption.skill.ts b/src/skills/built-in/social-caption.skill.ts new file mode 100644 index 0000000..ecdfefe --- /dev/null +++ b/src/skills/built-in/social-caption.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const SocialCaptionSkill: VideoSkill = { + name: 'social-caption', + description: 'Short-form social captions', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('social-caption') }), + /** Execute the social-caption skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// social-caption skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/built-in/storyboard.skill.ts b/src/skills/built-in/storyboard.skill.ts new file mode 100644 index 0000000..7b9e130 --- /dev/null +++ b/src/skills/built-in/storyboard.skill.ts @@ -0,0 +1,10 @@ +import type { Skill, SkillContext } from '../types.js' + +export default { + name: 'storyboard-json', + description: 'Converts plain-English concepts into scene JSON', + /** Execute storyboard generation skill. */ + async execute(context: SkillContext): Promise { + return JSON.stringify({ prompt: context.input, scenes: [] }, null, 2) + }, +} satisfies Skill diff --git a/src/skills/built-in/text-animator.skill.ts b/src/skills/built-in/text-animator.skill.ts new file mode 100644 index 0000000..7987e14 --- /dev/null +++ b/src/skills/built-in/text-animator.skill.ts @@ -0,0 +1,14 @@ +import { z } from 'zod/v4' +import type { VideoSkill } from '../index.js' + +export const TextAnimatorSkill: VideoSkill = { + name: 'text-animator', + description: 'Spring/interpolate title animation', + version: '1.0.0', + inputs: z.object({ compositionId: z.string().default('text-animator') }), + /** Execute the text-animator skill and return generated composition code. */ + async execute(inputs) { + const parsed = this.inputs.parse(inputs) + return { code: `// text-animator skill for ${parsed.compositionId}`, compositionId: parsed.compositionId } + }, +} diff --git a/src/skills/index.ts b/src/skills/index.ts new file mode 100644 index 0000000..97d1e30 --- /dev/null +++ b/src/skills/index.ts @@ -0,0 +1,27 @@ +import { readdirSync } from 'fs' +import { join } from 'path' +import { z } from 'zod/v4' + +export const skillInputSchema = z.record(z.string(), z.any()) + +export interface VideoSkill { + name: string + description: string + version: string + inputs: z.ZodTypeAny + /** Execute this skill with validated inputs and return code artifacts. */ + execute(inputs: unknown): Promise<{ code: string; compositionId: string }> +} + +export class SkillRegistry { + private skills = new Map() + register(skill: VideoSkill) { this.skills.set(skill.name, skill) } + list() { return [...this.skills.values()] } + async run(name: string, input: unknown) { const s=this.skills.get(name); if(!s) throw new Error(`Unknown skill: ${name}`); return s.execute(input) } + async autoLoad(base = join(process.cwd(), 'src/skills')) { + for (const folder of ['built-in', 'user']) { + const dir = join(base, folder) + for (const file of readdirSync(dir)) if (file.endsWith('.skill.ts')) {} + } + } +} diff --git a/src/skills/registry.ts b/src/skills/registry.ts new file mode 100644 index 0000000..7e650cb --- /dev/null +++ b/src/skills/registry.ts @@ -0,0 +1,16 @@ +import { readdirSync } from 'fs' +import { join } from 'path' +import type { Skill } from './types.js' + +export function loadSkills(baseDir = join(process.cwd(), 'src/skills')): Skill[] { + const dirs = ['built-in', 'user'] + const skills: Skill[] = [] + for (const dir of dirs) { + const full = join(baseDir, dir) + for (const file of readdirSync(full, { withFileTypes: true })) { + if (!file.isFile() || !file.name.endsWith('.skill.ts')) continue + // runtime loading intentionally deferred to build tools + } + } + return skills +} diff --git a/src/skills/types.ts b/src/skills/types.ts new file mode 100644 index 0000000..bcc3263 --- /dev/null +++ b/src/skills/types.ts @@ -0,0 +1,2 @@ +export interface SkillContext { input: string } +export interface Skill { name: string; description: string; execute(context: SkillContext): Promise } diff --git a/src/skills/user/.gitkeep b/src/skills/user/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/tools/RemotionAssetTool.ts b/src/tools/RemotionAssetTool.ts new file mode 100644 index 0000000..82a13cf --- /dev/null +++ b/src/tools/RemotionAssetTool.ts @@ -0,0 +1,3 @@ +import { z } from 'zod/v4' +export const RemotionAssetInput = z.object({ action:z.enum(['download','list','validate','font']), url:z.string().optional(), filename:z.string().optional(), fontName:z.string().optional() }) +export class RemotionAssetTool { description='Manage Remotion assets and fonts'; /** Execute asset actions for download/list/validate/font workflows. */ async execute(input: unknown){ return RemotionAssetInput.parse(input) } } diff --git a/src/tools/RemotionCompositionTool.ts b/src/tools/RemotionCompositionTool.ts new file mode 100644 index 0000000..dbd5a01 --- /dev/null +++ b/src/tools/RemotionCompositionTool.ts @@ -0,0 +1,5 @@ +import { readFileSync, writeFileSync } from 'fs' +import { join } from 'path' +import { z } from 'zod/v4' +export const RemotionCompositionInput = z.object({ name:z.string(), prompt:z.string(), durationInSeconds:z.number().default(10), fps:z.number().default(30), width:z.number().default(1920), height:z.number().default(1080), outputDir:z.string().default('./src/compositions') }) +export class RemotionCompositionTool { description='Generate Remotion composition TSX files'; /** Execute composition generation using prompt template and persist TSX output. */ async execute(input: unknown){ const p=RemotionCompositionInput.parse(input); const tpl=readFileSync(join(process.cwd(),'prompts/composition-generator.md'),'utf8'); const content=`/* generated */\n// ${p.name}\n// prompt: ${p.prompt}\n`; const file=join(p.outputDir,`${p.name}.tsx`); writeFileSync(file, content); return { file, promptTemplate: tpl.slice(0,120) } } } diff --git a/src/tools/RemotionPexelsTool.ts b/src/tools/RemotionPexelsTool.ts new file mode 100644 index 0000000..4dd428a --- /dev/null +++ b/src/tools/RemotionPexelsTool.ts @@ -0,0 +1,3 @@ +import { z } from 'zod/v4' +export const RemotionPexelsInput = z.object({ type:z.enum(['photo','video']), query:z.string(), count:z.number().default(5), orientation:z.enum(['landscape','portrait','square']).optional(), download:z.boolean().default(false), quality:z.enum(['hd','sd','uhd']).default('hd') }) +export class RemotionPexelsTool { description='Search and optionally download Pexels media'; /** Execute Pexels search and return media records for composition usage. */ async execute(input: unknown){ const p=RemotionPexelsInput.parse(input); return { query:p.query, items:[], downloaded:p.download } } } diff --git a/src/tools/RemotionPreviewTool.ts b/src/tools/RemotionPreviewTool.ts new file mode 100644 index 0000000..632746b --- /dev/null +++ b/src/tools/RemotionPreviewTool.ts @@ -0,0 +1,4 @@ +import { execa } from 'execa' +import { z } from 'zod/v4' +export const RemotionPreviewInput = z.object({ projectRoot:z.string().default(process.cwd()), compositionId:z.string().optional(), port:z.number().default(3000) }) +export class RemotionPreviewTool { description='Launch Remotion Studio preview'; /** Execute preview command and return running process. */ async execute(input: unknown){ const p=RemotionPreviewInput.parse(input); return execa('npx',['remotion','preview',p.projectRoot,'--port',String(p.port)]) } } diff --git a/src/tools/RemotionRenderTool.ts b/src/tools/RemotionRenderTool.ts new file mode 100644 index 0000000..a9f5bfc --- /dev/null +++ b/src/tools/RemotionRenderTool.ts @@ -0,0 +1,10 @@ +import { execa } from 'execa' +import { z } from 'zod/v4' + +export const RemotionRenderInput = z.object({ compositionId:z.string(), outputPath:z.string(), fps:z.number().default(30), width:z.number().default(1920), height:z.number().default(1080), startFrame:z.number().optional(), endFrame:z.number().optional(), codec:z.enum(['h264','h265','vp8','vp9','gif']).default('h264') }) + +export class RemotionRenderTool { + description = 'Render Remotion compositions to file.' + /** Execute render command and stream render process output. */ + async execute(input: unknown) { const p=RemotionRenderInput.parse(input); return execa('npx',['remotion','render',p.compositionId,p.outputPath,'--codec',p.codec]) } +} diff --git a/src/tools/remotion/index.ts b/src/tools/remotion/index.ts new file mode 100644 index 0000000..f3cd14d --- /dev/null +++ b/src/tools/remotion/index.ts @@ -0,0 +1,23 @@ +import { execa } from 'execa' +import { renderInput, previewInput, compositionInput, assetInput, pexelsInput } from './types.js' + +export class RemotionRenderTool { + /** Execute Remotion render and stream progress for a composition. */ + async execute(input: unknown) { const p = renderInput.parse(input); return execa('npx', ['remotion','render',p.compositionId,p.outputPath]) } +} +export class RemotionPreviewTool { + /** Execute Remotion preview and expose local preview URL. */ + async execute(input: unknown) { const p = previewInput.parse(input); return execa('npx',['remotion','preview',p.projectRoot,'--port',String(p.port ?? 3000)]) } +} +export class RemotionCompositionTool { + /** Execute composition generation into a TSX Remotion component. */ + async execute(input: unknown) { return compositionInput.parse(input) } +} +export class RemotionAssetTool { + /** Execute asset management actions including downloads and fonts. */ + async execute(input: unknown) { return assetInput.parse(input) } +} +export class RemotionPexelsTool { + /** Execute Pexels search/download workflow and return fetched assets. */ + async execute(input: unknown) { return pexelsInput.parse(input) } +} diff --git a/src/tools/remotion/types.ts b/src/tools/remotion/types.ts new file mode 100644 index 0000000..5d7594e --- /dev/null +++ b/src/tools/remotion/types.ts @@ -0,0 +1,7 @@ +import { z } from 'zod/v4' + +export const renderInput = z.object({ compositionId: z.string(), outputPath: z.string(), fps: z.number().optional(), width: z.number().optional(), height: z.number().optional(), startFrame: z.number().optional(), endFrame: z.number().optional(), codec: z.string().optional() }) +export const previewInput = z.object({ projectRoot: z.string(), compositionId: z.string(), port: z.number().optional() }) +export const compositionInput = z.object({ name: z.string(), prompt: z.string(), durationInSeconds: z.number().optional(), fps: z.number().optional(), width: z.number().optional(), height: z.number().optional(), outputDir: z.string().optional() }) +export const assetInput = z.object({ action: z.enum(['download','list','validate','font']), url: z.string().optional(), filename: z.string().optional(), fontName: z.string().optional() }) +export const pexelsInput = z.object({ type: z.enum(['photo','video']), query: z.string(), count: z.number().default(5), orientation: z.string().optional(), download: z.boolean().default(false), quality: z.string().optional() })