venice-video-harness 2.5.2 → 2.6.0

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 (65) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +55 -2
  3. package/dist/cli.js +0 -0
  4. package/dist/mini-drama/cli.d.ts +3 -0
  5. package/dist/mini-drama/cli.d.ts.map +1 -1
  6. package/dist/mini-drama/cli.js +222 -6
  7. package/dist/mini-drama/cli.js.map +1 -1
  8. package/dist/mini-drama/video-generator.d.ts.map +1 -1
  9. package/dist/mini-drama/video-generator.js +101 -3
  10. package/dist/mini-drama/video-generator.js.map +1 -1
  11. package/dist/mini-drama/workshop.d.ts +1 -0
  12. package/dist/mini-drama/workshop.d.ts.map +1 -1
  13. package/dist/mini-drama/workshop.js +66 -0
  14. package/dist/mini-drama/workshop.js.map +1 -1
  15. package/dist/session/context.d.ts +25 -0
  16. package/dist/session/context.d.ts.map +1 -0
  17. package/dist/session/context.js +86 -0
  18. package/dist/session/context.js.map +1 -0
  19. package/dist/session/jobs.d.ts +43 -0
  20. package/dist/session/jobs.d.ts.map +1 -0
  21. package/dist/session/jobs.js +110 -0
  22. package/dist/session/jobs.js.map +1 -0
  23. package/dist/session/output-router.d.ts +13 -0
  24. package/dist/session/output-router.d.ts.map +1 -0
  25. package/dist/session/output-router.js +67 -0
  26. package/dist/session/output-router.js.map +1 -0
  27. package/dist/session/program-context.d.ts +7 -0
  28. package/dist/session/program-context.d.ts.map +1 -0
  29. package/dist/session/program-context.js +98 -0
  30. package/dist/session/program-context.js.map +1 -0
  31. package/dist/session/program-runtime.d.ts +39 -0
  32. package/dist/session/program-runtime.d.ts.map +1 -0
  33. package/dist/session/program-runtime.js +149 -0
  34. package/dist/session/program-runtime.js.map +1 -0
  35. package/dist/session/shell.d.ts +18 -0
  36. package/dist/session/shell.d.ts.map +1 -0
  37. package/dist/session/shell.js +465 -0
  38. package/dist/session/shell.js.map +1 -0
  39. package/dist/session/status.d.ts +33 -0
  40. package/dist/session/status.d.ts.map +1 -0
  41. package/dist/session/status.js +160 -0
  42. package/dist/session/status.js.map +1 -0
  43. package/dist/user-config.d.ts +10 -0
  44. package/dist/user-config.d.ts.map +1 -1
  45. package/dist/user-config.js.map +1 -1
  46. package/dist/venice/audio.d.ts.map +1 -1
  47. package/dist/venice/audio.js +30 -6
  48. package/dist/venice/audio.js.map +1 -1
  49. package/dist/venice/client.d.ts +0 -2
  50. package/dist/venice/client.d.ts.map +1 -1
  51. package/dist/venice/client.js +16 -8
  52. package/dist/venice/client.js.map +1 -1
  53. package/dist/venice/job-store.d.ts +39 -0
  54. package/dist/venice/job-store.d.ts.map +1 -0
  55. package/dist/venice/job-store.js +119 -0
  56. package/dist/venice/job-store.js.map +1 -0
  57. package/dist/venice/operation-context.d.ts +47 -0
  58. package/dist/venice/operation-context.d.ts.map +1 -0
  59. package/dist/venice/operation-context.js +83 -0
  60. package/dist/venice/operation-context.js.map +1 -0
  61. package/dist/venice/video.d.ts +15 -0
  62. package/dist/venice/video.d.ts.map +1 -1
  63. package/dist/venice/video.js +73 -17
  64. package/dist/venice/video.js.map +1 -1
  65. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.6.0 — 2026-07-31
4
+
5
+ ### Added
6
+
7
+ - `venice-video shell` — a persistent interactive session. Commands run without
8
+ a process restart, so the Venice client's rate-limit pacing and caches stay
9
+ warm across a whole production instead of resetting on every invocation.
10
+ - A selected project and part. `use <project> [part]` makes `-p` and `-e`
11
+ optional on every command; `unuse` clears it. The selection is stored in user
12
+ config, so it applies to one-shot commands and survives restarts.
13
+ - Background commands: suffix any command with `&`, then `/jobs`,
14
+ `/jobs log <id>`, and `/jobs cancel <id>`. Output from a backgrounded render is
15
+ captured to its own buffer rather than sprayed over the prompt.
16
+ - `venice-video status` — reports where a project sits in the pipeline and which
17
+ command to run next.
18
+ - `venice-video queue` — lists Venice renders left in flight, with `prune` and
19
+ `clear` subcommands.
20
+ - Shell conveniences: tab completion for commands, flags, and project slugs; a
21
+ persistent history file; a context-aware prompt; `!<cmd>` passthrough to the
22
+ system shell.
23
+
24
+ ### Fixed
25
+
26
+ - **Interrupted renders no longer orphan paid work.** Each Venice `queue_id` is
27
+ now recorded to disk *before* polling starts. Re-running a command re-attaches
28
+ to the pending job instead of submitting and billing a second render. Previously
29
+ a Ctrl-C, crash, or dropped connection lost the id while Venice kept charging.
30
+ - `Ctrl-C` now cancels the in-flight operation through an `AbortSignal` threaded
31
+ into every Venice request, poll loop, and retry backoff, rather than only
32
+ killing the process between requests.
33
+ - Video polling gained an overall timeout and a consecutive-error ceiling, so a
34
+ wedged render fails instead of hanging indefinitely.
35
+ - Tests no longer inherit an ambient `VENICE_VIDEO_WORKSPACE`, which could
36
+ silently redirect the projects they create.
37
+
38
+ ## 2.5.3 — 2026-07-31
39
+
40
+ - Workshop generation now writes a formatted, self-contained `WORKSHOP.html` alongside JSON and Markdown.
41
+ - The HTML presents story, inputs, structure, visual language, cast, locations, production plan, references, open questions, and the shot script in a browser-friendly layout.
42
+ - The CLI opens `WORKSHOP.html` automatically in the default browser after generation/revision when running interactively; failure to open never fails the workshop.
43
+ - Workshop status now lists the HTML path, and all rendered content is HTML-escaped.
44
+
3
45
  ## 2.5.2 — 2026-07-31
4
46
 
5
47
  - The Creative references prompt now says to drag a file or directory into the terminal.
package/README.md CHANGED
@@ -215,8 +215,9 @@ The workshop develops the complete project—not only a shot list:
215
215
  - dialogue/audio approach and exact-lip-sync decisions
216
216
  - production-ready shot script, risks, and open questions
217
217
 
218
- It writes `WORKSHOP.md` for human review and `workshop.json` as the structured
219
- source. Iterate without losing project context:
218
+ It writes a formatted `WORKSHOP.html` for browser review, `WORKSHOP.md` as a
219
+ portable text version, and `workshop.json` as the structured source. In an
220
+ interactive terminal, the HTML opens automatically in your default browser. Iterate without losing project context:
220
221
 
221
222
  ```bash
222
223
  venice-video workshop -p ~/VeniceVideos/my-film --feedback "Make the middle more tense"
@@ -290,6 +291,58 @@ venice-video list-series
290
291
  venice-video --help
291
292
  ```
292
293
 
294
+ ### Interactive shell
295
+
296
+ Every command above also runs inside a persistent session:
297
+
298
+ ```bash
299
+ venice-video shell
300
+ ```
301
+
302
+ The shell keeps one warm process for the whole production, which changes three
303
+ things that matter over a long session:
304
+
305
+ - **A selected project and part.** `use <project> [part]` sets them once; after
306
+ that `-p` and `-e` are optional on every command and the prompt shows what you
307
+ are pointed at. `unuse` clears the selection. The selection persists across
308
+ shell restarts and applies to one-shot commands too.
309
+ - **Warm rate limiting and caches.** The Venice client's pacing state survives
310
+ between commands instead of resetting on every invocation, so back-to-back
311
+ generation stops tripping 429s.
312
+ - **Background commands.** Suffix any command with `&` to detach it, then keep
313
+ working. `/jobs` lists them with elapsed time and current progress detail,
314
+ `/jobs log <id>` replays captured output, `/jobs cancel <id>` aborts one.
315
+
316
+ ```
317
+ venice-video my-film · ep 01 › storyboard-episode
318
+ venice-video my-film · ep 01 › generate-videos &
319
+ [1] started in the background. Check with /jobs.
320
+ venice-video my-film · ep 01 › /jobs
321
+ [1] running 4m12s generate-videos — shot 3/12 polling
322
+ ```
323
+
324
+ Session extras: `Tab` completes commands, flags, and project slugs; `↑`/`↓` walk
325
+ a persistent history file; `Ctrl-C` cancels the running command without killing
326
+ the session (`Ctrl-D` or `/exit` leaves); `/help`, `/status`, `/jobs`, `/cd`, and
327
+ `/pwd` are shell meta-commands; `!<cmd>` runs something in your system shell.
328
+
329
+ ### Interrupted renders are resumable
330
+
331
+ The harness records each Venice `queue_id` to disk *before* it starts polling, so
332
+ a cancelled command, a crash, or a closed laptop no longer orphans a render you
333
+ have already paid for. Re-running the same command re-attaches to the pending job
334
+ and keeps polling it instead of submitting and billing a second one.
335
+
336
+ ```bash
337
+ venice-video queue # renders Venice still has in flight
338
+ venice-video queue prune # forget records too old for Venice to still hold
339
+ venice-video queue clear <id> # forget one record (does not refund it)
340
+ ```
341
+
342
+ The shell reports stranded renders in its banner on startup. Note the split:
343
+ `queue` is Venice's side of the work (real money, survives restarts), while
344
+ `/jobs` is only the background commands of the current session.
345
+
293
346
  For server environments, prefer `VENICE_API_KEY` instead of writing a user
294
347
  configuration file. Credential precedence is environment variable, then stored
295
348
  user configuration, then the repository `.env` compatibility path.
package/dist/cli.js CHANGED
File without changes
@@ -1,3 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import 'dotenv/config';
3
+ import { Command } from 'commander';
4
+ declare const program: Command;
5
+ export { program };
3
6
  //# sourceMappingURL=cli.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mini-drama/cli.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mini-drama/cli.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2HpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAu5G9B,OAAO,EAAE,OAAO,EAAE,CAAC"}
@@ -2,10 +2,11 @@
2
2
  import 'dotenv/config';
3
3
  import { Command } from 'commander';
4
4
  import { resolve, join, basename } from 'node:path';
5
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
5
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
6
6
  import { mkdir, readFile, writeFile, copyFile } from 'node:fs/promises';
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import { stdin } from 'node:process';
9
+ import { fileURLToPath } from 'node:url';
9
10
  import { createSeries, saveSeries, loadSeries, listSeries, addCharacter, getCharacter, addEpisode, getEpisodeDir, getCharacterDir, getLocationDir, getLocation, addLocation, locationSlugify, saveEpisodeScript, loadEpisodeScript, } from '../series/manager.js';
10
11
  import { FEMALE_BASE_TRAITS, MALE_BASE_TRAITS, DEFAULT_CHARACTER_CONSISTENCY_MODEL, DEFAULT_IMAGE_GENERATION_MODEL, DEFAULT_IMAGE_EDIT_MODEL, } from '../series/types.js';
11
12
  import { VeniceClient } from '../venice/client.js';
@@ -17,6 +18,11 @@ import { getVeniceApiKey } from '../config.js';
17
18
  import { promptChoice, promptText } from '../interactive.js';
18
19
  import { getConfigPath, getDefaultSetupWorkspace, getWorkspaceDir, maskApiKey, readUserConfig, updateUserConfig, validateVeniceApiKey, } from '../user-config.js';
19
20
  import { listVoices, filterVoices, auditionVoices } from '../venice/voices.js';
21
+ import { clearContext, MissingContextError, readContext, resolveProjectRef, setContext, } from '../session/context.js';
22
+ import { OperationAbortedError } from '../venice/operation-context.js';
23
+ import { applyContextDefaults } from '../session/program-context.js';
24
+ import { collectProjectStatus, formatProjectStatus } from '../session/status.js';
25
+ import { clearPendingJob, isStale, listPendingJobs, prunePendingJobs, } from '../venice/job-store.js';
20
26
  import { generateDialogueForShots, generateSoundEffect, generateMusic, generateSeedAudio, DEFAULT_VENICE_MUSIC_MODEL, DEFAULT_VENICE_SEED_AUDIO_MODEL, } from '../venice/audio.js';
21
27
  import { getMusicModel } from '../venice/models.js';
22
28
  import { buildImagePrompt, buildCharacterReferencePromptParts } from './prompt-builder.js';
@@ -32,13 +38,61 @@ import { buildGenerationPlan, saveGenerationPlan } from './generation-planner.js
32
38
  import { AUDIO_STRATEGY_CHOICES, VIDEO_FAMILY_CHOICES } from './choices.js';
33
39
  import { getProjectLanguage } from '../series/project-language.js';
34
40
  import { approveWorkshop, generateWorkshop, getWorkshopPath, inventoryReferencePath, loadWorkshop, saveWorkshop, } from './workshop.js';
41
+ // Read from package.json rather than a literal, which drifts on every release.
42
+ const packageVersion = (() => {
43
+ try {
44
+ const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url));
45
+ return JSON.parse(readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
46
+ }
47
+ catch {
48
+ return '0.0.0';
49
+ }
50
+ })();
35
51
  const program = new Command();
36
52
  program
37
53
  .name('venice-video')
38
54
  .description('Standalone consistency-first video production with Venice AI')
39
- .version('2.5.2')
55
+ .version(packageVersion)
40
56
  .option('--workspace <dir>', 'Workspace containing Venice Video projects');
41
57
  const VIDEO_FAMILIES = new Set(VIDEO_FAMILY_CHOICES.map(c => c.value));
58
+ function openInDefaultBrowser(path) {
59
+ if (process.env.VENICE_VIDEO_NO_OPEN === '1' || !process.stdout.isTTY)
60
+ return false;
61
+ const command = process.platform === 'darwin'
62
+ ? { name: 'open', args: [path] }
63
+ : process.platform === 'win32'
64
+ ? { name: 'cmd', args: ['/c', 'start', '', path] }
65
+ : { name: 'xdg-open', args: [path] };
66
+ const result = spawnSync(command.name, command.args, { stdio: 'ignore' });
67
+ return result.status === 0;
68
+ }
69
+ /**
70
+ * True when this file is the process entry point. The program is only parsed in
71
+ * that case, so `shell` (and tests) can import the same command tree without
72
+ * argv being consumed at import time.
73
+ *
74
+ * `bin` points straight at this file, so the comparison is against argv[1] with
75
+ * symlinks resolved. The basename fallback covers the tsx dev path (.ts entry
76
+ * for a .js module specifier) -- getting this wrong would leave the CLI silently
77
+ * doing nothing, so it errs toward "yes, run".
78
+ */
79
+ function isMainModule(moduleUrl) {
80
+ const entry = process.argv[1];
81
+ if (!entry)
82
+ return true;
83
+ const canonical = (path) => {
84
+ try {
85
+ return realpathSync(path);
86
+ }
87
+ catch {
88
+ return resolve(path);
89
+ }
90
+ };
91
+ const stripExtension = (path) => basename(path).replace(/\.[cm]?[jt]s$/, '');
92
+ const modulePath = canonical(fileURLToPath(moduleUrl));
93
+ const entryPath = canonical(entry);
94
+ return modulePath === entryPath || stripExtension(modulePath) === stripExtension(entryPath);
95
+ }
42
96
  function runCommand(command, args) {
43
97
  const result = spawnSync(command, args, {
44
98
  encoding: 'utf-8',
@@ -310,7 +364,7 @@ program
310
364
  console.log(` Script: ${existing.script.title} · ${existing.script.totalDuration} · ${existing.script.shots.length} shots`);
311
365
  console.log(` Open questions: ${existing.productionNotes.openQuestions.length}`);
312
366
  console.log(` Delivery: ${existing.productionNotes.delivery === '4k' ? '4K master after assembly' : 'Standard master'}`);
313
- console.log(` Files: ${getWorkshopPath(series)} · ${join(series.outputDir, 'WORKSHOP.md')}`);
367
+ console.log(` Files: ${getWorkshopPath(series)} · ${join(series.outputDir, 'WORKSHOP.html')} · ${join(series.outputDir, 'WORKSHOP.md')}`);
314
368
  }
315
369
  else {
316
370
  console.log(` Start: venice-video workshop -p "${series.outputDir}"`);
@@ -323,7 +377,10 @@ program
323
377
  await approveWorkshop(series, existing);
324
378
  console.log(`${language.projectNoun} workshop approved.`);
325
379
  console.log(' Aesthetic, characters, locations, and script are now production state.');
326
- console.log(` Review the script and assets listed in ${join(series.outputDir, 'WORKSHOP.md')}.`);
380
+ const approvedHtmlPath = join(series.outputDir, 'WORKSHOP.html');
381
+ console.log(` Review the approved workshop: ${approvedHtmlPath}`);
382
+ if (openInDefaultBrowser(approvedHtmlPath))
383
+ console.log(' Opened workshop in your default browser.');
327
384
  console.log(' The workshop remains your control center:');
328
385
  console.log(` venice-video workshop -p "${series.outputDir}" --status`);
329
386
  console.log(` venice-video workshop -p "${series.outputDir}" --feedback "..."`);
@@ -375,7 +432,12 @@ program
375
432
  console.log(` Script: ${draft.script.title} · ${draft.script.totalDuration} · ${draft.script.shots.length} shots`);
376
433
  console.log(` Open questions: ${draft.productionNotes.openQuestions.length}`);
377
434
  console.log(` Delivery: ${draft.productionNotes.delivery === '4k' ? '4K master after assembly' : 'Standard master'}`);
378
- console.log(` Review: ${join(series.outputDir, 'WORKSHOP.md')}`);
435
+ const htmlPath = join(series.outputDir, 'WORKSHOP.html');
436
+ console.log(` Review: ${htmlPath}`);
437
+ if (openInDefaultBrowser(htmlPath))
438
+ console.log(' Opened workshop in your default browser.');
439
+ else
440
+ console.log(' Open the HTML file above in your browser.');
379
441
  console.log(` Revise: venice-video workshop -p "${series.outputDir}" --feedback "..."`);
380
442
  console.log(` Approve: venice-video workshop -p "${series.outputDir}" --approve`);
381
443
  });
@@ -3043,7 +3105,161 @@ program
3043
3105
  await program.parseAsync(['', '', 'assemble-episode', '-p', opts.project, '-e', String(opts.episode)]);
3044
3106
  console.log('\n=== Production Complete ===');
3045
3107
  });
3046
- await program.parseAsync();
3108
+ // ── session: use / status / queue / shell ─────────────────────────────
3109
+ program
3110
+ .command('use')
3111
+ .description('Select the project (and episode) that -p / -e default to')
3112
+ .argument('[project]', 'Project slug, name, or directory')
3113
+ .option('-e, --episode <number>', 'Episode number to select', parseInt)
3114
+ .option('--clear', 'Clear the current selection', false)
3115
+ .action(async (projectRef, opts) => {
3116
+ if (opts.clear) {
3117
+ await clearContext();
3118
+ console.log('Selection cleared. Commands now require -p / -e again.');
3119
+ return;
3120
+ }
3121
+ if (!projectRef && opts.episode === undefined) {
3122
+ const context = await readContext();
3123
+ if (!context.project) {
3124
+ console.log('Nothing selected. Usage: venice-video use <project> [-e <episode>]');
3125
+ const workspace = await getWorkspaceDir(program.opts().workspace);
3126
+ const available = await listSeries(workspace);
3127
+ if (available.length > 0) {
3128
+ console.log('\nAvailable projects:');
3129
+ for (const entry of available)
3130
+ console.log(` ${entry.slug.padEnd(28)} ${entry.name}`);
3131
+ }
3132
+ return;
3133
+ }
3134
+ console.log(`Project: ${context.project}`);
3135
+ if (context.episode !== undefined)
3136
+ console.log(`Episode: ${context.episode}`);
3137
+ return;
3138
+ }
3139
+ const patch = {};
3140
+ if (projectRef) {
3141
+ const projectDir = await resolveProjectRef(projectRef, program.opts().workspace);
3142
+ const series = await loadSeries(projectDir);
3143
+ if (!series) {
3144
+ console.error(`No series.json found at ${projectDir}.`);
3145
+ console.error('Run `venice-video list-series` to see what is available.');
3146
+ process.exit(1);
3147
+ }
3148
+ patch.project = projectDir;
3149
+ }
3150
+ if (opts.episode !== undefined)
3151
+ patch.episode = opts.episode;
3152
+ const next = await setContext(patch);
3153
+ console.log(`Selected ${next.project}${next.episode !== undefined ? ` · episode ${next.episode}` : ''}`);
3154
+ });
3155
+ program
3156
+ .command('unuse')
3157
+ .description('Clear the selected project and episode')
3158
+ .action(async () => {
3159
+ await clearContext();
3160
+ console.log('Selection cleared.');
3161
+ });
3162
+ program
3163
+ .command('status')
3164
+ .description('Show pipeline state for a project and the next command to run')
3165
+ .option('-p, --project <dir>', 'Project directory (defaults to the selection)')
3166
+ .action(async (opts) => {
3167
+ const context = await readContext();
3168
+ const projectRef = opts.project ?? context.project;
3169
+ if (!projectRef) {
3170
+ console.log('No project selected. Run `venice-video use <project>` or pass -p <dir>.');
3171
+ return;
3172
+ }
3173
+ const projectDir = await resolveProjectRef(projectRef, program.opts().workspace);
3174
+ const status = await collectProjectStatus(projectDir);
3175
+ if (!status) {
3176
+ console.error(`No series.json found at ${projectDir}.`);
3177
+ process.exit(1);
3178
+ return;
3179
+ }
3180
+ console.log(formatProjectStatus(status, context.episode));
3181
+ });
3182
+ program
3183
+ .command('queue')
3184
+ .description('List Venice renders left in flight by an interrupted run')
3185
+ .argument('[action]', 'clear to drop a recorded job, prune to drop stale ones')
3186
+ .argument('[target]', 'Output path or queue id to clear')
3187
+ .action(async (action, target) => {
3188
+ if (action === 'prune') {
3189
+ const removed = await prunePendingJobs();
3190
+ console.log(`Pruned ${removed} stale job record(s).`);
3191
+ return;
3192
+ }
3193
+ if (action === 'clear') {
3194
+ if (!target) {
3195
+ console.error('Usage: venice-video queue clear <output-path|queue-id>');
3196
+ process.exit(1);
3197
+ return;
3198
+ }
3199
+ const pending = await listPendingJobs();
3200
+ const match = pending.find(job => job.queueId === target)
3201
+ ?? pending.find(job => job.outputPath === resolve(target))
3202
+ ?? pending.find(job => job.outputPath.endsWith(target));
3203
+ if (!match) {
3204
+ console.error(`No recorded job matching "${target}".`);
3205
+ process.exit(1);
3206
+ return;
3207
+ }
3208
+ await clearPendingJob(match.outputPath);
3209
+ console.log(`Dropped ${match.queueId} (${match.outputPath}).`);
3210
+ console.log('Note: this only forgets the id locally. Venice already charged for the render.');
3211
+ return;
3212
+ }
3213
+ const pending = await listPendingJobs();
3214
+ if (pending.length === 0) {
3215
+ console.log('No Venice jobs are recorded as in flight.');
3216
+ return;
3217
+ }
3218
+ console.log(`${pending.length} job(s) recorded as in flight:\n`);
3219
+ for (const job of pending) {
3220
+ const ageMin = Math.round((Date.now() - Date.parse(job.updatedAt)) / 60_000);
3221
+ console.log(` ${job.kind} ${job.model}`);
3222
+ console.log(` queue id ${job.queueId}`);
3223
+ console.log(` output ${job.outputPath}`);
3224
+ console.log(` last seen ${ageMin} min ago${isStale(job) ? ' (stale — Venice has likely dropped it)' : ''}`);
3225
+ if (job.prompt)
3226
+ console.log(` prompt ${job.prompt.slice(0, 70)}…`);
3227
+ console.log('');
3228
+ }
3229
+ console.log('Re-run the command that produced these to re-attach instead of re-billing.');
3230
+ });
3231
+ program
3232
+ .command('shell')
3233
+ .description('Start an interactive session that keeps the selected project and runs jobs in the background')
3234
+ .action(async () => {
3235
+ // Imported lazily so the shell (and its readline/job machinery) costs
3236
+ // nothing for one-shot invocations, and to avoid an import cycle back
3237
+ // into this module.
3238
+ const { startShell } = await import('../session/shell.js');
3239
+ await startShell(program);
3240
+ });
3241
+ applyContextDefaults(program);
3242
+ // Only parse when executed directly. The shell imports this module to reuse the
3243
+ // same command tree, and must not have argv consumed at import time.
3244
+ if (isMainModule(import.meta.url)) {
3245
+ try {
3246
+ await program.parseAsync();
3247
+ }
3248
+ catch (error) {
3249
+ // A missing project/episode selection is ordinary user error, not a crash —
3250
+ // report it the way Commander reports a missing required option.
3251
+ if (error instanceof MissingContextError) {
3252
+ console.error(`error: ${error.message}`);
3253
+ process.exit(1);
3254
+ }
3255
+ if (error instanceof OperationAbortedError) {
3256
+ console.error('Cancelled. Any in-flight Venice job is recorded — re-run to re-attach.');
3257
+ process.exit(130);
3258
+ }
3259
+ throw error;
3260
+ }
3261
+ }
3262
+ export { program };
3047
3263
  // ── Helpers ───────────────────────────────────────────────────────────
3048
3264
  function generateCompareHtml(styles, seriesName) {
3049
3265
  const cards = styles.map(s => `