opencode-skills-collection 4.0.73 → 4.0.75

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.
Files changed (63) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +16 -1
  2. package/bundled-skills/claude-api/SKILL.md +4 -4
  3. package/bundled-skills/claude-api/curl/examples.md +5 -5
  4. package/bundled-skills/claude-api/java/claude-api.md +1 -1
  5. package/bundled-skills/claude-api/php/claude-api.md +2 -2
  6. package/bundled-skills/claude-api/python/claude-api/README.md +13 -13
  7. package/bundled-skills/claude-api/python/claude-api/batches.md +3 -3
  8. package/bundled-skills/claude-api/python/claude-api/files-api.md +3 -3
  9. package/bundled-skills/claude-api/python/claude-api/streaming.md +6 -6
  10. package/bundled-skills/claude-api/python/claude-api/tool-use.md +19 -19
  11. package/bundled-skills/claude-api/ruby/claude-api.md +3 -3
  12. package/bundled-skills/claude-api/shared/error-codes.md +2 -2
  13. package/bundled-skills/claude-api/shared/live-sources.md +1 -1
  14. package/bundled-skills/claude-api/shared/models.md +4 -4
  15. package/bundled-skills/claude-api/shared/tool-use-concepts.md +2 -2
  16. package/bundled-skills/claude-api/typescript/claude-api/README.md +12 -12
  17. package/bundled-skills/claude-api/typescript/claude-api/batches.md +2 -2
  18. package/bundled-skills/claude-api/typescript/claude-api/files-api.md +1 -1
  19. package/bundled-skills/claude-api/typescript/claude-api/streaming.md +4 -4
  20. package/bundled-skills/claude-api/typescript/claude-api/tool-use.md +14 -14
  21. package/bundled-skills/electron-drive-skill/LICENSE +21 -0
  22. package/bundled-skills/electron-drive-skill/README.md +172 -0
  23. package/bundled-skills/electron-drive-skill/SKILL.md +237 -0
  24. package/bundled-skills/electron-drive-skill/drive.config.json.example +10 -0
  25. package/bundled-skills/electron-drive-skill/package.json +28 -0
  26. package/bundled-skills/electron-drive-skill/scripts/daemon.mjs +352 -0
  27. package/bundled-skills/electron-drive-skill/scripts/drive.mjs +250 -0
  28. package/bundled-skills/electron-drive-skill/scripts/lib.mjs +165 -0
  29. package/bundled-skills/electron-drive-skill/scripts/lib.test.mjs +56 -0
  30. package/bundled-skills/film-crew/SKILL.md +129 -0
  31. package/bundled-skills/film-crew/references/model-prompting.md +89 -0
  32. package/bundled-skills/film-crew/references/reroll-review.md +46 -0
  33. package/bundled-skills/film-crew/references/roles/director.md +38 -0
  34. package/bundled-skills/film-crew/references/roles/dp.md +28 -0
  35. package/bundled-skills/film-crew/references/roles/editor.md +34 -0
  36. package/bundled-skills/film-crew/references/roles/gaffer.md +30 -0
  37. package/bundled-skills/film-crew/references/roles/production-designer.md +43 -0
  38. package/bundled-skills/film-crew/references/roles/script-supervisor.md +35 -0
  39. package/bundled-skills/film-crew/references/roles/sound.md +23 -0
  40. package/bundled-skills/film-crew/references/templates/shot-list.md +38 -0
  41. package/bundled-skills/film-crew/references/unslop.md +29 -0
  42. package/bundled-skills/mcp-dependency-drift-audit/SKILL.md +169 -0
  43. package/bundled-skills/nexrad-mosaic-access/SKILL.md +297 -0
  44. package/bundled-skills/nexrad-mosaic-construction/SKILL.md +277 -0
  45. package/bundled-skills/nexrad-product-access/SKILL.md +258 -0
  46. package/bundled-skills/nexrad-radar-visualization/SKILL.md +318 -0
  47. package/bundled-skills/radar-satellite-analysis/SKILL.md +314 -0
  48. package/bundled-skills/testdriver-e2e-testing/SKILL.md +121 -0
  49. package/bundled-skills/traderspy-market-briefing/SKILL.md +132 -0
  50. package/bundled-skills/traderspy-market-screener/SKILL.md +182 -0
  51. package/bundled-skills/traderspy-market-screener/references/condition-cookbook.md +70 -0
  52. package/bundled-skills/traderspy-position-check/SKILL.md +130 -0
  53. package/bundled-skills/traderspy-position-check/references/risk-math.md +60 -0
  54. package/bundled-skills/traderspy-smart-money/SKILL.md +151 -0
  55. package/bundled-skills/traderspy-smart-money/references/exchange-notes.md +40 -0
  56. package/bundled-skills/traderspy-technical-analysis/SKILL.md +162 -0
  57. package/bundled-skills/traderspy-technical-analysis/references/derivatives-guide.md +57 -0
  58. package/bundled-skills/traderspy-technical-analysis/references/indicator-guide.md +47 -0
  59. package/bundled-skills/traderspy-trading-signals/SKILL.md +156 -0
  60. package/bundled-skills/traderspy-trading-signals/references/signal-fields.md +64 -0
  61. package/bundled-skills/unified-ai-gateway/SKILL.md +3 -3
  62. package/package.json +1 -1
  63. package/skills_index.json +499 -1
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Start an Electron app under Playwright and operate it from the command line,
4
+ * one command per call. Written for coding agents, usable by anyone.
5
+ *
6
+ * Run it from anywhere inside the project; see SKILL.md and README.md.
7
+ */
8
+ import { spawn, spawnSync } from 'node:child_process'
9
+ import { existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync } from 'node:fs'
10
+ import http from 'node:http'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ import {
15
+ daemonLogPath,
16
+ driveRoot,
17
+ isAlive,
18
+ loadConfig,
19
+ parseArgs,
20
+ profileDir,
21
+ PROJECT_ROOT,
22
+ readState,
23
+ startOptions,
24
+ statePath
25
+ } from './lib.mjs'
26
+
27
+ const here = path.dirname(fileURLToPath(import.meta.url))
28
+
29
+ const USAGE = `drive <command>
30
+
31
+ start [--dev] [--build] [--profile <name>] [--fresh]
32
+ Launch the app on a scratch profile. Default: the last build
33
+ ('build' in drive.config.json runs first with --build). --dev: the
34
+ dev launch, against a renderer dev server you started yourself.
35
+ --fresh wipes the profile first (first-run state).
36
+ status whether the app is up, its profile and window
37
+ snapshot accessibility tree of the window
38
+ screenshot [--out <png>] [--selector <css>]
39
+ click <target> Playwright selector: 'text=Settings', 'role=button[name="Save"]', css
40
+ fill <target> <text>
41
+ select <target> <value|label> a <select>, by option value or visible label
42
+ press <key> e.g. Enter, Escape, Control+K
43
+ wait <target> [--timeout <ms>]
44
+ eval <js | -> in the renderer; an expression, or a body with 'return'
45
+ main <js | -> in the main process; 'electron' and 'process' are in scope
46
+ '-' reads the code from stdin
47
+ logs [--lines <n>] recent console output, plus 'logFile' from the config
48
+ stop quit the app and the daemon
49
+ `
50
+
51
+ const fail = (message) => {
52
+ console.error(message)
53
+ process.exit(1)
54
+ }
55
+
56
+ const send = (command, args = {}) => {
57
+ const state = readState()
58
+ if (!state || !isAlive(state.pid)) {
59
+ if (state) rmSync(statePath(), { force: true })
60
+ return fail("Nothing is running. Start it with 'drive start'.")
61
+ }
62
+
63
+ const body = JSON.stringify({ token: state.token, command, args })
64
+ return new Promise((resolve, reject) => {
65
+ const request = http.request(
66
+ { host: '127.0.0.1', port: state.port, method: 'POST', headers: { 'content-type': 'application/json' } },
67
+ (response) => {
68
+ let data = ''
69
+ response.on('data', (chunk) => {
70
+ data += chunk
71
+ })
72
+ response.on('end', () => {
73
+ try {
74
+ const parsed = JSON.parse(data)
75
+ if (parsed.ok) resolve(parsed.result)
76
+ else reject(new Error(parsed.error))
77
+ } catch (e) {
78
+ reject(e)
79
+ }
80
+ })
81
+ }
82
+ )
83
+ request.on('error', reject)
84
+ request.end(body)
85
+ })
86
+ }
87
+
88
+ const print = (result) => {
89
+ if (typeof result === 'string') console.log(result)
90
+ else console.log(JSON.stringify(result, null, 2))
91
+ }
92
+
93
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
94
+
95
+ const urlUp = (url) =>
96
+ new Promise((resolve) => {
97
+ const request = http.get(url, { timeout: 1000 }, (response) => {
98
+ response.resume()
99
+ resolve(true)
100
+ })
101
+ request.on('error', () => resolve(false))
102
+ request.on('timeout', () => {
103
+ request.destroy()
104
+ resolve(false)
105
+ })
106
+ })
107
+
108
+ /** The file Electron will run for `args[0]`: the file itself, or a package's `main`. */
109
+ const entryFile = (target) => {
110
+ const resolved = path.resolve(PROJECT_ROOT, target)
111
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) return resolved
112
+ const pkg = JSON.parse(readFileSync(path.join(resolved, 'package.json'), 'utf8'))
113
+ return path.join(resolved, pkg.main ?? 'index.js')
114
+ }
115
+
116
+ const start = async (flags) => {
117
+ const options = startOptions(flags)
118
+ const config = loadConfig()
119
+ const existing = readState()
120
+ if (existing && isAlive(existing.pid)) {
121
+ fail(`Already driving the app (profile '${existing.profile}', ${existing.mode}). Run 'drive stop' first.`)
122
+ }
123
+ if (existing) rmSync(statePath(), { force: true })
124
+
125
+ if (options.mode === 'dev') {
126
+ if (options.build) fail("--build applies to the built app only. With --dev, the dev server rebuilds the renderer.")
127
+ if (config.dev.url && !(await urlUp(config.dev.url))) {
128
+ fail(`The renderer dev server is not up at ${config.dev.url}. Start it first, without letting it launch its own Electron.`)
129
+ }
130
+ } else {
131
+ if (options.build) {
132
+ if (!config.build) fail("--build needs a 'build' command in drive.config.json.")
133
+ console.log(`Building (${config.build})...`)
134
+ const built = spawnSync(config.build, { cwd: PROJECT_ROOT, stdio: 'inherit', shell: true })
135
+ if (built.status !== 0) fail(`'${config.build}' failed.`)
136
+ }
137
+ const entry = entryFile(config.args[0] ?? '.')
138
+ if (!existsSync(entry)) fail(`No built app: ${entry} does not exist. Run 'drive start --build'.`)
139
+ const ageMinutes = Math.round((Date.now() - statSync(entry).mtimeMs) / 60000)
140
+ console.log(`Using the build from ${ageMinutes} minute(s) ago. Pass --build to rebuild.`)
141
+ }
142
+
143
+ const dataDir = profileDir(options.profile)
144
+ if (options.fresh) rmSync(dataDir, { recursive: true, force: true })
145
+ mkdirSync(dataDir, { recursive: true })
146
+ mkdirSync(driveRoot(), { recursive: true })
147
+
148
+ const logFd = openSync(daemonLogPath(), 'w')
149
+ const daemon = spawn(
150
+ process.execPath,
151
+ [path.join(here, 'daemon.mjs'), options.mode, options.profile, dataDir],
152
+ {
153
+ cwd: PROJECT_ROOT,
154
+ detached: true,
155
+ stdio: ['ignore', logFd, logFd],
156
+ env: { ...process.env, DRIVE_PROJECT_ROOT: PROJECT_ROOT }
157
+ }
158
+ )
159
+ daemon.unref()
160
+
161
+ console.log(`Starting (${options.mode}, profile '${options.profile}')...`)
162
+ const deadline = Date.now() + 120_000
163
+ while (Date.now() < deadline) {
164
+ if (readState()) break
165
+ if (!isAlive(daemon.pid)) {
166
+ console.error(readFileSync(daemonLogPath(), 'utf8'))
167
+ fail('The driver failed to start the app (log above).')
168
+ }
169
+ await sleep(500)
170
+ }
171
+ if (!readState()) fail(`Timed out waiting for the app. See ${daemonLogPath()}.`)
172
+ print(await send('status'))
173
+ }
174
+
175
+ /** The code argument, or stdin when it is '-'. */
176
+ const codeFrom = (positional) =>
177
+ positional.length === 1 && positional[0] === '-' ? readFileSync(0, 'utf8') : positional.join(' ')
178
+
179
+ const run = async () => {
180
+ const { command, positional, flags } = parseArgs(process.argv.slice(2))
181
+ const need = (count, what) => {
182
+ if (positional.length < count) fail(`Usage: drive ${command} ${what}`)
183
+ }
184
+
185
+ switch (command) {
186
+ case 'start':
187
+ return start(flags)
188
+ case 'status': {
189
+ const state = readState()
190
+ if (!state || !isAlive(state.pid)) return print('Not running.')
191
+ return print(await send('status'))
192
+ }
193
+ case 'stop': {
194
+ const state = readState()
195
+ if (!state || !isAlive(state.pid)) {
196
+ rmSync(statePath(), { force: true })
197
+ if (state?.electronPid && isAlive(state.electronPid)) {
198
+ process.kill(state.electronPid, 'SIGKILL')
199
+ return print('The daemon was gone but the app was not; killed it.')
200
+ }
201
+ return print('Not running.')
202
+ }
203
+ return print(await send('stop'))
204
+ }
205
+ case 'snapshot':
206
+ return print(await send('snapshot'))
207
+ case 'screenshot':
208
+ return print(
209
+ await send('screenshot', {
210
+ out: typeof flags.out === 'string' ? path.resolve(flags.out) : undefined,
211
+ selector: typeof flags.selector === 'string' ? flags.selector : undefined
212
+ })
213
+ )
214
+ case 'click':
215
+ need(1, '<target>')
216
+ return print(await send('click', { target: positional[0] }))
217
+ case 'fill':
218
+ need(2, '<target> <text>')
219
+ return print(await send('fill', { target: positional[0], text: positional.slice(1).join(' ') }))
220
+ case 'select':
221
+ need(2, '<target> <value|label>')
222
+ return print(await send('select', { target: positional[0], value: positional.slice(1).join(' ') }))
223
+ case 'press':
224
+ need(1, '<key>')
225
+ return print(await send('press', { key: positional[0] }))
226
+ case 'wait':
227
+ need(1, '<target>')
228
+ return print(
229
+ await send('wait', {
230
+ target: positional[0],
231
+ timeout: typeof flags.timeout === 'string' ? Number(flags.timeout) : undefined
232
+ })
233
+ )
234
+ case 'eval':
235
+ need(1, '<js>')
236
+ return print(await send('eval', { code: codeFrom(positional) }))
237
+ case 'main':
238
+ need(1, '<js>')
239
+ return print(await send('main', { code: codeFrom(positional) }))
240
+ case 'logs':
241
+ return print(await send('logs', { lines: typeof flags.lines === 'string' ? Number(flags.lines) : undefined }))
242
+ case 'help':
243
+ case '--help':
244
+ return print(USAGE)
245
+ default:
246
+ return fail(`Unknown command '${command}'.\n\n${USAGE}`)
247
+ }
248
+ }
249
+
250
+ run().catch((e) => fail(e.message))
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The pure half of the driver: project discovery, config, argument parsing,
3
+ * paths and the state file. No Playwright and no Electron here, so it can be
4
+ * tested without launching anything (`node --test scripts/`).
5
+ */
6
+ import { createHash } from 'node:crypto'
7
+ import { existsSync, readFileSync } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import path from 'node:path'
10
+
11
+ /** The nearest directory at or above `from` with a package.json. */
12
+ export const findProjectRoot = (from = process.cwd()) => {
13
+ let dir = path.resolve(from)
14
+ for (;;) {
15
+ if (existsSync(path.join(dir, 'package.json'))) return dir
16
+ const parent = path.dirname(dir)
17
+ if (parent === dir) throw new Error(`No package.json at or above ${from}. Run the driver from inside your Electron project.`)
18
+ dir = parent
19
+ }
20
+ }
21
+
22
+ export const PROJECT_ROOT = process.env.DRIVE_PROJECT_ROOT
23
+ ? path.resolve(process.env.DRIVE_PROJECT_ROOT)
24
+ : findProjectRoot()
25
+
26
+ /**
27
+ * `drive.config.json` at the project root, over the defaults. Every field is
28
+ * optional; with no file at all, `start` runs `electron .` on the last build.
29
+ */
30
+ export const loadConfig = (root = PROJECT_ROOT) => {
31
+ const file = path.join(root, 'drive.config.json')
32
+ let config = {}
33
+ if (existsSync(file)) {
34
+ try {
35
+ config = JSON.parse(readFileSync(file, 'utf8'))
36
+ } catch (e) {
37
+ throw new Error(`${file} is not valid JSON: ${e.message}`)
38
+ }
39
+ }
40
+ return {
41
+ build: config.build ?? null,
42
+ args: config.args ?? ['.'],
43
+ env: config.env ?? {},
44
+ logFile: config.logFile ?? null,
45
+ dev: {
46
+ args: config.dev?.args ?? config.args ?? ['.'],
47
+ env: config.dev?.env ?? {},
48
+ url: config.dev?.url ?? null
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Everything the driver owns lives under here, never in the real profile. One
55
+ * directory per project, so two apps never share a scratch profile.
56
+ */
57
+ export const driveRoot = (root = PROJECT_ROOT) => {
58
+ if (process.env.DRIVE_ROOT) return process.env.DRIVE_ROOT
59
+ const hash = createHash('sha256').update(root).digest('hex').slice(0, 8)
60
+ return path.join(tmpdir(), 'electron-drive', `${path.basename(root)}-${hash}`)
61
+ }
62
+
63
+ export const statePath = () => path.join(driveRoot(), 'daemon.json')
64
+ export const daemonLogPath = () => path.join(driveRoot(), 'daemon.log')
65
+ export const screenshotDir = () => path.join(driveRoot(), 'screenshots')
66
+
67
+ /**
68
+ * A profile name becomes a directory name, so it is restricted to something
69
+ * that cannot climb out of `profiles/`.
70
+ */
71
+ export const profileDir = (profile) => {
72
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(profile)) {
73
+ throw new Error(`Invalid profile name '${profile}': letters, digits, '-' and '_' only.`)
74
+ }
75
+ return path.join(driveRoot(), 'profiles', profile)
76
+ }
77
+
78
+ export const readState = () => {
79
+ const file = statePath()
80
+ if (!existsSync(file)) return null
81
+ try {
82
+ return JSON.parse(readFileSync(file, 'utf8'))
83
+ } catch {
84
+ return null
85
+ }
86
+ }
87
+
88
+ export const isAlive = (pid) => {
89
+ if (!pid) return false
90
+ try {
91
+ process.kill(pid, 0)
92
+ return true
93
+ } catch (e) {
94
+ return e?.code === 'EPERM'
95
+ }
96
+ }
97
+
98
+ const BOOLEAN_FLAGS = new Set(['dev', 'build', 'fresh'])
99
+
100
+ /**
101
+ * `drive <command> [positional...] [--flag [value]]`.
102
+ *
103
+ * Flags known to be boolean take no value, so `start --dev --fresh` is two
104
+ * flags rather than `dev = '--fresh'`. Everything after a bare `--` is
105
+ * positional, for JavaScript that itself starts with dashes.
106
+ */
107
+ export const parseArgs = (argv) => {
108
+ const [command = 'help', ...rest] = argv
109
+ const positional = []
110
+ const flags = {}
111
+
112
+ for (let i = 0; i < rest.length; i += 1) {
113
+ const arg = rest[i]
114
+ if (arg === '--') {
115
+ positional.push(...rest.slice(i + 1))
116
+ break
117
+ }
118
+ if (arg.startsWith('--')) {
119
+ const name = arg.slice(2)
120
+ const next = rest[i + 1]
121
+ if (BOOLEAN_FLAGS.has(name) || next === undefined || next.startsWith('--')) {
122
+ flags[name] = true
123
+ } else {
124
+ flags[name] = next
125
+ i += 1
126
+ }
127
+ } else {
128
+ positional.push(arg)
129
+ }
130
+ }
131
+
132
+ return { command, positional, flags }
133
+ }
134
+
135
+ export const startOptions = (flags) => ({
136
+ mode: flags.dev ? 'dev' : 'built',
137
+ build: flags.build === true,
138
+ profile: typeof flags.profile === 'string' ? flags.profile : 'default',
139
+ fresh: flags.fresh === true
140
+ })
141
+
142
+ /**
143
+ * The child's environment: the caller's, minus the variables that break a
144
+ * launch, plus the config's, where `{profile}` becomes the scratch profile
145
+ * directory.
146
+ *
147
+ * `ELECTRON_RUN_AS_NODE=1` makes Electron start as plain Node, where `app` is
148
+ * undefined and nothing works, and some shells set it.
149
+ */
150
+ export const childEnv = (base, dataDir, mode, extra) => {
151
+ const env = { ...base }
152
+ delete env.ELECTRON_RUN_AS_NODE
153
+ if (mode === 'dev') env.NODE_ENV = 'development'
154
+ else delete env.NODE_ENV
155
+ for (const [key, value] of Object.entries(extra)) {
156
+ env[key] = String(value).replaceAll('{profile}', dataDir)
157
+ }
158
+ return env
159
+ }
160
+
161
+ /**
162
+ * JavaScript from the command line, made into an async function body: a bare
163
+ * expression is returned, anything with its own `return` is used as written.
164
+ */
165
+ export const asFunctionBody = (code) => (/\breturn\b/.test(code) ? code : `return (${code})`)
@@ -0,0 +1,56 @@
1
+ // node --test scripts/
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import path from 'node:path'
6
+ import { test } from 'node:test'
7
+
8
+ const project = mkdtempSync(path.join(tmpdir(), 'drive-test-'))
9
+ writeFileSync(path.join(project, 'package.json'), '{}')
10
+ process.env.DRIVE_PROJECT_ROOT = project
11
+ delete process.env.DRIVE_ROOT
12
+ const lib = await import('./lib.mjs')
13
+
14
+ test('parses commands, valued and boolean flags, and --', () => {
15
+ assert.deepEqual(lib.parseArgs(['fill', 'role=textbox', 'hello', 'world']), {
16
+ command: 'fill',
17
+ positional: ['role=textbox', 'hello', 'world'],
18
+ flags: {}
19
+ })
20
+ assert.deepEqual(lib.parseArgs(['start', '--dev', '--profile', 'x', '--fresh']).flags, {
21
+ dev: true,
22
+ profile: 'x',
23
+ fresh: true
24
+ })
25
+ assert.deepEqual(lib.parseArgs(['eval', '--', '--x', '1']).positional, ['--x', '1'])
26
+ assert.equal(lib.parseArgs([]).command, 'help')
27
+ })
28
+
29
+ test('keeps profiles inside the driver root', () => {
30
+ assert.ok(lib.profileDir('default').startsWith(lib.driveRoot()))
31
+ for (const bad of ['../../Library', 'a/b', '']) assert.throws(() => lib.profileDir(bad), /Invalid profile/)
32
+ })
33
+
34
+ test('child env drops ELECTRON_RUN_AS_NODE and fills {profile}', () => {
35
+ const env = lib.childEnv({ ELECTRON_RUN_AS_NODE: '1', NODE_ENV: 'test', KEEP: 'me' }, '/scratch', 'built', {
36
+ APP_DATA: '{profile}/data'
37
+ })
38
+ assert.deepEqual(env, { KEEP: 'me', APP_DATA: '/scratch/data' })
39
+ assert.equal(lib.childEnv({}, '/s', 'dev', {}).NODE_ENV, 'development')
40
+ })
41
+
42
+ test('config defaults to electron . with no file', () => {
43
+ assert.deepEqual(lib.loadConfig(project).args, ['.'])
44
+ assert.deepEqual(lib.loadConfig(project).dev.args, ['.'])
45
+ })
46
+
47
+ test('a malformed config names the file', () => {
48
+ const broken = mkdtempSync(path.join(tmpdir(), 'drive-test-'))
49
+ writeFileSync(path.join(broken, 'drive.config.json'), '{ "args": [".",] }')
50
+ assert.throws(() => lib.loadConfig(broken), /drive\.config\.json is not valid JSON/)
51
+ })
52
+
53
+ test('wraps bare expressions, keeps bodies with return', () => {
54
+ assert.equal(lib.asFunctionBody('1 + 1'), 'return (1 + 1)')
55
+ assert.equal(lib.asFunctionBody('return 2'), 'return 2')
56
+ })
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: film-crew
3
+ description: "Turn a one-line AI video idea into a shot list and per-shot, model-ready prompts via a film crew (director, DP, gaffer, editor, script supervisor). Also fixes failing video prompts and diagnoses bad clips before a reroll. Works with Wan, LTX, Kling, Veo, Seedance, Hailuo, Runway."
4
+ category: media
5
+ risk: safe
6
+ source: "https://github.com/HEOJUNFO/ai-film-crew/tree/533f61be2741c10932c6c86e47777cac9ffe3fe4/skills/film-crew"
7
+ source_repo: HEOJUNFO/ai-film-crew
8
+ source_type: official
9
+ date_added: "2026-09-25"
10
+ author: HEOJUNFO
11
+ license: MIT
12
+ license_source: "https://github.com/HEOJUNFO/ai-film-crew/blob/533f61be2741c10932c6c86e47777cac9ffe3fe4/LICENSE"
13
+ tags: [video, text-to-video, shot-list, prompting, film]
14
+ tools: [claude, codex, cursor, gemini]
15
+ ---
16
+
17
+ # Film Crew
18
+
19
+ You are the producer. You don't write one prompt and hope. You run the idea past a crew,
20
+ each with one job, then hand the user a shot list and one prompt per shot that a video
21
+ model can actually execute.
22
+
23
+ Most failed AI video clips fail for boring, fixable reasons: two actions in one clip,
24
+ no camera instruction, a subject described differently in every shot, physics the model
25
+ can't do in 5 seconds, or a prompt that describes a *feeling* instead of a *frame*.
26
+ The crew exists to catch those before generation, and to diagnose them after.
27
+
28
+ ## When to Use This Skill
29
+
30
+ - Use when someone wants to plan an AI video (ad, short, music video intro) before generating clips.
31
+ - Use when a text-to-video or image-to-video prompt keeps failing and needs a diagnosis and rewrite.
32
+ - Use after a bad generation to decide the one change to make before the next reroll.
33
+ - Do not use it to generate, edit or render video; it produces planning documents and prompts only.
34
+
35
+ ## Modes
36
+
37
+ Pick the mode from the request. Default is **Plan**.
38
+
39
+ | Mode | When | Output |
40
+ |---|---|---|
41
+ | **Plan** | "make a video of…", an idea, a script, a product | `SHOT_LIST.md` + per-shot prompts |
42
+ | **Fix** | user pastes a prompt that isn't working | diagnosis + rewritten prompt |
43
+ | **Review** | user describes or shares a generated clip | what went wrong + the one change to make before the next reroll |
44
+
45
+ ## Plan mode
46
+
47
+ ### 0. Intake (keep it short)
48
+
49
+ Only ask what you can't reasonably default. Ask at most 3 questions, in one message.
50
+ If the user said "just go", use the defaults.
51
+
52
+ | Field | Default |
53
+ |---|---|
54
+ | Platform / aspect | 9:16 vertical, short-form feed |
55
+ | Total length | 15 s |
56
+ | Target model | model-agnostic (write the generic adapter) |
57
+ | Mode per shot | text-to-video, unless the user has a reference image. **Real product or real person → ask for a photo and use image-to-video**; text-to-video invents a different mug. |
58
+ | Audio | none in the prompt, unless the target model generates audio *and* the user wants it in-model (otherwise add it in the edit) |
59
+
60
+ ### 1. Run the crew, in order
61
+
62
+ Read each role file before running that role. Each role writes its own short section.
63
+ Keep every section terse: the output is a production document, not an essay.
64
+
65
+ 1. **Director** → `references/roles/director.md` — logline, intent, beat sheet with timings, the hook.
66
+ 2. **Production designer** → `references/roles/production-designer.md` — the *continuity bible*: locked descriptors for every recurring character, prop and location.
67
+ 3. **Director of photography** → `references/roles/dp.md` — per shot: size, lens, angle, height, ONE camera move.
68
+ 4. **Gaffer** → `references/roles/gaffer.md` — time of day, key direction, color temperature, practicals, contrast.
69
+ 5. **Editor** → `references/roles/editor.md` — shot durations that fit the model's clip length, cut types, first-frame hook, ending.
70
+ 6. **Sound** → `references/roles/sound.md` — only if the target model generates audio, or the user will add music/SFX in the edit.
71
+ 7. **Script supervisor** → `references/roles/script-supervisor.md` — continuity + feasibility + "unslop" pass. Has veto power: any shot it flags gets rewritten before output.
72
+
73
+ ### 2. Write the prompts
74
+
75
+ Read `references/model-prompting.md`. For each shot, assemble the prompt from the crew's
76
+ decisions in this order, then run it through the target model's adapter:
77
+
78
+ ```
79
+ [shot size + lens + camera move] of [subject, verbatim from the continuity bible]
80
+ [doing ONE visible action, present tense], in [location, verbatim from the bible].
81
+ [lighting from the gaffer]. [style / film look]. [audio line, only if supported]
82
+ ```
83
+
84
+ This order is the generic default. When the target model's adapter specifies a different
85
+ order, **the adapter wins**.
86
+
87
+ Rules that apply to every model:
88
+ - One subject action and one camera move per shot. Two actions = two shots.
89
+ - Paste continuity descriptors **verbatim** in every shot: the FULL version when the whole subject is in frame, the TIGHT version for close-ups. Never paraphrase either.
90
+ - Describe what the camera sees, not what the viewer should feel. "Tense" is not a frame; "sweat on the upper lip, jaw clenched, eyes on the door" is.
91
+ - No on-screen text, logos or readable signage unless the model is known to render text. Add text in the edit.
92
+ - Keep each shot within the model's max clip length (see adapter). Plan for 1–2 s of handles.
93
+ - Image-to-video: describe only motion and camera. Don't re-describe what's already in the image.
94
+
95
+ ### 3. Output
96
+
97
+ Write `SHOT_LIST.md` using `references/templates/shot-list.md`. If the user is working in a repo
98
+ or folder, save it there; otherwise print it. Include:
99
+ - the crew sections (short),
100
+ - the shot table,
101
+ - one copy-paste prompt block per shot (plus negative prompt where the model supports it),
102
+ - a **reroll plan**: for each shot, the most likely failure and what to change if it happens.
103
+
104
+ End with one line telling the user which shot is highest-risk and why.
105
+
106
+ ## Fix mode
107
+
108
+ 1. Read `references/roles/script-supervisor.md` and `references/unslop.md`.
109
+ 2. Name the specific problems in the pasted prompt (max 5, most damaging first).
110
+ 3. Rewrite it using the Plan-mode prompt order. If it contains two actions, split it into two shots and say so.
111
+ 4. Show before → after. Keep the user's intent; change the mechanics.
112
+
113
+ ## Review mode
114
+
115
+ Read `references/reroll-review.md`. Diagnose from what the user describes or shares
116
+ (frames, a clip, a description). Classify the failure, then give **one** change to make
117
+ before the next generation. Changing five things at once means you learn nothing from the reroll.
118
+
119
+ ## Limitations
120
+
121
+ - It plans, diagnoses, and rewrites prompts; it does not generate, edit, or render video.
122
+ - Output quality depends on the target model's current clip limits and prompt handling, which change often. Check the model's own docs before trusting an adapter's defaults.
123
+ - Continuity locks hold only as far as the model honors them; long sequences may still drift and need a reroll.
124
+ - It has no way to see a clip unless the user shares frames, a file, or a description of what happened.
125
+
126
+ ## Tone
127
+
128
+ Talk like a working crew: specific, fast, no hype. Use real film vocabulary (it helps the
129
+ models), but explain any term the user might not know in 3–5 words the first time.
@@ -0,0 +1,89 @@
1
+ # Model prompting adapters
2
+
3
+ The crew's decisions are model-agnostic. This file turns them into the phrasing each model
4
+ family tends to follow best.
5
+
6
+ > Video models change every few months. Treat these as conservative defaults, check the
7
+ > model's current docs for hard limits (clip length, resolution, prompt length), and send a
8
+ > PR when something here goes stale.
9
+
10
+ ## Generic adapter (use when the model is unknown)
11
+
12
+ One paragraph, present tense, concrete, in this order:
13
+
14
+ 1. Shot size, lens, camera move
15
+ 2. Subject (verbatim from the continuity bible)
16
+ 3. The one action
17
+ 4. Location (verbatim)
18
+ 5. Lighting
19
+ 6. Look / film stock
20
+ 7. Audio (only if the model generates audio)
21
+
22
+ ~40–90 words. Longer is fine for open-weights models; shorter for models with prompt
23
+ rewriting ("prompt enhancement") turned on.
24
+
25
+ ## Open-weights models (Wan, HunyuanVideo, LTX-Video, Mochi, CogVideoX…)
26
+
27
+ - **Be literal and long.** These models generally reward detailed, chronological,
28
+ paragraph-style descriptions (roughly 60–150 words): what's in frame, what moves, in
29
+ what order.
30
+ - **Describe motion in sequence:** "…lifts the cup, pauses, then sets it down."
31
+ - **Use the negative prompt** if your workflow exposes one:
32
+ `blurry, warped hands, extra fingers, morphing, flicker, text, watermark, logo, static frame, oversaturated`
33
+ - **Seed discipline:** when rerolling to test a single change, keep the seed fixed so you
34
+ isolate the effect of the change.
35
+ - Clip length is bounded by frame count and VRAM. Plan short shots and cut them together.
36
+
37
+ ## Kling
38
+
39
+ - Structure: subject → movement → scene → camera → lighting → atmosphere.
40
+ - Keep it tighter than open-weights prompts; one clear action.
41
+ - Clip length is chosen from presets (commonly 5 s or 10 s). Plan with the editor's
42
+ fixed-length rule.
43
+ - Negative prompt: use it where your version/interface exposes the field.
44
+ - Some newer versions can generate audio. If yours does and the user wants in-model
45
+ sound, apply the audio rules from the Veo section; otherwise keep sound out of the prompt.
46
+ - Image-to-video: describe only the motion and the camera; the image already carries the
47
+ look.
48
+
49
+ ## Veo (and other models that generate audio)
50
+
51
+ - Write sound explicitly, as its own sentence(s):
52
+ - Dialogue: `The barista says, "Sorry — my fault."`
53
+ - SFX: `SFX: paper cup hits tile, liquid splash.`
54
+ - Ambience: `Ambient café murmur, espresso machine hiss.`
55
+ - Keep dialogue short (one line per shot). Long lines drift out of sync.
56
+ - Camera and lens terminology is followed well; use it.
57
+
58
+ ## Seedance and other multi-shot-capable models
59
+
60
+ - If your version supports multiple shots in one generation, you can write the sequence
61
+ explicitly: `Shot 1: … Cut to Shot 2: …`. Keep each shot to one action and restate the
62
+ character descriptors in each.
63
+ - If you need tight control, still generate shot by shot and cut in the edit.
64
+
65
+ ## Hailuo / MiniMax
66
+
67
+ - Camera moves can be stated plainly in the prompt ("slow push-in", "pan left"). Some
68
+ versions and front-ends accept bracketed camera commands such as `[Push in]` or
69
+ `[Pan left]`; use them if your interface documents them.
70
+ - Keep one move per shot even when the syntax allows chaining several.
71
+
72
+ ## Runway / Luma / Pika / Sora-style prompt-enhanced models
73
+
74
+ - These often rewrite your prompt. Put the non-negotiables first (subject descriptors,
75
+ the action, the camera move) and keep the prompt short enough that the rewrite doesn't
76
+ bury them.
77
+ - For image-to-video: motion + camera only.
78
+
79
+ ## Image-to-video (every model)
80
+
81
+ The most reliable way to keep a character consistent across shots:
82
+ 1. Generate or pick **one** reference still per character/location.
83
+ 2. Use it as the first frame for every shot where they appear.
84
+ 3. In the prompt, describe **only** what moves and how the camera moves.
85
+
86
+ ## Clip length cheat sheet
87
+
88
+ Check your model's current limit. Common ranges in practice: 4–10 s per generation.
89
+ The editor role should never plan a shot longer than the limit minus ~1 s of handles.