@jiggai/recipes 0.4.34 → 0.4.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "id": "recipes",
3
3
  "name": "Recipes",
4
4
  "description": "Markdown recipes that scaffold agents and teams (workspace-local).",
5
- "version": "0.4.34",
5
+ "version": "0.4.35",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.4.34",
3
+ "version": "0.4.35",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -0,0 +1,49 @@
1
+ import { getAllDrivers, isDriverAvailable } from '../lib/workflows/media-drivers/registry';
2
+ import { loadConfigEnv } from '../lib/workflows/media-drivers/utils';
3
+
4
+ export interface DurationConstraintsInfo {
5
+ minSeconds: number;
6
+ maxSeconds: number;
7
+ defaultSeconds: number;
8
+ stepSeconds?: number;
9
+ }
10
+
11
+ export interface MediaDriverInfo {
12
+ slug: string;
13
+ displayName: string;
14
+ mediaType: 'image' | 'video' | 'audio';
15
+ requiredEnvVars: string[];
16
+ available: boolean;
17
+ missingEnvVars: string[];
18
+ durationConstraints: DurationConstraintsInfo | null;
19
+ }
20
+
21
+ /**
22
+ * List all known media drivers with availability status.
23
+ */
24
+ export async function handleMediaDriversList(): Promise<MediaDriverInfo[]> {
25
+ const configEnv = await loadConfigEnv();
26
+
27
+ // Merge process.env (strings only) with config env vars
28
+ const mergedEnv: Record<string, string> = {};
29
+ for (const [k, v] of Object.entries(process.env)) {
30
+ if (typeof v === 'string') mergedEnv[k] = v;
31
+ }
32
+ Object.assign(mergedEnv, configEnv);
33
+
34
+ return getAllDrivers().map((driver) => {
35
+ const available = isDriverAvailable(driver.slug, mergedEnv);
36
+ const missing = driver.requiredEnvVars.filter(
37
+ (v) => !mergedEnv[v] || mergedEnv[v].trim().length === 0
38
+ );
39
+ return {
40
+ slug: driver.slug,
41
+ displayName: driver.displayName,
42
+ mediaType: driver.mediaType,
43
+ requiredEnvVars: driver.requiredEnvVars,
44
+ available,
45
+ missingEnvVars: missing,
46
+ durationConstraints: driver.durationConstraints ?? null,
47
+ };
48
+ });
49
+ }
@@ -0,0 +1,128 @@
1
+ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult } from './types';
2
+ import { findSkillDir, findVenvPython, runScript, parseMediaOutput, findScriptInSkill } from './utils';
3
+
4
+ export class GenericDriver implements MediaDriver {
5
+ slug: string;
6
+ mediaType: 'image' | 'video' | 'audio';
7
+ displayName: string;
8
+ requiredEnvVars: string[] = [];
9
+ durationConstraints = null;
10
+
11
+ constructor(slug: string, mediaType: 'image' | 'video' | 'audio', displayName?: string) {
12
+ this.slug = slug;
13
+ this.mediaType = mediaType;
14
+ this.displayName = displayName || `Generic ${mediaType} driver for ${slug}`;
15
+ }
16
+
17
+ async invoke(opts: MediaDriverInvokeOpts): Promise<MediaDriverResult> {
18
+ const { prompt, outputDir, env, timeout } = opts;
19
+
20
+ // Find the skill directory
21
+ const skillDir = await findSkillDir(this.slug);
22
+ if (!skillDir) {
23
+ throw new Error(`Skill directory not found for ${this.slug}`);
24
+ }
25
+
26
+ // Determine script candidates based on media type
27
+ const scriptCandidates = this.mediaType === 'image'
28
+ ? ['generate_image.py', 'generate_image.sh', 'generate.sh']
29
+ : this.mediaType === 'video'
30
+ ? ['generate_video.py', 'generate_video.sh', 'generate.py', 'generate.sh']
31
+ : ['generate_audio.py', 'generate_audio.sh', 'generate.py', 'generate.sh'];
32
+
33
+ // Find the script
34
+ const scriptPath = await findScriptInSkill(skillDir, scriptCandidates);
35
+ if (!scriptPath) {
36
+ throw new Error(`No generation script found in ${skillDir}. Looked for: ${scriptCandidates.join(', ')}`);
37
+ }
38
+
39
+ // Determine runner
40
+ let runner = 'bash';
41
+ if (scriptPath.endsWith('.py')) {
42
+ runner = await findVenvPython(skillDir);
43
+ }
44
+
45
+ // Execute the script with stdin input (most common interface)
46
+ const scriptOutput = runScript({
47
+ runner,
48
+ script: scriptPath,
49
+ stdin: prompt,
50
+ env: {
51
+ ...env,
52
+ HOME: process.env.HOME || '/home/control',
53
+ },
54
+ cwd: outputDir,
55
+ timeout,
56
+ });
57
+
58
+ // Try to parse MEDIA: output first
59
+ let filePath = parseMediaOutput(scriptOutput);
60
+
61
+ // If no MEDIA: prefix, try to find the actual file path in the output
62
+ if (!filePath) {
63
+ const lines = scriptOutput.split('\n').map(line => line.trim()).filter(Boolean);
64
+ // Look for lines that look like file paths
65
+ for (const line of lines.reverse()) {
66
+ if (line.includes('/') && (line.includes('.') || line.includes(outputDir))) {
67
+ filePath = line;
68
+ break;
69
+ }
70
+ }
71
+ }
72
+
73
+ if (!filePath) {
74
+ throw new Error(`No file path found in script output. Output: ${scriptOutput}`);
75
+ }
76
+
77
+ return {
78
+ filePath,
79
+ metadata: {
80
+ skill: this.slug,
81
+ prompt,
82
+ script_output: scriptOutput,
83
+ script_path: scriptPath,
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Create a generic driver by auto-detecting a skill's capabilities
90
+ */
91
+ static async createFromSkill(slug: string): Promise<GenericDriver | null> {
92
+ const skillDir = await findSkillDir(slug);
93
+ if (!skillDir) {
94
+ return null;
95
+ }
96
+
97
+ // Check what types of scripts are available
98
+ const imageScripts = ['generate_image.py', 'generate_image.sh'];
99
+ const videoScripts = ['generate_video.py', 'generate_video.sh'];
100
+ const audioScripts = ['generate_audio.py', 'generate_audio.sh'];
101
+
102
+ // Check for image generation capability
103
+ const imageScript = await findScriptInSkill(skillDir, imageScripts);
104
+ if (imageScript) {
105
+ return new GenericDriver(slug, 'image', `${slug} Image Generation`);
106
+ }
107
+
108
+ // Check for video generation capability
109
+ const videoScript = await findScriptInSkill(skillDir, videoScripts);
110
+ if (videoScript) {
111
+ return new GenericDriver(slug, 'video', `${slug} Video Generation`);
112
+ }
113
+
114
+ // Check for audio generation capability
115
+ const audioScript = await findScriptInSkill(skillDir, audioScripts);
116
+ if (audioScript) {
117
+ return new GenericDriver(slug, 'audio', `${slug} Audio Generation`);
118
+ }
119
+
120
+ // Fall back to generic generate script
121
+ const genericScript = await findScriptInSkill(skillDir, ['generate.py', 'generate.sh']);
122
+ if (genericScript) {
123
+ return new GenericDriver(slug, 'image', `${slug} Generic Generation`);
124
+ }
125
+
126
+ return null;
127
+ }
128
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ getDriver,
3
+ getDriversByType,
4
+ getAllDrivers,
5
+ isDriverAvailable,
6
+ getAvailableDrivers,
7
+ getAvailableDriversByType
8
+ } from './registry';
9
+
10
+ export type {
11
+ MediaDriver,
12
+ MediaDriverInvokeOpts,
13
+ MediaDriverResult,
14
+ DurationConstraints
15
+ } from './types';
16
+
17
+ export { NanoBananaPro } from './nano-banana-pro.driver';
18
+ export { OpenAIImageGen } from './openai-image-gen.driver';
19
+ export { RunwayVideo } from './runway-video.driver';
20
+ export { KlingVideo } from './kling-video.driver';
21
+ export { LumaVideo } from './luma-video.driver';
22
+ export { GenericDriver } from './generic.driver';
@@ -0,0 +1,110 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult, DurationConstraints, parseDuration } from './types';
4
+ import { findSkillDir, runScript, parseMediaOutput } from './utils';
5
+
6
+ /**
7
+ * Kling AI video driver — uses official `klingai` ClawHub skill.
8
+ *
9
+ * Auth: JWT via ~/.config/kling/.credentials (Access Key + Secret Key).
10
+ * NOT a simple Bearer API key — the skill's auth.mjs handles JWT signing.
11
+ * No env var needed; credentials file is the source of truth.
12
+ */
13
+ export class KlingVideo implements MediaDriver {
14
+ slug = 'klingai';
15
+ mediaType = 'video' as const;
16
+ displayName = 'Kling AI Video (Official)';
17
+ // Auth is via ~/.config/kling/.credentials, not env vars.
18
+ // We check for the credentials file in a custom availability method.
19
+ requiredEnvVars: string[] = [];
20
+ durationConstraints: DurationConstraints = { minSeconds: 3, maxSeconds: 15, defaultSeconds: 5, stepSeconds: 1 };
21
+
22
+ /**
23
+ * Check if Kling credentials are configured (credentials file exists with AK/SK).
24
+ */
25
+ isConfigured(): boolean {
26
+ const home = process.env.HOME || '/home/control';
27
+ const credPath = path.join(home, '.config', 'kling', '.credentials');
28
+ try {
29
+ const content = fs.readFileSync(credPath, 'utf8');
30
+ return content.includes('access_key_id') && content.includes('secret_access_key');
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ async invoke(opts: MediaDriverInvokeOpts): Promise<MediaDriverResult> {
37
+ const { prompt, outputDir, env, timeout, config } = opts;
38
+ // Kling supports 3-15s; clamp to valid range
39
+ const rawDuration = Math.max(3, Math.min(15, Number(parseDuration(config))));
40
+ const duration = String(rawDuration);
41
+
42
+ const skillDir = await findSkillDir(this.slug);
43
+ if (!skillDir) {
44
+ throw new Error(
45
+ `Skill directory not found for ${this.slug}. Install it: clawhub install klingai --force`
46
+ );
47
+ }
48
+
49
+ const scriptPath = path.join(skillDir, 'scripts', 'video.mjs');
50
+
51
+ // The official skill is a Node.js script (not Python)
52
+ const runner = 'node';
53
+
54
+ const scriptOutput = runScript({
55
+ runner,
56
+ script: scriptPath,
57
+ args: [
58
+ '--prompt', prompt,
59
+ '--output_dir', outputDir,
60
+ '--duration', duration,
61
+ '--aspect_ratio', String(config?.aspect_ratio ?? config?.size ?? '16:9'),
62
+ '--mode', 'pro',
63
+ ],
64
+ env: {
65
+ ...env,
66
+ HOME: process.env.HOME || '/home/control',
67
+ },
68
+ cwd: outputDir,
69
+ timeout,
70
+ });
71
+
72
+ // The script prints "Done: /path/to/file.mp4" or "Saved: /path/to/file.mp4"
73
+ const doneMatch = scriptOutput.match(/(?:Done|完成|Saved|已保存):\s*(.+\.mp4)/m);
74
+ if (doneMatch) {
75
+ return {
76
+ filePath: doneMatch[1].trim(),
77
+ metadata: { skill: this.slug, prompt },
78
+ };
79
+ }
80
+
81
+ // Fallback: check for MEDIA: prefix (in case a bridge wrapper is used)
82
+ const mediaPath = parseMediaOutput(scriptOutput);
83
+ if (mediaPath) {
84
+ return {
85
+ filePath: mediaPath,
86
+ metadata: { skill: this.slug, prompt },
87
+ };
88
+ }
89
+
90
+ // Last resort: look for any .mp4 in output dir
91
+ try {
92
+ const files = fs.readdirSync(outputDir)
93
+ .filter(f => f.endsWith('.mp4'))
94
+ .sort()
95
+ .reverse();
96
+ if (files.length > 0) {
97
+ return {
98
+ filePath: path.join(outputDir, files[0]),
99
+ metadata: { skill: this.slug, prompt },
100
+ };
101
+ }
102
+ } catch {
103
+ // ignore
104
+ }
105
+
106
+ throw new Error(
107
+ `Could not find generated video in output. Script output:\n${scriptOutput}`
108
+ );
109
+ }
110
+ }
@@ -0,0 +1,59 @@
1
+ import * as path from 'path';
2
+ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult, DurationConstraints, parseDuration } from './types';
3
+ import { findSkillDir, findVenvPython, runScript, parseMediaOutput } from './utils';
4
+
5
+ export class LumaVideo implements MediaDriver {
6
+ slug = 'skill-luma-video';
7
+ mediaType = 'video' as const;
8
+ displayName = 'Luma Video Generation';
9
+ requiredEnvVars = ['LUMAAI_API_KEY'];
10
+ durationConstraints: DurationConstraints = { minSeconds: 5, maxSeconds: 9, defaultSeconds: 5, stepSeconds: 4 };
11
+
12
+ async invoke(opts: MediaDriverInvokeOpts): Promise<MediaDriverResult> {
13
+ const { prompt, outputDir, env, timeout, config } = opts;
14
+ const duration = parseDuration(config);
15
+
16
+ // Find the skill directory
17
+ const skillDir = await findSkillDir(this.slug);
18
+ if (!skillDir) {
19
+ throw new Error(`Skill directory not found for ${this.slug}`);
20
+ }
21
+
22
+ // Find the script
23
+ const scriptPath = path.join(skillDir, 'generate_video.py');
24
+
25
+ // Find Python runner
26
+ const runner = await findVenvPython(skillDir);
27
+
28
+ // Execute the script with stdin input
29
+ const scriptOutput = runScript({
30
+ runner,
31
+ script: scriptPath,
32
+ stdin: prompt,
33
+ env: {
34
+ ...env,
35
+ HOME: process.env.HOME || '/home/control',
36
+ MEDIA_DURATION: duration,
37
+ MEDIA_ASPECT_RATIO: String(config?.aspect_ratio ?? config?.size ?? '16:9'),
38
+ },
39
+ cwd: outputDir,
40
+ timeout,
41
+ });
42
+
43
+ // Parse the MEDIA: output
44
+ const filePath = parseMediaOutput(scriptOutput);
45
+
46
+ if (!filePath) {
47
+ throw new Error(`No MEDIA: path found in script output. Output: ${scriptOutput}`);
48
+ }
49
+
50
+ return {
51
+ filePath,
52
+ metadata: {
53
+ skill: this.slug,
54
+ prompt,
55
+ script_output: scriptOutput,
56
+ },
57
+ };
58
+ }
59
+ }
@@ -0,0 +1,70 @@
1
+ import * as path from 'path';
2
+ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult } from './types';
3
+ import { findSkillDir, findVenvPython, runScript } from './utils';
4
+
5
+ export class NanoBananaPro implements MediaDriver {
6
+ slug = 'nano-banana-pro';
7
+ mediaType = 'image' as const;
8
+ displayName = 'Nano Banana Pro (Gemini Image Generation)';
9
+ requiredEnvVars = ['GEMINI_API_KEY'];
10
+ durationConstraints = null;
11
+
12
+ async invoke(opts: MediaDriverInvokeOpts): Promise<MediaDriverResult> {
13
+ const { prompt, outputDir, env, timeout, config } = opts;
14
+
15
+ // Find the skill directory
16
+ const skillDir = await findSkillDir(this.slug);
17
+ if (!skillDir) {
18
+ throw new Error(`Skill directory not found for ${this.slug}`);
19
+ }
20
+
21
+ // Find the script
22
+ const scriptPath = path.join(skillDir, 'scripts', 'generate_image.py');
23
+
24
+ // Find Python runner - check for venv first, fallback to uv run
25
+ let runner: string;
26
+ try {
27
+ runner = await findVenvPython(skillDir);
28
+ } catch {
29
+ // Fallback to uv run if no venv
30
+ runner = 'uv run python';
31
+ }
32
+
33
+ // Generate a filename for the output
34
+ const filename = 'output.png';
35
+
36
+ // Map pixel size to resolution tier (nano-banana-pro uses 1K/2K/4K)
37
+ const sizeStr = String(config?.size ?? '1024x1024');
38
+ const maxDim = Math.max(...sizeStr.split('x').map(Number).filter(n => !isNaN(n)), 1024);
39
+ const resolution = maxDim >= 3840 ? '4K' : maxDim >= 1792 ? '2K' : '1K';
40
+
41
+ // Execute the script with argparse CLI interface
42
+ const scriptOutput = runScript({
43
+ runner,
44
+ script: scriptPath,
45
+ args: ['--prompt', prompt, '--filename', filename, '--resolution', resolution],
46
+ env: {
47
+ ...env,
48
+ HOME: process.env.HOME || '/home/control',
49
+ },
50
+ cwd: outputDir,
51
+ timeout,
52
+ });
53
+
54
+ // nano-banana-pro prints the full path on stdout
55
+ const outputPath = scriptOutput.trim();
56
+
57
+ if (!outputPath || !outputPath.includes('.')) {
58
+ throw new Error(`No valid file path returned from script. Output: ${scriptOutput}`);
59
+ }
60
+
61
+ return {
62
+ filePath: outputPath,
63
+ metadata: {
64
+ skill: this.slug,
65
+ prompt,
66
+ script_output: scriptOutput,
67
+ },
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,60 @@
1
+ import * as path from 'path';
2
+ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult } from './types';
3
+ import { findSkillDir, findVenvPython, runScript, parseMediaOutput } from './utils';
4
+
5
+ export class OpenAIImageGen implements MediaDriver {
6
+ slug = 'openai-image-gen';
7
+ mediaType = 'image' as const;
8
+ displayName = 'OpenAI Image Generation (DALL-E)';
9
+ requiredEnvVars = ['OPENAI_API_KEY'];
10
+ durationConstraints = null;
11
+
12
+ async invoke(opts: MediaDriverInvokeOpts): Promise<MediaDriverResult> {
13
+ const { prompt, outputDir, env, timeout, config } = opts;
14
+
15
+ // Find the skill directory
16
+ const skillDir = await findSkillDir(this.slug);
17
+ if (!skillDir) {
18
+ throw new Error(`Skill directory not found for ${this.slug}`);
19
+ }
20
+
21
+ // Find the script
22
+ const scriptPath = path.join(skillDir, 'generate_image.py');
23
+
24
+ // Find Python runner
25
+ const runner = await findVenvPython(skillDir);
26
+
27
+ // Pass size via env var (script reads DALL_E_SIZE, defaults to 1024x1024)
28
+ const size = String(config?.size ?? '1024x1024');
29
+
30
+ // Execute the script with stdin input
31
+ const scriptOutput = runScript({
32
+ runner,
33
+ script: scriptPath,
34
+ stdin: prompt,
35
+ env: {
36
+ ...env,
37
+ HOME: process.env.HOME || '/home/control',
38
+ DALL_E_SIZE: size,
39
+ },
40
+ cwd: outputDir,
41
+ timeout,
42
+ });
43
+
44
+ // Parse the MEDIA: output
45
+ const filePath = parseMediaOutput(scriptOutput);
46
+
47
+ if (!filePath) {
48
+ throw new Error(`No MEDIA: path found in script output. Output: ${scriptOutput}`);
49
+ }
50
+
51
+ return {
52
+ filePath,
53
+ metadata: {
54
+ skill: this.slug,
55
+ prompt,
56
+ script_output: scriptOutput,
57
+ },
58
+ };
59
+ }
60
+ }
@@ -0,0 +1,96 @@
1
+ import { MediaDriver } from './types';
2
+ import { NanoBananaPro } from './nano-banana-pro.driver';
3
+ import { OpenAIImageGen } from './openai-image-gen.driver';
4
+ import { RunwayVideo } from './runway-video.driver';
5
+ import { KlingVideo } from './kling-video.driver';
6
+ import { LumaVideo } from './luma-video.driver';
7
+ import { GenericDriver } from './generic.driver';
8
+
9
+ // Registry of known drivers
10
+ const knownDrivers: MediaDriver[] = [
11
+ new NanoBananaPro(),
12
+ new OpenAIImageGen(),
13
+ new RunwayVideo(),
14
+ new KlingVideo(),
15
+ new LumaVideo(),
16
+ ];
17
+
18
+ // Cache for discovered generic drivers
19
+ const genericDriverCache = new Map<string, MediaDriver | null>();
20
+
21
+ /**
22
+ * Get a driver by slug
23
+ */
24
+ export function getDriver(slug: string): MediaDriver | undefined {
25
+ // First check known drivers
26
+ const knownDriver = knownDrivers.find(d => d.slug === slug);
27
+ if (knownDriver) {
28
+ return knownDriver;
29
+ }
30
+
31
+ // Check cache for generic drivers
32
+ if (genericDriverCache.has(slug)) {
33
+ const cached = genericDriverCache.get(slug);
34
+ return cached || undefined;
35
+ }
36
+
37
+ // Try to create a generic driver
38
+ let genericDriver: MediaDriver | null = null;
39
+ try {
40
+ // Use sync approach since we need this to be synchronous for the registry
41
+ // The actual skill discovery will be done lazily
42
+ genericDriver = new GenericDriver(slug, 'image', `Generic driver for ${slug}`);
43
+ genericDriverCache.set(slug, genericDriver);
44
+ return genericDriver;
45
+ } catch {
46
+ genericDriverCache.set(slug, null);
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Get all drivers that produce a specific media type
53
+ */
54
+ export function getDriversByType(mediaType: 'image' | 'video' | 'audio'): MediaDriver[] {
55
+ return knownDrivers.filter(d => d.mediaType === mediaType);
56
+ }
57
+
58
+ /**
59
+ * Get all known drivers (does not include auto-discovered generic drivers)
60
+ */
61
+ export function getAllDrivers(): MediaDriver[] {
62
+ return [...knownDrivers];
63
+ }
64
+
65
+ /**
66
+ * Check if a driver is available (has required environment variables)
67
+ */
68
+ export function isDriverAvailable(slug: string, env: Record<string, string>): boolean {
69
+ const driver = getDriver(slug);
70
+ if (!driver) {
71
+ return false;
72
+ }
73
+
74
+ // Check if all required environment variables are present
75
+ return driver.requiredEnvVars.every(envVar => {
76
+ const value = env[envVar];
77
+ return value && typeof value === 'string' && value.trim().length > 0;
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Get drivers available with the current environment
83
+ */
84
+ export function getAvailableDrivers(env: Record<string, string>): MediaDriver[] {
85
+ return knownDrivers.filter(driver => isDriverAvailable(driver.slug, env));
86
+ }
87
+
88
+ /**
89
+ * Get available drivers by media type
90
+ */
91
+ export function getAvailableDriversByType(
92
+ mediaType: 'image' | 'video' | 'audio',
93
+ env: Record<string, string>
94
+ ): MediaDriver[] {
95
+ return getDriversByType(mediaType).filter(driver => isDriverAvailable(driver.slug, env));
96
+ }
@@ -0,0 +1,59 @@
1
+ import * as path from 'path';
2
+ import { MediaDriver, MediaDriverInvokeOpts, MediaDriverResult, DurationConstraints, parseDuration } from './types';
3
+ import { findSkillDir, findVenvPython, runScript, parseMediaOutput } from './utils';
4
+
5
+ export class RunwayVideo implements MediaDriver {
6
+ slug = 'skill-runway-video';
7
+ mediaType = 'video' as const;
8
+ displayName = 'Runway Video Generation';
9
+ requiredEnvVars = ['RUNWAYML_API_SECRET'];
10
+ durationConstraints: DurationConstraints = { minSeconds: 5, maxSeconds: 10, defaultSeconds: 10, stepSeconds: 5 };
11
+
12
+ async invoke(opts: MediaDriverInvokeOpts): Promise<MediaDriverResult> {
13
+ const { prompt, outputDir, env, timeout, config } = opts;
14
+ const duration = parseDuration(config);
15
+
16
+ // Find the skill directory
17
+ const skillDir = await findSkillDir(this.slug);
18
+ if (!skillDir) {
19
+ throw new Error(`Skill directory not found for ${this.slug}`);
20
+ }
21
+
22
+ // Find the script
23
+ const scriptPath = path.join(skillDir, 'generate_video.py');
24
+
25
+ // Find Python runner
26
+ const runner = await findVenvPython(skillDir);
27
+
28
+ // Execute the script with stdin input
29
+ const scriptOutput = runScript({
30
+ runner,
31
+ script: scriptPath,
32
+ stdin: prompt,
33
+ env: {
34
+ ...env,
35
+ HOME: process.env.HOME || '/home/control',
36
+ MEDIA_DURATION: duration,
37
+ MEDIA_ASPECT_RATIO: String(config?.aspect_ratio ?? config?.size ?? '1280:768'),
38
+ },
39
+ cwd: outputDir,
40
+ timeout,
41
+ });
42
+
43
+ // Parse the MEDIA: output
44
+ const filePath = parseMediaOutput(scriptOutput);
45
+
46
+ if (!filePath) {
47
+ throw new Error(`No MEDIA: path found in script output. Output: ${scriptOutput}`);
48
+ }
49
+
50
+ return {
51
+ filePath,
52
+ metadata: {
53
+ skill: this.slug,
54
+ prompt,
55
+ script_output: scriptOutput,
56
+ },
57
+ };
58
+ }
59
+ }