bazaar.it 0.1.0 → 0.2.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 (57) hide show
  1. package/README.md +489 -3
  2. package/bin/baz.js +6 -1
  3. package/dist/commands/auth.d.ts +2 -0
  4. package/dist/commands/auth.js +109 -0
  5. package/dist/commands/capabilities.d.ts +2 -0
  6. package/dist/commands/capabilities.js +44 -0
  7. package/dist/commands/context.d.ts +13 -0
  8. package/dist/commands/context.js +498 -0
  9. package/dist/commands/export.d.ts +2 -0
  10. package/dist/commands/export.js +360 -0
  11. package/dist/commands/logs.d.ts +2 -0
  12. package/dist/commands/logs.js +180 -0
  13. package/dist/commands/loop.d.ts +2 -0
  14. package/dist/commands/loop.js +538 -0
  15. package/dist/commands/mcp.d.ts +2 -0
  16. package/dist/commands/mcp.js +143 -0
  17. package/dist/commands/media.d.ts +2 -0
  18. package/dist/commands/media.js +362 -0
  19. package/dist/commands/project.d.ts +2 -0
  20. package/dist/commands/project.js +786 -0
  21. package/dist/commands/prompt.d.ts +2 -0
  22. package/dist/commands/prompt.js +529 -0
  23. package/dist/commands/recipe.d.ts +15 -0
  24. package/dist/commands/recipe.js +607 -0
  25. package/dist/commands/review.d.ts +17 -0
  26. package/dist/commands/review.js +345 -0
  27. package/dist/commands/scenes.d.ts +2 -0
  28. package/dist/commands/scenes.js +481 -0
  29. package/dist/commands/share.d.ts +2 -0
  30. package/dist/commands/share.js +226 -0
  31. package/dist/commands/state.d.ts +2 -0
  32. package/dist/commands/state.js +171 -0
  33. package/dist/commands/status.d.ts +2 -0
  34. package/dist/commands/status.js +219 -0
  35. package/dist/commands/template.d.ts +2 -0
  36. package/dist/commands/template.js +123 -0
  37. package/dist/commands/verify.d.ts +2 -0
  38. package/dist/commands/verify.js +150 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +124 -0
  41. package/dist/lib/api.d.ts +186 -0
  42. package/dist/lib/api.js +717 -0
  43. package/dist/lib/banner.d.ts +12 -0
  44. package/dist/lib/banner.js +69 -0
  45. package/dist/lib/config.d.ts +33 -0
  46. package/dist/lib/config.js +99 -0
  47. package/dist/lib/output.d.ts +52 -0
  48. package/dist/lib/output.js +162 -0
  49. package/dist/lib/project-state.d.ts +52 -0
  50. package/dist/lib/project-state.js +178 -0
  51. package/dist/lib/sse.d.ts +168 -0
  52. package/dist/lib/sse.js +227 -0
  53. package/dist/lib/version.d.ts +1 -0
  54. package/dist/lib/version.js +3 -0
  55. package/dist/repl.d.ts +4 -0
  56. package/dist/repl.js +764 -0
  57. package/package.json +39 -5
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Display the startup banner
3
+ */
4
+ export declare function showBanner(version: string, configInfo?: {
5
+ hasApiKey: boolean;
6
+ apiUrl: string;
7
+ activeProject?: string;
8
+ }): void;
9
+ /**
10
+ * Show a compact banner for subcommands
11
+ */
12
+ export declare function showCompactBanner(): void;
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ // Big chunky ASCII art for "BAZ"
3
+ const BAZ_ASCII = `
4
+ ██████╗ █████╗ ███████╗
5
+ ██╔══██╗██╔══██╗╚══███╔╝
6
+ ██████╔╝███████║ ███╔╝
7
+ ██╔══██╗██╔══██║ ███╔╝
8
+ ██████╔╝██║ ██║███████╗
9
+ ╚═════╝ ╚═╝ ╚═╝╚══════╝
10
+ `.trim();
11
+ // Gradient colors (purple -> magenta -> cyan)
12
+ const gradientColors = [
13
+ '#a855f7', // purple
14
+ '#c084fc', // light purple
15
+ '#e879f9', // magenta
16
+ '#f0abfc', // pink
17
+ '#67e8f9', // cyan
18
+ ];
19
+ /**
20
+ * Apply gradient colors to each line of ASCII art
21
+ */
22
+ function applyGradient(text) {
23
+ const lines = text.split('\n');
24
+ return lines.map((line, lineIndex) => {
25
+ // Color varies by line for vertical gradient effect
26
+ const colorIndex = Math.min(lineIndex, gradientColors.length - 1);
27
+ const color = gradientColors[colorIndex];
28
+ return chalk.hex(color)(line);
29
+ }).join('\n');
30
+ }
31
+ /**
32
+ * Display the startup banner
33
+ */
34
+ export function showBanner(version, configInfo) {
35
+ console.log();
36
+ console.log(applyGradient(BAZ_ASCII));
37
+ console.log();
38
+ console.log(chalk.gray(' Bazaar CLI'), chalk.white(`v${version}`), chalk.gray('·'), chalk.gray('AI-Powered Video Generation'));
39
+ // Show config status if available
40
+ if (configInfo) {
41
+ const parts = [];
42
+ if (configInfo.hasApiKey) {
43
+ parts.push(chalk.green('✓') + chalk.gray(' authenticated'));
44
+ }
45
+ else {
46
+ parts.push(chalk.yellow('○') + chalk.gray(' not authenticated'));
47
+ }
48
+ if (configInfo.activeProject) {
49
+ // Truncate long UUIDs to first 8 chars
50
+ const shortId = configInfo.activeProject.length > 12
51
+ ? configInfo.activeProject.slice(0, 8)
52
+ : configInfo.activeProject;
53
+ parts.push(chalk.cyan('▸') + chalk.gray(` project: ${shortId}`));
54
+ }
55
+ if (configInfo.apiUrl !== 'https://bazaar.it') {
56
+ parts.push(chalk.gray(`→ ${configInfo.apiUrl}`));
57
+ }
58
+ if (parts.length > 0) {
59
+ console.log(chalk.gray(' ') + parts.join(chalk.gray(' | ')));
60
+ }
61
+ }
62
+ console.log();
63
+ }
64
+ /**
65
+ * Show a compact banner for subcommands
66
+ */
67
+ export function showCompactBanner() {
68
+ console.log(chalk.hex('#a855f7')('▸'), chalk.bold('baz'), chalk.gray('—'), chalk.gray('bazaar.it'));
69
+ }
@@ -0,0 +1,33 @@
1
+ export interface Config {
2
+ apiKey?: string;
3
+ apiUrl: string;
4
+ activeProjectId?: string;
5
+ defaults?: {
6
+ mode?: 'agent' | 'agent-max' | 'multi-scene';
7
+ outputFormat?: 'pretty' | 'json';
8
+ };
9
+ }
10
+ /**
11
+ * Load config from file, environment, and CLI options
12
+ */
13
+ export declare function loadConfig(options?: {
14
+ configPath?: string;
15
+ apiUrl?: string;
16
+ projectId?: string;
17
+ }): Config;
18
+ /**
19
+ * Save config to file
20
+ */
21
+ export declare function saveConfig(updates: Partial<Config>): void;
22
+ /**
23
+ * Get active project ID or throw if not set
24
+ */
25
+ export declare function getProjectId(config: Config, override?: string): string;
26
+ /**
27
+ * Check if config has valid API key
28
+ */
29
+ export declare function hasAuth(config: Config): boolean;
30
+ /**
31
+ * Get config file path for display
32
+ */
33
+ export declare function getConfigPath(): string;
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ const DEFAULT_CONFIG = {
5
+ apiUrl: 'https://bazaar.it',
6
+ defaults: {
7
+ mode: 'agent',
8
+ outputFormat: 'pretty',
9
+ },
10
+ };
11
+ const CONFIG_DIR = join(homedir(), '.bazaar');
12
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
13
+ /**
14
+ * Load config from file, environment, and CLI options
15
+ */
16
+ export function loadConfig(options) {
17
+ // Start with defaults
18
+ let config = { ...DEFAULT_CONFIG };
19
+ // Load from file
20
+ const configPath = options?.configPath || CONFIG_FILE;
21
+ if (existsSync(configPath)) {
22
+ try {
23
+ const fileContent = readFileSync(configPath, 'utf-8');
24
+ const fileConfig = JSON.parse(fileContent);
25
+ config = { ...config, ...fileConfig };
26
+ }
27
+ catch (err) {
28
+ // Ignore parse errors, use defaults
29
+ }
30
+ }
31
+ // Override with environment variables
32
+ if (process.env.BAZ_API_KEY) {
33
+ config.apiKey = process.env.BAZ_API_KEY;
34
+ }
35
+ if (process.env.BAZ_API_URL) {
36
+ config.apiUrl = process.env.BAZ_API_URL;
37
+ }
38
+ if (process.env.BAZ_PROJECT_ID) {
39
+ config.activeProjectId = process.env.BAZ_PROJECT_ID;
40
+ }
41
+ if (process.env.BAZ_JSON === '1') {
42
+ config.defaults = { ...config.defaults, outputFormat: 'json' };
43
+ }
44
+ // Override with CLI options
45
+ if (options?.apiUrl) {
46
+ config.apiUrl = options.apiUrl;
47
+ }
48
+ if (options?.projectId) {
49
+ config.activeProjectId = options.projectId;
50
+ }
51
+ return config;
52
+ }
53
+ /**
54
+ * Save config to file
55
+ */
56
+ export function saveConfig(updates) {
57
+ // Ensure directory exists
58
+ if (!existsSync(CONFIG_DIR)) {
59
+ mkdirSync(CONFIG_DIR, { recursive: true });
60
+ }
61
+ // Load existing config
62
+ let config = { ...DEFAULT_CONFIG };
63
+ if (existsSync(CONFIG_FILE)) {
64
+ try {
65
+ const fileContent = readFileSync(CONFIG_FILE, 'utf-8');
66
+ config = { ...config, ...JSON.parse(fileContent) };
67
+ }
68
+ catch {
69
+ // Ignore
70
+ }
71
+ }
72
+ // Apply updates
73
+ config = { ...config, ...updates };
74
+ // Write back
75
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
76
+ }
77
+ /**
78
+ * Get active project ID or throw if not set
79
+ */
80
+ export function getProjectId(config, override) {
81
+ const projectId = override || config.activeProjectId;
82
+ if (!projectId) {
83
+ throw new Error('No active project. Run: baz project use <id>\n' +
84
+ 'Or pass --project-id <id>');
85
+ }
86
+ return projectId;
87
+ }
88
+ /**
89
+ * Check if config has valid API key
90
+ */
91
+ export function hasAuth(config) {
92
+ return Boolean(config.apiKey);
93
+ }
94
+ /**
95
+ * Get config file path for display
96
+ */
97
+ export function getConfigPath() {
98
+ return CONFIG_FILE;
99
+ }
@@ -0,0 +1,52 @@
1
+ export interface OutputOptions {
2
+ json?: boolean;
3
+ verbose?: boolean;
4
+ compact?: boolean;
5
+ }
6
+ /**
7
+ * Output data in JSON or pretty format
8
+ * When compact=true, outputs minified JSON (saves tokens for agents)
9
+ */
10
+ export declare function output(data: unknown, options: OutputOptions): void;
11
+ /**
12
+ * Output NDJSON line (for streaming)
13
+ * Always compact - no pretty printing for streams
14
+ */
15
+ export declare function outputNDJSON(data: unknown): void;
16
+ /**
17
+ * Pretty print any data structure
18
+ */
19
+ export declare function prettyPrint(data: unknown): void;
20
+ /**
21
+ * Print success message
22
+ */
23
+ export declare function success(message: string): void;
24
+ /**
25
+ * Print error message
26
+ */
27
+ export declare function error(message: string, suggestion?: string): void;
28
+ /**
29
+ * Print warning message
30
+ */
31
+ export declare function warn(message: string): void;
32
+ /**
33
+ * Print info message
34
+ */
35
+ export declare function info(message: string): void;
36
+ /**
37
+ * Print a table
38
+ */
39
+ export declare function table(headers: string[], rows: string[][]): void;
40
+ /**
41
+ * Format duration in mm:ss
42
+ */
43
+ export declare function formatDuration(seconds: number): string;
44
+ /**
45
+ * Format relative time (e.g., "2 hours ago")
46
+ */
47
+ /**
48
+ * Prompt the user for confirmation before a destructive action.
49
+ * In non-interactive (piped/agent) mode, returns false — callers must use --force.
50
+ */
51
+ export declare function confirmAction(message: string): Promise<boolean>;
52
+ export declare function formatRelativeTime(date: Date | string): string;
@@ -0,0 +1,162 @@
1
+ import chalk from 'chalk';
2
+ import * as readline from 'readline';
3
+ /**
4
+ * Output data in JSON or pretty format
5
+ * When compact=true, outputs minified JSON (saves tokens for agents)
6
+ */
7
+ export function output(data, options) {
8
+ if (options.json) {
9
+ // Compact mode: no whitespace (token-efficient for agents)
10
+ // Normal JSON mode: pretty-printed with 2-space indent
11
+ const indent = options.compact ? undefined : 2;
12
+ console.log(JSON.stringify(data, null, indent));
13
+ }
14
+ else {
15
+ prettyPrint(data);
16
+ }
17
+ }
18
+ /**
19
+ * Output NDJSON line (for streaming)
20
+ * Always compact - no pretty printing for streams
21
+ */
22
+ export function outputNDJSON(data) {
23
+ console.log(JSON.stringify(data));
24
+ }
25
+ /**
26
+ * Pretty print any data structure
27
+ */
28
+ export function prettyPrint(data) {
29
+ if (data === null || data === undefined) {
30
+ return;
31
+ }
32
+ if (typeof data === 'string') {
33
+ console.log(data);
34
+ return;
35
+ }
36
+ if (Array.isArray(data)) {
37
+ data.forEach((item, i) => {
38
+ console.log(`${chalk.gray(`${i + 1}.`)} ${formatValue(item)}`);
39
+ });
40
+ return;
41
+ }
42
+ if (typeof data === 'object') {
43
+ for (const [key, value] of Object.entries(data)) {
44
+ console.log(`${chalk.cyan(key)}: ${formatValue(value)}`);
45
+ }
46
+ return;
47
+ }
48
+ console.log(String(data));
49
+ }
50
+ function formatValue(value) {
51
+ if (value === null || value === undefined) {
52
+ return chalk.gray('null');
53
+ }
54
+ if (typeof value === 'boolean') {
55
+ return value ? chalk.green('✓') : chalk.red('✗');
56
+ }
57
+ if (typeof value === 'number') {
58
+ return chalk.yellow(String(value));
59
+ }
60
+ if (typeof value === 'string') {
61
+ return value;
62
+ }
63
+ if (Array.isArray(value)) {
64
+ return `[${value.length} items]`;
65
+ }
66
+ if (typeof value === 'object') {
67
+ return JSON.stringify(value);
68
+ }
69
+ return String(value);
70
+ }
71
+ /**
72
+ * Print success message
73
+ */
74
+ export function success(message) {
75
+ console.log(chalk.green('✓'), message);
76
+ }
77
+ /**
78
+ * Print error message
79
+ */
80
+ export function error(message, suggestion) {
81
+ console.error(chalk.red('Error:'), message);
82
+ if (suggestion) {
83
+ console.error(chalk.gray('Suggestion:'), suggestion);
84
+ }
85
+ }
86
+ /**
87
+ * Print warning message
88
+ */
89
+ export function warn(message) {
90
+ console.warn(chalk.yellow('⚠'), message);
91
+ }
92
+ /**
93
+ * Print info message
94
+ */
95
+ export function info(message) {
96
+ console.log(chalk.blue('ℹ'), message);
97
+ }
98
+ /**
99
+ * Print a table
100
+ */
101
+ export function table(headers, rows) {
102
+ // Calculate column widths
103
+ const widths = headers.map((h, i) => {
104
+ const colValues = [h, ...rows.map(r => r[i] || '')];
105
+ return Math.max(...colValues.map(v => v.length));
106
+ });
107
+ // Print header
108
+ const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(' ');
109
+ console.log(chalk.bold(headerLine));
110
+ console.log(chalk.gray('─'.repeat(headerLine.length)));
111
+ // Print rows
112
+ for (const row of rows) {
113
+ const rowLine = row.map((cell, i) => (cell || '').padEnd(widths[i])).join(' ');
114
+ console.log(rowLine);
115
+ }
116
+ }
117
+ /**
118
+ * Format duration in mm:ss
119
+ */
120
+ export function formatDuration(seconds) {
121
+ const mins = Math.floor(seconds / 60);
122
+ const secs = Math.floor(seconds % 60);
123
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
124
+ }
125
+ /**
126
+ * Format relative time (e.g., "2 hours ago")
127
+ */
128
+ /**
129
+ * Prompt the user for confirmation before a destructive action.
130
+ * In non-interactive (piped/agent) mode, returns false — callers must use --force.
131
+ */
132
+ export async function confirmAction(message) {
133
+ if (!process.stdin.isTTY) {
134
+ return false;
135
+ }
136
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
137
+ return new Promise((resolve) => {
138
+ rl.question(`${chalk.yellow('⚠')} ${message} ${chalk.gray('[y/N]')} `, (answer) => {
139
+ rl.close();
140
+ resolve(answer.trim().toLowerCase() === 'y');
141
+ });
142
+ });
143
+ }
144
+ export function formatRelativeTime(date) {
145
+ const now = new Date();
146
+ const then = new Date(date);
147
+ const diffMs = now.getTime() - then.getTime();
148
+ const diffMins = Math.floor(diffMs / 60000);
149
+ const diffHours = Math.floor(diffMins / 60);
150
+ const diffDays = Math.floor(diffHours / 24);
151
+ if (diffMins < 1)
152
+ return 'just now';
153
+ if (diffMins < 60)
154
+ return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
155
+ if (diffHours < 24)
156
+ return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
157
+ if (diffDays === 1)
158
+ return 'yesterday';
159
+ if (diffDays < 7)
160
+ return `${diffDays} days ago`;
161
+ return then.toLocaleDateString();
162
+ }
@@ -0,0 +1,52 @@
1
+ export interface NormalizedScene {
2
+ id: string;
3
+ name: string;
4
+ order: number;
5
+ track: number;
6
+ startFrame: number;
7
+ durationFrames: number;
8
+ endFrame: number;
9
+ hasCode: boolean;
10
+ hasCompilationError: boolean;
11
+ compilationError?: string;
12
+ }
13
+ export interface LayoutIssue {
14
+ code: string;
15
+ message: string;
16
+ sceneId?: string;
17
+ track?: number;
18
+ startFrame?: number;
19
+ endFrame?: number;
20
+ }
21
+ export interface ProjectValidationResult {
22
+ valid: boolean;
23
+ errors: LayoutIssue[];
24
+ warnings: LayoutIssue[];
25
+ summary: {
26
+ sceneCount: number;
27
+ trackCount: number;
28
+ totalFrames: number;
29
+ totalDurationSeconds: number;
30
+ compilationErrorCount: number;
31
+ };
32
+ }
33
+ type ProjectMeta = {
34
+ format?: string;
35
+ width?: number;
36
+ height?: number;
37
+ } | undefined;
38
+ export declare function normalizeScenes(rawScenes: unknown[], options?: {
39
+ codeOmitted?: boolean;
40
+ }): NormalizedScene[];
41
+ export declare function validateProjectStructure(scenes: NormalizedScene[], options?: {
42
+ fps?: number;
43
+ meta?: ProjectMeta;
44
+ }): ProjectValidationResult;
45
+ export declare function buildSearchCorpus(scenes: NormalizedScene[], options?: {
46
+ projectTitle?: string;
47
+ projectFormat?: string;
48
+ recipePrompts?: string[];
49
+ voiceovers?: string[];
50
+ sceneCodeById?: Record<string, string | undefined>;
51
+ }): string;
52
+ export {};
@@ -0,0 +1,178 @@
1
+ const FORMAT_DIMENSIONS = {
2
+ landscape: { width: 1920, height: 1080 },
3
+ portrait: { width: 1080, height: 1920 },
4
+ square: { width: 1080, height: 1080 },
5
+ };
6
+ export function normalizeScenes(rawScenes, options) {
7
+ return rawScenes.map((scene, index) => {
8
+ const id = String(scene.id ?? `scene-${index + 1}`);
9
+ const name = String(scene.name ?? scene.data?.name ?? `Scene ${index + 1}`);
10
+ const startFrame = Number(scene.start ?? scene.props?.start ?? scene.startFrame ?? 0);
11
+ const durationFrames = Number(scene.duration ?? scene.durationInFrames ?? 0);
12
+ const order = Number(scene.order ?? index);
13
+ const track = Number(scene.track ?? 0);
14
+ // When code is omitted from the response, tsxCode will be undefined --
15
+ // but the schema is NOT NULL, so every scene always has code.
16
+ const hasCode = options?.codeOmitted
17
+ ? true
18
+ : Boolean(scene.tsxCode || scene.code || scene.data?.code);
19
+ const compilationError = scene.compilationError ?? scene.compilation_error;
20
+ return {
21
+ id,
22
+ name,
23
+ order,
24
+ track,
25
+ startFrame,
26
+ durationFrames,
27
+ endFrame: startFrame + durationFrames,
28
+ hasCode,
29
+ hasCompilationError: Boolean(compilationError),
30
+ compilationError: typeof compilationError === "string" ? compilationError : undefined,
31
+ };
32
+ });
33
+ }
34
+ export function validateProjectStructure(scenes, options) {
35
+ const fps = options?.fps ?? 30;
36
+ const errors = [];
37
+ const warnings = [];
38
+ if (scenes.length === 0) {
39
+ warnings.push({
40
+ code: "NO_SCENES",
41
+ message: "Project has no scenes.",
42
+ });
43
+ }
44
+ for (const scene of scenes) {
45
+ if (!Number.isFinite(scene.startFrame) || scene.startFrame < 0) {
46
+ errors.push({
47
+ code: "INVALID_START",
48
+ message: `Scene "${scene.name}" has invalid start frame (${scene.startFrame}).`,
49
+ sceneId: scene.id,
50
+ track: scene.track,
51
+ startFrame: scene.startFrame,
52
+ });
53
+ }
54
+ if (!Number.isFinite(scene.durationFrames) || scene.durationFrames <= 0) {
55
+ errors.push({
56
+ code: "INVALID_DURATION",
57
+ message: `Scene "${scene.name}" has invalid duration (${scene.durationFrames}).`,
58
+ sceneId: scene.id,
59
+ track: scene.track,
60
+ startFrame: scene.startFrame,
61
+ });
62
+ }
63
+ if (!scene.hasCode) {
64
+ warnings.push({
65
+ code: "MISSING_CODE",
66
+ message: `Scene "${scene.name}" has no TSX code.`,
67
+ sceneId: scene.id,
68
+ track: scene.track,
69
+ });
70
+ }
71
+ if (scene.hasCompilationError) {
72
+ errors.push({
73
+ code: "COMPILATION_ERROR",
74
+ message: `Scene "${scene.name}" has a compilation error.`,
75
+ sceneId: scene.id,
76
+ track: scene.track,
77
+ });
78
+ }
79
+ }
80
+ // Overlap detection per track.
81
+ const scenesByTrack = new Map();
82
+ for (const scene of scenes) {
83
+ if (!scenesByTrack.has(scene.track))
84
+ scenesByTrack.set(scene.track, []);
85
+ scenesByTrack.get(scene.track).push(scene);
86
+ }
87
+ for (const [track, trackScenes] of scenesByTrack.entries()) {
88
+ const ordered = [...trackScenes].sort((a, b) => a.startFrame - b.startFrame || a.order - b.order);
89
+ for (let i = 1; i < ordered.length; i += 1) {
90
+ const prev = ordered[i - 1];
91
+ const curr = ordered[i];
92
+ if (curr.startFrame < prev.endFrame) {
93
+ errors.push({
94
+ code: "OVERLAP",
95
+ message: `Scenes "${prev.name}" and "${curr.name}" overlap on track ${track}.`,
96
+ sceneId: curr.id,
97
+ track,
98
+ startFrame: curr.startFrame,
99
+ endFrame: prev.endFrame,
100
+ });
101
+ }
102
+ else if (track === 0 && curr.startFrame > prev.endFrame) {
103
+ warnings.push({
104
+ code: "GAP",
105
+ message: `Gap detected on track 0 between "${prev.name}" and "${curr.name}".`,
106
+ sceneId: curr.id,
107
+ track,
108
+ startFrame: prev.endFrame,
109
+ endFrame: curr.startFrame,
110
+ });
111
+ }
112
+ }
113
+ if (track === 0 && ordered.length > 0 && ordered[0].startFrame > 0) {
114
+ warnings.push({
115
+ code: "LEADING_GAP",
116
+ message: `Track 0 starts at frame ${ordered[0].startFrame} instead of 0.`,
117
+ sceneId: ordered[0].id,
118
+ track,
119
+ startFrame: 0,
120
+ endFrame: ordered[0].startFrame,
121
+ });
122
+ }
123
+ }
124
+ const format = options?.meta?.format;
125
+ const width = options?.meta?.width;
126
+ const height = options?.meta?.height;
127
+ if (format && FORMAT_DIMENSIONS[format]) {
128
+ const expected = FORMAT_DIMENSIONS[format];
129
+ if (width && height && (width !== expected.width || height !== expected.height)) {
130
+ warnings.push({
131
+ code: "FORMAT_DIMENSION_MISMATCH",
132
+ message: `Project format "${format}" expects ${expected.width}x${expected.height}, got ${width}x${height}.`,
133
+ });
134
+ }
135
+ }
136
+ else if (format) {
137
+ warnings.push({
138
+ code: "UNKNOWN_FORMAT",
139
+ message: `Project format "${format}" is not recognized.`,
140
+ });
141
+ }
142
+ const totalFrames = scenes.reduce((max, scene) => Math.max(max, scene.endFrame), 0);
143
+ const trackCount = scenesByTrack.size || 0;
144
+ const compilationErrorCount = scenes.filter((scene) => scene.hasCompilationError).length;
145
+ return {
146
+ valid: errors.length === 0,
147
+ errors,
148
+ warnings,
149
+ summary: {
150
+ sceneCount: scenes.length,
151
+ trackCount,
152
+ totalFrames,
153
+ totalDurationSeconds: totalFrames / fps,
154
+ compilationErrorCount,
155
+ },
156
+ };
157
+ }
158
+ export function buildSearchCorpus(scenes, options) {
159
+ const parts = [];
160
+ if (options?.projectTitle)
161
+ parts.push(options.projectTitle);
162
+ if (options?.projectFormat)
163
+ parts.push(`format:${options.projectFormat}`);
164
+ for (const scene of scenes) {
165
+ parts.push(scene.name);
166
+ parts.push(`track:${scene.track}`);
167
+ if (options?.sceneCodeById?.[scene.id]) {
168
+ parts.push(options.sceneCodeById[scene.id] || "");
169
+ }
170
+ }
171
+ for (const prompt of options?.recipePrompts ?? []) {
172
+ parts.push(prompt);
173
+ }
174
+ for (const voiceover of options?.voiceovers ?? []) {
175
+ parts.push(voiceover);
176
+ }
177
+ return parts.join("\n").toLowerCase();
178
+ }