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.
- package/bundled-skills/.antigravity-install-manifest.json +16 -1
- package/bundled-skills/claude-api/SKILL.md +4 -4
- package/bundled-skills/claude-api/curl/examples.md +5 -5
- package/bundled-skills/claude-api/java/claude-api.md +1 -1
- package/bundled-skills/claude-api/php/claude-api.md +2 -2
- package/bundled-skills/claude-api/python/claude-api/README.md +13 -13
- package/bundled-skills/claude-api/python/claude-api/batches.md +3 -3
- package/bundled-skills/claude-api/python/claude-api/files-api.md +3 -3
- package/bundled-skills/claude-api/python/claude-api/streaming.md +6 -6
- package/bundled-skills/claude-api/python/claude-api/tool-use.md +19 -19
- package/bundled-skills/claude-api/ruby/claude-api.md +3 -3
- package/bundled-skills/claude-api/shared/error-codes.md +2 -2
- package/bundled-skills/claude-api/shared/live-sources.md +1 -1
- package/bundled-skills/claude-api/shared/models.md +4 -4
- package/bundled-skills/claude-api/shared/tool-use-concepts.md +2 -2
- package/bundled-skills/claude-api/typescript/claude-api/README.md +12 -12
- package/bundled-skills/claude-api/typescript/claude-api/batches.md +2 -2
- package/bundled-skills/claude-api/typescript/claude-api/files-api.md +1 -1
- package/bundled-skills/claude-api/typescript/claude-api/streaming.md +4 -4
- package/bundled-skills/claude-api/typescript/claude-api/tool-use.md +14 -14
- package/bundled-skills/electron-drive-skill/LICENSE +21 -0
- package/bundled-skills/electron-drive-skill/README.md +172 -0
- package/bundled-skills/electron-drive-skill/SKILL.md +237 -0
- package/bundled-skills/electron-drive-skill/drive.config.json.example +10 -0
- package/bundled-skills/electron-drive-skill/package.json +28 -0
- package/bundled-skills/electron-drive-skill/scripts/daemon.mjs +352 -0
- package/bundled-skills/electron-drive-skill/scripts/drive.mjs +250 -0
- package/bundled-skills/electron-drive-skill/scripts/lib.mjs +165 -0
- package/bundled-skills/electron-drive-skill/scripts/lib.test.mjs +56 -0
- package/bundled-skills/film-crew/SKILL.md +129 -0
- package/bundled-skills/film-crew/references/model-prompting.md +89 -0
- package/bundled-skills/film-crew/references/reroll-review.md +46 -0
- package/bundled-skills/film-crew/references/roles/director.md +38 -0
- package/bundled-skills/film-crew/references/roles/dp.md +28 -0
- package/bundled-skills/film-crew/references/roles/editor.md +34 -0
- package/bundled-skills/film-crew/references/roles/gaffer.md +30 -0
- package/bundled-skills/film-crew/references/roles/production-designer.md +43 -0
- package/bundled-skills/film-crew/references/roles/script-supervisor.md +35 -0
- package/bundled-skills/film-crew/references/roles/sound.md +23 -0
- package/bundled-skills/film-crew/references/templates/shot-list.md +38 -0
- package/bundled-skills/film-crew/references/unslop.md +29 -0
- package/bundled-skills/mcp-dependency-drift-audit/SKILL.md +169 -0
- package/bundled-skills/nexrad-mosaic-access/SKILL.md +297 -0
- package/bundled-skills/nexrad-mosaic-construction/SKILL.md +277 -0
- package/bundled-skills/nexrad-product-access/SKILL.md +258 -0
- package/bundled-skills/nexrad-radar-visualization/SKILL.md +318 -0
- package/bundled-skills/radar-satellite-analysis/SKILL.md +314 -0
- package/bundled-skills/testdriver-e2e-testing/SKILL.md +121 -0
- package/bundled-skills/traderspy-market-briefing/SKILL.md +132 -0
- package/bundled-skills/traderspy-market-screener/SKILL.md +182 -0
- package/bundled-skills/traderspy-market-screener/references/condition-cookbook.md +70 -0
- package/bundled-skills/traderspy-position-check/SKILL.md +130 -0
- package/bundled-skills/traderspy-position-check/references/risk-math.md +60 -0
- package/bundled-skills/traderspy-smart-money/SKILL.md +151 -0
- package/bundled-skills/traderspy-smart-money/references/exchange-notes.md +40 -0
- package/bundled-skills/traderspy-technical-analysis/SKILL.md +162 -0
- package/bundled-skills/traderspy-technical-analysis/references/derivatives-guide.md +57 -0
- package/bundled-skills/traderspy-technical-analysis/references/indicator-guide.md +47 -0
- package/bundled-skills/traderspy-trading-signals/SKILL.md +156 -0
- package/bundled-skills/traderspy-trading-signals/references/signal-fields.md +64 -0
- package/bundled-skills/unified-ai-gateway/SKILL.md +3 -3
- package/package.json +1 -1
- 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.
|