feedeas 0.1.0-alpha.18 → 0.1.0-alpha.19

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 (2) hide show
  1. package/dist/cli/index.js +253 -207
  2. package/package.json +2 -1
package/dist/cli/index.js CHANGED
@@ -93,75 +93,141 @@ import { Command } from "commander";
93
93
  import { chromium } from "playwright-core";
94
94
  import { spawn as spawn4, spawnSync } from "child_process";
95
95
  import fs8 from "fs";
96
- import path8 from "path";
96
+ import path9 from "path";
97
97
  import { fileURLToPath as fileURLToPath3 } from "url";
98
98
 
99
99
  // src/cli/services/ffprobe.ts
100
- import { spawn } from "child_process";
101
100
  import fs from "fs";
101
+ import path from "path";
102
+ function isNodeRuntime() {
103
+ return typeof process !== "undefined" && !!process.versions?.node;
104
+ }
105
+ function isImageExt(ext) {
106
+ return [".jpg", ".jpeg", ".png", ".webp", ".gif"].includes(ext);
107
+ }
108
+ function isAudioExt(ext) {
109
+ return [".mp3", ".wav", ".m4a", ".ogg"].includes(ext);
110
+ }
111
+ async function getAudioMetadataFallback(filePath) {
112
+ const musicMetadata = await import("music-metadata");
113
+ const metadata = await musicMetadata.parseFile(filePath);
114
+ return {
115
+ duration: metadata.format.duration,
116
+ sampleRate: metadata.format.sampleRate,
117
+ channels: metadata.format.numberOfChannels,
118
+ codec: metadata.format.codec,
119
+ bitrate: metadata.format.bitrate ?? void 0
120
+ };
121
+ }
122
+ async function getImageDimensionsFallback(filePath) {
123
+ const imageSize = await import("image-size");
124
+ const buffer = fs.readFileSync(filePath);
125
+ const dimensions = imageSize.default(buffer);
126
+ return { width: dimensions.width ?? void 0, height: dimensions.height ?? void 0 };
127
+ }
102
128
  var FFprobeService = class {
103
129
  static async getMetadata(filePath) {
104
130
  if (!fs.existsSync(filePath)) {
105
131
  throw new Error(`File not found: ${filePath}`);
106
132
  }
107
- return new Promise((resolve, reject) => {
108
- const ffprobe = spawn("ffprobe", [
109
- "-v",
110
- "quiet",
111
- "-print_format",
112
- "json",
113
- "-show_format",
114
- "-show_streams",
115
- filePath
116
- ]);
117
- let stdout = "";
118
- let stderr = "";
119
- ffprobe.stdout.on("data", (data) => {
120
- stdout += data;
121
- });
122
- ffprobe.stderr.on("data", (data) => {
123
- stderr += data;
124
- });
125
- ffprobe.on("close", (code) => {
126
- if (code !== 0) {
127
- reject(new Error(`ffprobe exited with code ${code}: ${stderr}`));
128
- return;
129
- }
130
- try {
131
- const data = JSON.parse(stdout);
132
- const format = data.format || {};
133
- const streams = data.streams || [];
134
- const videoStream = streams.find((s) => s.codec_type === "video");
135
- const audioStream = streams.find((s) => s.codec_type === "audio");
136
- const metadata = {
137
- path: filePath,
138
- format: format.format_name || "unknown",
139
- duration: parseFloat(format.duration || "0"),
140
- size: parseInt(format.size || "0")
141
- };
142
- if (videoStream) {
143
- metadata.width = videoStream.width;
144
- metadata.height = videoStream.height;
145
- }
146
- if (audioStream) {
147
- metadata.audioChannels = audioStream.channels;
148
- metadata.sampleRate = parseInt(audioStream.sample_rate || "0");
149
- }
150
- resolve(metadata);
151
- } catch (err) {
152
- reject(new Error(`Failed to parse ffprobe output: ${err}`));
133
+ const ext = path.extname(filePath).toLowerCase();
134
+ const stats = fs.statSync(filePath);
135
+ if (isNodeRuntime()) {
136
+ try {
137
+ const { spawn: spawn5 } = await import("node:child_process");
138
+ return await new Promise((resolve, reject) => {
139
+ const ffprobe = spawn5("ffprobe", [
140
+ "-v",
141
+ "quiet",
142
+ "-print_format",
143
+ "json",
144
+ "-show_format",
145
+ "-show_streams",
146
+ filePath
147
+ ]);
148
+ let stdout = "";
149
+ let stderr = "";
150
+ ffprobe.stdout.on("data", (data) => {
151
+ stdout += data;
152
+ });
153
+ ffprobe.stderr.on("data", (data) => {
154
+ stderr += data;
155
+ });
156
+ ffprobe.on("close", (code) => {
157
+ if (code !== 0) {
158
+ reject(new Error(`ffprobe exited with code ${code}: ${stderr}`));
159
+ return;
160
+ }
161
+ try {
162
+ const data = JSON.parse(stdout);
163
+ const format = data.format || {};
164
+ const streams = data.streams || [];
165
+ const videoStream = streams.find((s) => s.codec_type === "video");
166
+ const audioStream = streams.find((s) => s.codec_type === "audio");
167
+ const metadata = {
168
+ path: filePath,
169
+ format: format.format_name || "unknown",
170
+ duration: parseFloat(format.duration || "0"),
171
+ size: parseInt(format.size || "0")
172
+ };
173
+ if (videoStream) {
174
+ metadata.width = videoStream.width;
175
+ metadata.height = videoStream.height;
176
+ }
177
+ if (audioStream) {
178
+ metadata.audioChannels = audioStream.channels;
179
+ metadata.sampleRate = parseInt(audioStream.sample_rate || "0");
180
+ metadata.audioCodec = audioStream.codec_name || void 0;
181
+ }
182
+ if (format?.bit_rate) {
183
+ metadata.bitrate = parseInt(format.bit_rate || "0");
184
+ }
185
+ resolve(metadata);
186
+ } catch (err) {
187
+ reject(new Error(`Failed to parse ffprobe output: ${err}`));
188
+ }
189
+ });
190
+ ffprobe.on("error", (err) => {
191
+ reject(new Error(`Failed to spawn ffprobe: ${err.message}`));
192
+ });
193
+ });
194
+ } catch (err) {
195
+ if (err?.code !== "ENOENT") {
196
+ throw err;
153
197
  }
154
- });
155
- ffprobe.on("error", (err) => {
156
- reject(new Error(`Failed to spawn ffprobe: ${err.message}`));
157
- });
158
- });
198
+ }
199
+ }
200
+ if (isImageExt(ext)) {
201
+ const dims = await getImageDimensionsFallback(filePath);
202
+ return {
203
+ path: filePath,
204
+ format: ext.replace(".", "") || "image",
205
+ duration: 0,
206
+ size: stats.size,
207
+ width: dims.width,
208
+ height: dims.height
209
+ };
210
+ }
211
+ if (isAudioExt(ext)) {
212
+ const audio = await getAudioMetadataFallback(filePath);
213
+ return {
214
+ path: filePath,
215
+ format: ext.replace(".", "") || "audio",
216
+ duration: audio.duration || 0,
217
+ size: stats.size,
218
+ audioChannels: audio.channels,
219
+ sampleRate: audio.sampleRate,
220
+ audioCodec: audio.codec,
221
+ bitrate: audio.bitrate
222
+ };
223
+ }
224
+ throw new Error("ffprobe is required to inspect this file type.");
159
225
  }
160
226
  };
161
227
 
162
228
  // src/cli/services/assets/resolver.ts
163
229
  import fs2 from "node:fs";
164
- import path from "node:path";
230
+ import path2 from "node:path";
165
231
  function looksLikeScheme(uri) {
166
232
  return /^[a-z][a-z0-9+.-]*:/.test(uri);
167
233
  }
@@ -177,7 +243,7 @@ var RelativeFsResolver = class {
177
243
  const localPath2 = decodeURIComponent(new URL(uri).pathname);
178
244
  return { type: "local", uri, localPath: localPath2 };
179
245
  }
180
- const localPath = path.isAbsolute(uri) ? uri : path.resolve(rootPath, uri);
246
+ const localPath = path2.isAbsolute(uri) ? uri : path2.resolve(rootPath, uri);
181
247
  return { type: "local", uri, localPath };
182
248
  }
183
249
  };
@@ -337,24 +403,24 @@ import { existsSync } from "node:fs";
337
403
 
338
404
  // src/cli/server/cli-runner.ts
339
405
  import { spawn as spawn2 } from "child_process";
340
- import path2 from "path";
406
+ import path3 from "path";
341
407
  import fs3 from "fs";
342
408
  import { fileURLToPath } from "url";
343
409
  var __filename = fileURLToPath(import.meta.url);
344
- var __dirname = path2.dirname(__filename);
410
+ var __dirname = path3.dirname(__filename);
345
411
  async function runCliCommand(args, cwd) {
346
412
  const isCompiled = __filename.endsWith(".js");
347
413
  let command;
348
414
  let spawnArgs;
349
415
  if (isCompiled) {
350
- const binPath = path2.resolve(__dirname, "../../../bin/feedeas.js");
416
+ const binPath = path3.resolve(__dirname, "../../../bin/feedeas.js");
351
417
  if (!fs3.existsSync(binPath)) {
352
418
  throw new Error(`Could not find compiled CLI binary at ${binPath}`);
353
419
  }
354
420
  command = "node";
355
421
  spawnArgs = [binPath, ...args];
356
422
  } else {
357
- const srcPath = path2.resolve(__dirname, "../index.ts");
423
+ const srcPath = path3.resolve(__dirname, "../index.ts");
358
424
  command = "bun";
359
425
  spawnArgs = ["run", srcPath, ...args];
360
426
  }
@@ -420,7 +486,7 @@ ${stderrData || stdoutData}`));
420
486
  }
421
487
 
422
488
  // src/cli/services/taste.ts
423
- import path3 from "node:path";
489
+ import path4 from "node:path";
424
490
  var SAMPLE_SCENE_HEADER = "## Sample Scene (Starter)";
425
491
  var DEFAULT_SAMPLE_SCENE_JSON = {
426
492
  meta: {
@@ -550,12 +616,12 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
550
616
  ]);
551
617
  function safeResolveFromCwd(relativePath) {
552
618
  const cwd = process.cwd();
553
- const normalized = path3.normalize(relativePath);
619
+ const normalized = path4.normalize(relativePath);
554
620
  if (normalized.includes("..")) {
555
621
  throw new Error("Invalid path");
556
622
  }
557
- const fullPath = path3.resolve(cwd, normalized);
558
- if (fullPath !== cwd && !fullPath.startsWith(`${cwd}${path3.sep}`)) {
623
+ const fullPath = path4.resolve(cwd, normalized);
624
+ if (fullPath !== cwd && !fullPath.startsWith(`${cwd}${path4.sep}`)) {
559
625
  throw new Error("Invalid path");
560
626
  }
561
627
  return fullPath;
@@ -1366,7 +1432,7 @@ async function simulateIdeas(params) {
1366
1432
 
1367
1433
  // src/cli/services/taste-store.ts
1368
1434
  import fs4 from "node:fs";
1369
- import path4 from "node:path";
1435
+ import path5 from "node:path";
1370
1436
  import { createHash } from "node:crypto";
1371
1437
  var DEFAULT_SUGGESTIONS_FILE_CONTENT = "# Taste Suggestions v1\n";
1372
1438
  function hashContent(content) {
@@ -1489,9 +1555,9 @@ var MarkdownTasteStore = class {
1489
1555
  }
1490
1556
  async ensureWorkspace() {
1491
1557
  const { tastePath, memoryPath, suggestionsPath } = this.resolveAll();
1492
- fs4.mkdirSync(path4.dirname(tastePath), { recursive: true });
1493
- fs4.mkdirSync(path4.dirname(memoryPath), { recursive: true });
1494
- fs4.mkdirSync(path4.dirname(suggestionsPath), { recursive: true });
1558
+ fs4.mkdirSync(path5.dirname(tastePath), { recursive: true });
1559
+ fs4.mkdirSync(path5.dirname(memoryPath), { recursive: true });
1560
+ fs4.mkdirSync(path5.dirname(suggestionsPath), { recursive: true });
1495
1561
  if (!fs4.existsSync(tastePath)) {
1496
1562
  fs4.writeFileSync(tastePath, DEFAULT_TASTE_FILE_CONTENT, "utf-8");
1497
1563
  }
@@ -1648,7 +1714,7 @@ function createTasteStore(params) {
1648
1714
 
1649
1715
  // src/cli/services/scenes/file-store.ts
1650
1716
  import fs5 from "node:fs";
1651
- import path5 from "node:path";
1717
+ import path6 from "node:path";
1652
1718
  import { randomUUID } from "node:crypto";
1653
1719
 
1654
1720
  // src/cli/services/scenes/types.ts
@@ -1670,10 +1736,10 @@ function nowIso() {
1670
1736
  return (/* @__PURE__ */ new Date()).toISOString();
1671
1737
  }
1672
1738
  function getStorePath(cwd) {
1673
- return path5.resolve(cwd, ".feedeas/store.json");
1739
+ return path6.resolve(cwd, ".feedeas/store.json");
1674
1740
  }
1675
1741
  function ensureStore(pathname) {
1676
- const dir = path5.dirname(pathname);
1742
+ const dir = path6.dirname(pathname);
1677
1743
  if (!fs5.existsSync(dir)) {
1678
1744
  fs5.mkdirSync(dir, { recursive: true });
1679
1745
  }
@@ -1900,10 +1966,10 @@ api.get("/v1/files/read", async (c) => {
1900
1966
  });
1901
1967
  api.post("/v1/files/write", async (c) => {
1902
1968
  const body = await c.req.json();
1903
- const { path: path22, content } = body;
1904
- if (!path22 || content === void 0) return c.json({ error: "Path and content required" }, 400);
1969
+ const { path: path23, content } = body;
1970
+ if (!path23 || content === void 0) return c.json({ error: "Path and content required" }, 400);
1905
1971
  try {
1906
- const fullPath = resolveSafePath(path22);
1972
+ const fullPath = resolveSafePath(path23);
1907
1973
  const dir = dirname(fullPath);
1908
1974
  if (!existsSync(dir)) {
1909
1975
  await mkdir(dir, { recursive: true });
@@ -2406,14 +2472,14 @@ api.post("/cli/run", async (c) => {
2406
2472
  var api_default = api;
2407
2473
 
2408
2474
  // src/cli/server/index.ts
2409
- import path6 from "path";
2475
+ import path7 from "path";
2410
2476
  import { fileURLToPath as fileURLToPath2 } from "url";
2411
2477
  import fs6 from "fs";
2412
2478
  var app = new Hono2();
2413
2479
  app.use("/*", cors());
2414
2480
  app.route("/api", api_default);
2415
2481
  var __filename2 = fileURLToPath2(import.meta.url);
2416
- var __dirname2 = path6.dirname(__filename2);
2482
+ var __dirname2 = path7.dirname(__filename2);
2417
2483
  var createServer = (staticRoot) => {
2418
2484
  const app2 = new Hono2();
2419
2485
  app2.use("/*", cors());
@@ -2444,7 +2510,7 @@ var createServer = (staticRoot) => {
2444
2510
  };
2445
2511
  app2.get("/*", async (c) => {
2446
2512
  const requestPath = c.req.path === "/" ? "/index.html" : c.req.path;
2447
- const filePath = path6.join(staticRoot, requestPath);
2513
+ const filePath = path7.join(staticRoot, requestPath);
2448
2514
  try {
2449
2515
  if (fs6.existsSync(filePath)) {
2450
2516
  const file = await fs6.promises.readFile(filePath);
@@ -2459,7 +2525,7 @@ var createServer = (staticRoot) => {
2459
2525
  }
2460
2526
  if (!requestPath.includes(".")) {
2461
2527
  try {
2462
- const indexPath = path6.join(staticRoot, "index.html");
2528
+ const indexPath = path7.join(staticRoot, "index.html");
2463
2529
  if (fs6.existsSync(indexPath)) {
2464
2530
  const indexFile = await fs6.promises.readFile(indexPath);
2465
2531
  return new Response(indexFile, {
@@ -2582,12 +2648,12 @@ var SceneApiClient = class {
2582
2648
 
2583
2649
  // src/cli/services/scenes/flow-state.ts
2584
2650
  import fs7 from "node:fs";
2585
- import path7 from "node:path";
2651
+ import path8 from "node:path";
2586
2652
  function sessionPath(cwd) {
2587
- return path7.resolve(cwd, ".feedeas/session.json");
2653
+ return path8.resolve(cwd, ".feedeas/session.json");
2588
2654
  }
2589
2655
  function ensureDir(filePath) {
2590
- const dir = path7.dirname(filePath);
2656
+ const dir = path8.dirname(filePath);
2591
2657
  if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
2592
2658
  }
2593
2659
  function readFlowState(cwd = process.cwd()) {
@@ -2631,7 +2697,7 @@ function resolveApiUrl(explicit) {
2631
2697
 
2632
2698
  // src/cli/commands/record.ts
2633
2699
  var __filename3 = fileURLToPath3(import.meta.url);
2634
- var __dirname3 = path8.dirname(__filename3);
2700
+ var __dirname3 = path9.dirname(__filename3);
2635
2701
  function formatBytes(bytes) {
2636
2702
  if (bytes === 0) return "0 B";
2637
2703
  const k = 1024;
@@ -2640,7 +2706,7 @@ function formatBytes(bytes) {
2640
2706
  return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
2641
2707
  }
2642
2708
  function getMissingUiAssets(staticRoot) {
2643
- const indexPath = path8.join(staticRoot, "index.html");
2709
+ const indexPath = path9.join(staticRoot, "index.html");
2644
2710
  if (!fs8.existsSync(indexPath)) {
2645
2711
  return {
2646
2712
  missingFiles: [indexPath],
@@ -2653,7 +2719,7 @@ function getMissingUiAssets(staticRoot) {
2653
2719
  const missingFiles = [];
2654
2720
  const expectedUrls = [];
2655
2721
  for (const assetPath of assetPaths) {
2656
- const localPath = path8.join(staticRoot, assetPath.replace(/^\//, ""));
2722
+ const localPath = path9.join(staticRoot, assetPath.replace(/^\//, ""));
2657
2723
  if (!fs8.existsSync(localPath)) {
2658
2724
  missingFiles.push(localPath);
2659
2725
  expectedUrls.push(assetPath);
@@ -2861,9 +2927,9 @@ Ready to record. Run without --dry-run to start rendering.`);
2861
2927
  ensureFfmpegAvailableOrExit();
2862
2928
  await ensurePlaywrightReadyOrExit();
2863
2929
  console.log("Starting temporary server...");
2864
- let staticRoot = path8.resolve(__dirname3, "../../dist/ui");
2930
+ let staticRoot = path9.resolve(__dirname3, "../../dist/ui");
2865
2931
  if (!fs8.existsSync(staticRoot)) {
2866
- staticRoot = path8.resolve(__dirname3, "../ui");
2932
+ staticRoot = path9.resolve(__dirname3, "../ui");
2867
2933
  }
2868
2934
  if (!fs8.existsSync(staticRoot)) {
2869
2935
  console.error(`Error: UI assets not found at ${staticRoot}. Please run 'bun run build' first.`);
@@ -3081,10 +3147,10 @@ Ready to record. Run without --dry-run to start rendering.`);
3081
3147
  // src/cli/commands/inspect.ts
3082
3148
  import { Command as Command2 } from "commander";
3083
3149
  import fs9 from "fs";
3084
- import path9 from "path";
3150
+ import path10 from "path";
3085
3151
  var inspectCommand = new Command2("inspect");
3086
3152
  inspectCommand.description("Inspect an asset file to get metadata (duration, format, etc.)").argument("<file>", "Path to the asset file").option("--json", "Output in JSON format").action(async (file, options) => {
3087
- const filePath = path9.resolve(process.cwd(), file);
3153
+ const filePath = path10.resolve(process.cwd(), file);
3088
3154
  if (!fs9.existsSync(filePath)) {
3089
3155
  console.error(`File not found: ${filePath}`);
3090
3156
  process.exit(1);
@@ -3248,14 +3314,14 @@ validateCommand.description("Validate a scene by project and scene ID").option("
3248
3314
  // src/cli/commands/set-scene.ts
3249
3315
  import { Command as Command4 } from "commander";
3250
3316
  import fs11 from "fs";
3251
- import path10 from "path";
3317
+ import path11 from "path";
3252
3318
  var setSceneCommand = new Command4("set-scene");
3253
3319
  setSceneCommand.description("Update an existing scene by project/scene ID").option("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").option("-c, --content <json>", "JSON content string").option("-i, --input <file>", "Input JSON file to copy from").option("--version <number>", "Expected scene version").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
3254
3320
  let contentStr = "";
3255
3321
  if (options.content) {
3256
3322
  contentStr = options.content;
3257
3323
  } else if (options.input) {
3258
- const inputPath = path10.resolve(process.cwd(), options.input);
3324
+ const inputPath = path11.resolve(process.cwd(), options.input);
3259
3325
  if (!fs11.existsSync(inputPath)) {
3260
3326
  console.error(`Input file not found: ${inputPath}`);
3261
3327
  process.exit(1);
@@ -3302,29 +3368,29 @@ setSceneCommand.description("Update an existing scene by project/scene ID").opti
3302
3368
  // src/cli/commands/imagine.ts
3303
3369
  import { Command as Command5 } from "commander";
3304
3370
  import fs13 from "fs";
3305
- import path12 from "path";
3371
+ import path13 from "path";
3306
3372
 
3307
3373
  // src/cli/utils/path.ts
3308
- import path11 from "path";
3374
+ import path12 from "path";
3309
3375
  import fs12 from "fs";
3310
3376
  function resolveAssetOutputPath(outputOption, cwd = process.cwd()) {
3311
- if (path11.isAbsolute(outputOption)) {
3377
+ if (path12.isAbsolute(outputOption)) {
3312
3378
  return outputOption;
3313
3379
  }
3314
- const assetsDir = path11.resolve(cwd, "assets");
3380
+ const assetsDir = path12.resolve(cwd, "assets");
3315
3381
  if (!fs12.existsSync(assetsDir)) {
3316
3382
  fs12.mkdirSync(assetsDir, { recursive: true });
3317
3383
  }
3318
- const normalizedInput = path11.normalize(outputOption);
3384
+ const normalizedInput = path12.normalize(outputOption);
3319
3385
  let cleanRelativePath = normalizedInput;
3320
- const segments = normalizedInput.split(path11.sep);
3386
+ const segments = normalizedInput.split(path12.sep);
3321
3387
  if (segments[0] === "assets") {
3322
- cleanRelativePath = segments.slice(1).join(path11.sep);
3388
+ cleanRelativePath = segments.slice(1).join(path12.sep);
3323
3389
  }
3324
3390
  if (!cleanRelativePath) {
3325
3391
  cleanRelativePath = `unnamed-asset-${Date.now()}`;
3326
3392
  }
3327
- return path11.join(assetsDir, cleanRelativePath);
3393
+ return path12.join(assetsDir, cleanRelativePath);
3328
3394
  }
3329
3395
 
3330
3396
  // src/cli/commands/imagine.ts
@@ -3421,15 +3487,15 @@ Note:
3421
3487
  imageSize
3422
3488
  });
3423
3489
  const outputPath = resolveAssetOutputPath(options.output);
3424
- const outputDir = path12.dirname(outputPath);
3425
- const outputExt = path12.extname(outputPath) || ".png";
3426
- const outputBase = path12.basename(outputPath, outputExt);
3490
+ const outputDir = path13.dirname(outputPath);
3491
+ const outputExt = path13.extname(outputPath) || ".png";
3492
+ const outputBase = path13.basename(outputPath, outputExt);
3427
3493
  if (!fs13.existsSync(outputDir)) {
3428
3494
  fs13.mkdirSync(outputDir, { recursive: true });
3429
3495
  }
3430
3496
  const savedFiles = [];
3431
3497
  for (let i = 0; i < images.length; i++) {
3432
- const filename = images.length > 1 ? path12.join(outputDir, `${outputBase}_${i + 1}${outputExt}`) : outputPath;
3498
+ const filename = images.length > 1 ? path13.join(outputDir, `${outputBase}_${i + 1}${outputExt}`) : outputPath;
3433
3499
  fs13.writeFileSync(filename, images[i]);
3434
3500
  if (!options.json) console.log(`\u2705 Image saved: ${filename}`);
3435
3501
  savedFiles.push(filename);
@@ -3541,7 +3607,7 @@ function resolveAspectRatio(input, projectFile) {
3541
3607
  return normalizeAspectRatio(input);
3542
3608
  }
3543
3609
  function inferAspectRatioFromProject(projectFile) {
3544
- const filePath = path12.resolve(process.cwd(), projectFile);
3610
+ const filePath = path13.resolve(process.cwd(), projectFile);
3545
3611
  if (!fs13.existsSync(filePath)) return null;
3546
3612
  try {
3547
3613
  const scene = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
@@ -3585,43 +3651,42 @@ function gcd(a, b) {
3585
3651
  // src/cli/commands/audio.ts
3586
3652
  import { Command as Command6 } from "commander";
3587
3653
  import fs16 from "fs";
3588
- import path15 from "path";
3654
+ import path16 from "path";
3589
3655
 
3590
3656
  // src/cli/services/whisper.ts
3591
3657
  import fs14 from "fs";
3592
- import path13 from "path";
3658
+ import path14 from "path";
3593
3659
  import os from "os";
3594
- import { spawn as spawn5 } from "child_process";
3595
3660
  import https from "https";
3596
3661
  var WhisperService = class {
3597
3662
  static getHomeDir() {
3598
3663
  return os.homedir();
3599
3664
  }
3600
3665
  static getBaseDir() {
3601
- return path13.join(this.getHomeDir(), ".feedeas");
3666
+ return path14.join(this.getHomeDir(), ".feedeas");
3602
3667
  }
3603
3668
  static getBinDir() {
3604
- return path13.join(this.getBaseDir(), "bin");
3669
+ return path14.join(this.getBaseDir(), "bin");
3605
3670
  }
3606
3671
  static getModelsDir() {
3607
- return path13.join(this.getBaseDir(), "models");
3672
+ return path14.join(this.getBaseDir(), "models");
3608
3673
  }
3609
3674
  static getExecutablePath() {
3610
- const repoDir = path13.join(this.getBaseDir(), "whisper.cpp-repo");
3675
+ const repoDir = path14.join(this.getBaseDir(), "whisper.cpp-repo");
3611
3676
  const possiblePaths = [
3612
- path13.join(repoDir, "build", "bin", "whisper-cli"),
3613
- path13.join(repoDir, "build", "bin", "main"),
3614
- path13.join(repoDir, "main"),
3615
- path13.join(this.getBinDir(), "whisper-main")
3677
+ path14.join(repoDir, "build", "bin", "whisper-cli"),
3678
+ path14.join(repoDir, "build", "bin", "main"),
3679
+ path14.join(repoDir, "main"),
3680
+ path14.join(this.getBinDir(), "whisper-main")
3616
3681
  // fallback to copied/downloaded
3617
3682
  ];
3618
3683
  for (const p of possiblePaths) {
3619
3684
  if (fs14.existsSync(p)) return p;
3620
3685
  }
3621
- return path13.join(this.getBinDir(), "whisper-main");
3686
+ return path14.join(this.getBinDir(), "whisper-main");
3622
3687
  }
3623
3688
  static getModelPath(modelName = "base.en") {
3624
- return path13.join(this.getModelsDir(), `ggml-${modelName}.bin`);
3689
+ return path14.join(this.getModelsDir(), `ggml-${modelName}.bin`);
3625
3690
  }
3626
3691
  /**
3627
3692
  * Download a file from a URL to a destination path
@@ -3663,7 +3728,7 @@ var WhisperService = class {
3663
3728
  if (!fs14.existsSync(execPath)) {
3664
3729
  console.log("\u2B07\uFE0F Whisper binary not found. Attempting to build...");
3665
3730
  try {
3666
- const repoDir = path13.join(this.getBaseDir(), "whisper.cpp-repo");
3731
+ const repoDir = path14.join(this.getBaseDir(), "whisper.cpp-repo");
3667
3732
  if (!fs14.existsSync(repoDir)) {
3668
3733
  console.log("\u{1F4E6} Cloning whisper.cpp...");
3669
3734
  await runCommand("git", ["clone", "https://github.com/ggerganov/whisper.cpp.git", repoDir]);
@@ -3696,12 +3761,12 @@ Please install manually: 'brew install whisper-cpp'`);
3696
3761
  static async transcribe(audioPath) {
3697
3762
  const execPath = this.getExecutablePath();
3698
3763
  const modelPath = this.getModelPath();
3699
- const ext = path13.extname(audioPath).toLowerCase();
3764
+ const ext = path14.extname(audioPath).toLowerCase();
3700
3765
  let inputToWhisper = audioPath;
3701
3766
  let isTempFile = false;
3702
3767
  if (ext !== ".wav") {
3703
3768
  console.log("\u{1F504} Converting audio to 16kHz WAV for Whisper...");
3704
- const tempWav = path13.join(path13.dirname(audioPath), `temp_${Date.now()}.wav`);
3769
+ const tempWav = path14.join(path14.dirname(audioPath), `temp_${Date.now()}.wav`);
3705
3770
  await runCommand("ffmpeg", [
3706
3771
  "-i",
3707
3772
  audioPath,
@@ -3718,7 +3783,7 @@ Please install manually: 'brew install whisper-cpp'`);
3718
3783
  isTempFile = true;
3719
3784
  }
3720
3785
  try {
3721
- const outputBase = path13.join(path13.dirname(audioPath), path13.basename(audioPath, path13.extname(audioPath)));
3786
+ const outputBase = path14.join(path14.dirname(audioPath), path14.basename(audioPath, path14.extname(audioPath)));
3722
3787
  const baseArgs = [
3723
3788
  "-m",
3724
3789
  modelPath,
@@ -3761,19 +3826,30 @@ Please install manually: 'brew install whisper-cpp'`);
3761
3826
  }
3762
3827
  }
3763
3828
  };
3829
+ function isNodeRuntime2() {
3830
+ return typeof process !== "undefined" && !!process.versions?.node;
3831
+ }
3764
3832
  function runCommand(command, args, cwd) {
3765
- return new Promise((resolve, reject) => {
3766
- const proc = spawn5(command, args, { cwd, stdio: "inherit" });
3767
- proc.on("close", (code) => {
3768
- if (code === 0) resolve();
3769
- else reject(new Error(`${command} exited with code ${code}`));
3770
- });
3771
- proc.on("error", reject);
3833
+ if (!isNodeRuntime2()) {
3834
+ return Promise.reject(new Error(`"${command}" requires a Node runtime with child_process support.`));
3835
+ }
3836
+ return new Promise(async (resolve, reject) => {
3837
+ try {
3838
+ const { spawn: spawn5 } = await import("node:child_process");
3839
+ const proc = spawn5(command, args, { cwd, stdio: "inherit" });
3840
+ proc.on("close", (code) => {
3841
+ if (code === 0) resolve();
3842
+ else reject(new Error(`${command} exited with code ${code}`));
3843
+ });
3844
+ proc.on("error", reject);
3845
+ } catch (err) {
3846
+ reject(err);
3847
+ }
3772
3848
  });
3773
3849
  }
3774
3850
  function runWhisper(execPath, args) {
3775
3851
  return new Promise((resolve, reject) => {
3776
- const proc = spawn5(execPath, args, { stdio: "inherit" });
3852
+ const proc = spawn(execPath, args, { stdio: "inherit" });
3777
3853
  proc.on("close", (code) => {
3778
3854
  if (code !== 0) {
3779
3855
  reject(new Error(`Whisper process exited with code ${code}`));
@@ -3837,11 +3913,10 @@ function roundTo3(value) {
3837
3913
 
3838
3914
  // src/cli/services/audio-convert.ts
3839
3915
  import fs15 from "fs";
3840
- import path14 from "path";
3841
- import { spawn as spawn6 } from "node:child_process";
3916
+ import path15 from "path";
3842
3917
  import { pathToFileURL } from "node:url";
3843
3918
  import { FFmpeg } from "@ffmpeg/ffmpeg";
3844
- function isNodeRuntime() {
3919
+ function isNodeRuntime3() {
3845
3920
  return typeof process !== "undefined" && !!process.versions?.node;
3846
3921
  }
3847
3922
  function buildCodecArgs(options) {
@@ -3856,8 +3931,9 @@ function buildCodecArgs(options) {
3856
3931
  async function convertWithNodeFfmpeg(options) {
3857
3932
  const { pcmPath, outputPath, sampleRate, channels, durationSec } = options;
3858
3933
  const codecArgs = buildCodecArgs(options);
3934
+ const { spawn: spawn5 } = await import("node:child_process");
3859
3935
  await new Promise((resolve, reject) => {
3860
- const ffmpeg = spawn6("ffmpeg", [
3936
+ const ffmpeg = spawn5("ffmpeg", [
3861
3937
  "-f",
3862
3938
  "s16le",
3863
3939
  "-ar",
@@ -3882,10 +3958,10 @@ async function convertWithWasm(options) {
3882
3958
  const { pcmPath, outputPath, sampleRate, channels, durationSec } = options;
3883
3959
  const ffmpeg = new FFmpeg();
3884
3960
  const baseDir = typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".";
3885
- const coreDir = path14.resolve(baseDir, "node_modules/@ffmpeg/core/dist");
3886
- const coreURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.js")).toString();
3887
- const wasmURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.wasm")).toString();
3888
- const workerURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.worker.js")).toString();
3961
+ const coreDir = path15.resolve(baseDir, "node_modules/@ffmpeg/core/dist");
3962
+ const coreURL = pathToFileURL(path15.join(coreDir, "ffmpeg-core.js")).toString();
3963
+ const wasmURL = pathToFileURL(path15.join(coreDir, "ffmpeg-core.wasm")).toString();
3964
+ const workerURL = pathToFileURL(path15.join(coreDir, "ffmpeg-core.worker.js")).toString();
3889
3965
  await ffmpeg.load({ coreURL, wasmURL, workerURL });
3890
3966
  const pcmBytes = await fs15.promises.readFile(pcmPath);
3891
3967
  await ffmpeg.writeFile("input.pcm", pcmBytes);
@@ -3909,7 +3985,7 @@ async function convertWithWasm(options) {
3909
3985
  }
3910
3986
  async function convertPcmToAudio(options) {
3911
3987
  const { pcmPath } = options;
3912
- let useWasm = !isNodeRuntime();
3988
+ let useWasm = !isNodeRuntime3();
3913
3989
  try {
3914
3990
  try {
3915
3991
  if (!useWasm) {
@@ -3977,9 +4053,9 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
3977
4053
  console.log("\u{1F5E3}\uFE0F Generating speech with Gemini...");
3978
4054
  const audioBuffer = await generateGeminiAudio(text, apiKey, options.voice);
3979
4055
  const outputPath = resolveAssetOutputPath(options.output);
3980
- const tempPcmPath = path15.join(path15.dirname(outputPath), `temp_${Date.now()}.pcm`);
4056
+ const tempPcmPath = path16.join(path16.dirname(outputPath), `temp_${Date.now()}.pcm`);
3981
4057
  fs16.writeFileSync(tempPcmPath, audioBuffer);
3982
- console.log(`\u{1F504} Converting raw PCM to ${path15.extname(outputPath)}...`);
4058
+ console.log(`\u{1F504} Converting raw PCM to ${path16.extname(outputPath)}...`);
3983
4059
  await convertPcmToMp3({
3984
4060
  pcmPath: tempPcmPath,
3985
4061
  outputPath
@@ -3990,7 +4066,7 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
3990
4066
  console.log("\u{1F50D} Aligning audio with Whisper...");
3991
4067
  await WhisperService.ensureReady();
3992
4068
  const metadata = await WhisperService.transcribe(outputPath);
3993
- const metaPath = outputPath.replace(path15.extname(outputPath), ".json");
4069
+ const metaPath = outputPath.replace(path16.extname(outputPath), ".json");
3994
4070
  fs16.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
3995
4071
  console.log(`\u2705 Metadata saved: ${metaPath}`);
3996
4072
  } catch (wErr) {
@@ -4042,7 +4118,7 @@ async function generateGeminiAudio(text, apiKey, voiceName) {
4042
4118
  // src/cli/commands/bgm.ts
4043
4119
  import { Command as Command7 } from "commander";
4044
4120
  import fs17 from "fs";
4045
- import path16 from "path";
4121
+ import path17 from "path";
4046
4122
  var bgmCommand = new Command7("generate:bgm");
4047
4123
  bgmCommand.alias("bgm").alias("music").description("Generate background music with Gemini Lyria (Live Music API)").argument("<prompt-or-file>", "Music prompt text or path to a text file").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "bgm.mp3").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("-d, --duration <seconds>", "Target duration in seconds (5-300)", "30").option("--seed <number>", "Optional seed for reproducible output").option("--json", "Emit machine-readable JSON output").action(async (promptOrFile, options) => {
4048
4124
  try {
@@ -4115,7 +4191,7 @@ function parseSeed(input) {
4115
4191
  return value;
4116
4192
  }
4117
4193
  function inferOutputFormat(outputPath) {
4118
- const ext = path16.extname(outputPath).toLowerCase();
4194
+ const ext = path17.extname(outputPath).toLowerCase();
4119
4195
  if (ext === ".wav") return "wav";
4120
4196
  if (ext === ".mp3" || !ext) return "mp3";
4121
4197
  throw new Error("Invalid output format. Use .mp3 or .wav.");
@@ -4203,9 +4279,9 @@ async function generateGeminiMusicPcm(request) {
4203
4279
  });
4204
4280
  }
4205
4281
  async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
4206
- const outputDir = path16.dirname(outputPath);
4282
+ const outputDir = path17.dirname(outputPath);
4207
4283
  if (!fs17.existsSync(outputDir)) fs17.mkdirSync(outputDir, { recursive: true });
4208
- const tempPcmPath = path16.join(outputDir, `temp_bgm_${Date.now()}.pcm`);
4284
+ const tempPcmPath = path17.join(outputDir, `temp_bgm_${Date.now()}.pcm`);
4209
4285
  fs17.writeFileSync(tempPcmPath, pcmBuffer);
4210
4286
  await convertPcmToAudio({
4211
4287
  pcmPath: tempPcmPath,
@@ -4221,18 +4297,17 @@ async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
4221
4297
  // src/cli/commands/asset.ts
4222
4298
  import { Command as Command8 } from "commander";
4223
4299
  import fs18 from "fs";
4224
- import path17 from "path";
4225
- import { spawn as spawn7 } from "child_process";
4300
+ import path18 from "path";
4226
4301
  var assetCommand = new Command8("asset").description("Asset information and management");
4227
4302
  assetCommand.command("info <file>").description("Show detailed information about an asset").action(async (file) => {
4228
- const assetPath = path17.resolve(process.cwd(), "assets", file);
4303
+ const assetPath = path18.resolve(process.cwd(), "assets", file);
4229
4304
  if (!fs18.existsSync(assetPath)) {
4230
4305
  console.error(`Asset not found: ${file}`);
4231
4306
  console.error(`Looked in: ${assetPath}`);
4232
4307
  process.exit(1);
4233
4308
  }
4234
4309
  const stats = fs18.statSync(assetPath);
4235
- const ext = path17.extname(file).toLowerCase();
4310
+ const ext = path18.extname(file).toLowerCase();
4236
4311
  console.log(`
4237
4312
  Asset: ${file}`);
4238
4313
  console.log(`Size: ${formatBytes2(stats.size)}`);
@@ -4255,7 +4330,7 @@ Asset: ${file}`);
4255
4330
  console.log(`Channels: ${audioInfo.channels}`);
4256
4331
  console.log(`Bitrate: ${audioInfo.bitrate}`);
4257
4332
  const metadataFile = file.replace(ext, ".json");
4258
- const metadataPath = path17.resolve(process.cwd(), "assets", metadataFile);
4333
+ const metadataPath = path18.resolve(process.cwd(), "assets", metadataFile);
4259
4334
  if (fs18.existsSync(metadataPath)) {
4260
4335
  console.log(`
4261
4336
  Metadata: ${metadataFile}`);
@@ -4304,47 +4379,18 @@ async function getImageDimensions(filePath) {
4304
4379
  }
4305
4380
  }
4306
4381
  async function getAudioInfo(filePath) {
4307
- return new Promise((resolve, reject) => {
4308
- const ffprobe = spawn7("ffprobe", [
4309
- "-v",
4310
- "error",
4311
- "-show_entries",
4312
- "format=duration,bit_rate:stream=codec_name,sample_rate,channels",
4313
- "-of",
4314
- "json",
4315
- filePath
4316
- ]);
4317
- let output = "";
4318
- ffprobe.stdout.on("data", (data) => {
4319
- output += data.toString();
4320
- });
4321
- ffprobe.on("close", (code) => {
4322
- if (code !== 0) {
4323
- reject(new Error("ffprobe failed"));
4324
- return;
4325
- }
4326
- try {
4327
- const data = JSON.parse(output);
4328
- const stream = data.streams?.[0] || {};
4329
- const format = data.format || {};
4330
- resolve({
4331
- duration: format.duration ? parseFloat(format.duration).toFixed(2) : "unknown",
4332
- codec: stream.codec_name || "unknown",
4333
- sampleRate: stream.sample_rate ? `${stream.sample_rate} Hz` : "unknown",
4334
- channels: stream.channels ? stream.channels === 1 ? "mono" : stream.channels === 2 ? "stereo" : `${stream.channels}ch` : "unknown",
4335
- bitrate: format.bit_rate ? `${Math.round(parseInt(format.bit_rate) / 1e3)} kbps` : "unknown"
4336
- });
4337
- } catch (err) {
4338
- reject(err);
4339
- }
4340
- });
4341
- ffprobe.on("error", reject);
4342
- });
4382
+ const metadata = await FFprobeService.getMetadata(filePath);
4383
+ const duration = metadata.duration ? metadata.duration.toFixed(2) : "unknown";
4384
+ const codec = metadata.audioCodec || metadata.format || "unknown";
4385
+ const sampleRate = metadata.sampleRate ? `${metadata.sampleRate} Hz` : "unknown";
4386
+ const channels = metadata.audioChannels ? metadata.audioChannels === 1 ? "mono" : metadata.audioChannels === 2 ? "stereo" : `${metadata.audioChannels}ch` : "unknown";
4387
+ const bitrate = metadata.bitrate ? `${Math.round(metadata.bitrate / 1e3)} kbps` : "unknown";
4388
+ return { duration, codec, sampleRate, channels, bitrate };
4343
4389
  }
4344
4390
 
4345
4391
  // src/cli/index.ts
4346
4392
  import open2 from "open";
4347
- import path21 from "path";
4393
+ import path22 from "path";
4348
4394
  import { fileURLToPath as fileURLToPath5 } from "url";
4349
4395
  import fs21 from "fs";
4350
4396
 
@@ -4763,7 +4809,7 @@ addEntityCommand.description("Add an entity to an existing scene").option("--pro
4763
4809
 
4764
4810
  // src/cli/services/telemetry.ts
4765
4811
  import os2 from "os";
4766
- import path18 from "path";
4812
+ import path19 from "path";
4767
4813
  import { createHash as createHash2 } from "crypto";
4768
4814
  var POSTHOG_CAPTURE_URL = "https://us.i.posthog.com/capture/";
4769
4815
  var TELEMETRY_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
@@ -4775,7 +4821,7 @@ var PostHogTelemetryService = class {
4775
4821
  if (!this.enabled) return;
4776
4822
  this.capture("cli_feedback", {
4777
4823
  details: String(details).slice(0, 2e3),
4778
- cwd_basename: path18.basename(process.cwd())
4824
+ cwd_basename: path19.basename(process.cwd())
4779
4825
  });
4780
4826
  }
4781
4827
  apiKey;
@@ -4798,7 +4844,7 @@ var PostHogTelemetryService = class {
4798
4844
  command_name: this.getCommandPath(actionCommand),
4799
4845
  command_aliases: actionCommand.aliases(),
4800
4846
  options_used: this.getUsedOptionNames(actionCommand),
4801
- cwd_basename: path18.basename(process.cwd()),
4847
+ cwd_basename: path19.basename(process.cwd()),
4802
4848
  node_version: process.version,
4803
4849
  platform: process.platform,
4804
4850
  arch: process.arch
@@ -4811,7 +4857,7 @@ var PostHogTelemetryService = class {
4811
4857
  status,
4812
4858
  duration_ms: Math.max(0, Math.round(durationMs)),
4813
4859
  error_message: errorMessage ? String(errorMessage).slice(0, 300) : void 0,
4814
- cwd_basename: path18.basename(process.cwd())
4860
+ cwd_basename: path19.basename(process.cwd())
4815
4861
  });
4816
4862
  }
4817
4863
  getCommandPath(command) {
@@ -4865,25 +4911,25 @@ var PostHogTelemetryService = class {
4865
4911
  // src/cli/commands/taste.ts
4866
4912
  import { Command as Command12 } from "commander";
4867
4913
  import fs19 from "node:fs";
4868
- import path19 from "node:path";
4914
+ import path20 from "node:path";
4869
4915
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4870
4916
  import open from "open";
4871
4917
  var __filename4 = fileURLToPath4(import.meta.url);
4872
- var __dirname4 = path19.dirname(__filename4);
4918
+ var __dirname4 = path20.dirname(__filename4);
4873
4919
  function resolveStaticRoot() {
4874
- let staticRoot = path19.resolve(__dirname4, "../../../dist/ui");
4920
+ let staticRoot = path20.resolve(__dirname4, "../../../dist/ui");
4875
4921
  if (!fs19.existsSync(staticRoot)) {
4876
- staticRoot = path19.resolve(__dirname4, "../../ui");
4922
+ staticRoot = path20.resolve(__dirname4, "../../ui");
4877
4923
  }
4878
4924
  return staticRoot;
4879
4925
  }
4880
4926
  function prepareWorkingDirectory(pathArg) {
4881
4927
  if (!pathArg) return;
4882
- const targetPath = path19.resolve(process.cwd(), pathArg);
4928
+ const targetPath = path20.resolve(process.cwd(), pathArg);
4883
4929
  if (fs19.existsSync(targetPath)) {
4884
4930
  const stats = fs19.statSync(targetPath);
4885
4931
  if (stats.isFile()) {
4886
- process.chdir(path19.dirname(targetPath));
4932
+ process.chdir(path20.dirname(targetPath));
4887
4933
  return;
4888
4934
  }
4889
4935
  if (stats.isDirectory()) {
@@ -4891,8 +4937,8 @@ function prepareWorkingDirectory(pathArg) {
4891
4937
  return;
4892
4938
  }
4893
4939
  }
4894
- if (path19.extname(pathArg)) {
4895
- const dir = path19.dirname(targetPath);
4940
+ if (path20.extname(pathArg)) {
4941
+ const dir = path20.dirname(targetPath);
4896
4942
  fs19.mkdirSync(dir, { recursive: true });
4897
4943
  process.chdir(dir);
4898
4944
  return;
@@ -5185,7 +5231,7 @@ projectCommand.command("list").description("List projects").option("--api-url <u
5185
5231
 
5186
5232
  // src/cli/commands/scene.ts
5187
5233
  import fs20 from "node:fs";
5188
- import path20 from "node:path";
5234
+ import path21 from "node:path";
5189
5235
  import { Command as Command15 } from "commander";
5190
5236
  var sceneCommand = new Command15("scene").description("Manage scenes");
5191
5237
  sceneCommand.command("list").option("--project-id <id>", "Project ID").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
@@ -5217,7 +5263,7 @@ sceneCommand.command("create").option("--project-id <id>", "Project ID").option(
5217
5263
  });
5218
5264
  sceneCommand.command("import").option("--project-id <id>", "Project ID").requiredOption("--file <path>", "Scene JSON file path").option("--name <name>", "Scene name").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
5219
5265
  try {
5220
- const filePath = path20.resolve(process.cwd(), String(options.file));
5266
+ const filePath = path21.resolve(process.cwd(), String(options.file));
5221
5267
  if (!fs20.existsSync(filePath)) {
5222
5268
  throw new Error(`File not found: ${filePath}`);
5223
5269
  }
@@ -5228,10 +5274,10 @@ sceneCommand.command("import").option("--project-id <id>", "Project ID").require
5228
5274
  const projectId = resolveProjectId(options.projectId);
5229
5275
  const apiUrl = resolveApiUrl(options.apiUrl);
5230
5276
  const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
5231
- name: options.name ? String(options.name) : path20.basename(filePath, path20.extname(filePath)),
5277
+ name: options.name ? String(options.name) : path21.basename(filePath, path21.extname(filePath)),
5232
5278
  scene: { meta: parsed.meta, entities: parsed.entities }
5233
5279
  }) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
5234
- name: options.name ? String(options.name) : path20.basename(filePath, path20.extname(filePath)),
5280
+ name: options.name ? String(options.name) : path21.basename(filePath, path21.extname(filePath)),
5235
5281
  scene: { meta: parsed.meta, entities: parsed.entities }
5236
5282
  });
5237
5283
  writeFlowState({ projectId, sceneId: scene.id, apiUrl });
@@ -5250,8 +5296,8 @@ sceneCommand.command("export").option("--project-id <id>", "Project ID").option(
5250
5296
  if (!scene) {
5251
5297
  throw new Error(`Scene not found: ${sceneId}`);
5252
5298
  }
5253
- const out = path20.resolve(process.cwd(), String(options.file));
5254
- fs20.mkdirSync(path20.dirname(out), { recursive: true });
5299
+ const out = path21.resolve(process.cwd(), String(options.file));
5300
+ fs20.mkdirSync(path21.dirname(out), { recursive: true });
5255
5301
  fs20.writeFileSync(out, JSON.stringify({ meta: scene.meta, entities: scene.entities }, null, 2), "utf-8");
5256
5302
  writeFlowState({ projectId, sceneId, apiUrl });
5257
5303
  console.log(`Scene exported to ${out}`);
@@ -5271,11 +5317,11 @@ sceneCommand.command("use").description("Set current flow state for project/scen
5271
5317
 
5272
5318
  // src/cli/index.ts
5273
5319
  var __filename5 = fileURLToPath5(import.meta.url);
5274
- var __dirname5 = path21.dirname(__filename5);
5320
+ var __dirname5 = path22.dirname(__filename5);
5275
5321
  var program = new Command16();
5276
5322
  var telemetry = new PostHogTelemetryService();
5277
5323
  var commandStartTimes = /* @__PURE__ */ new WeakMap();
5278
- program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.18");
5324
+ program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.19");
5279
5325
  program.hook("preAction", (_thisCommand, actionCommand) => {
5280
5326
  commandStartTimes.set(actionCommand, Date.now());
5281
5327
  telemetry.trackCommandStarted(actionCommand);
@@ -5302,9 +5348,9 @@ program.command("edit").alias("start").alias("init").description("Start the Feed
5302
5348
  })();
5303
5349
  const cwd = process.cwd();
5304
5350
  console.log(`Starting Feedeas in ${cwd}...`);
5305
- let staticRoot = path21.resolve(__dirname5, "../../dist/ui");
5351
+ let staticRoot = path22.resolve(__dirname5, "../../dist/ui");
5306
5352
  if (!fs21.existsSync(staticRoot)) {
5307
- staticRoot = path21.resolve(__dirname5, "../ui");
5353
+ staticRoot = path22.resolve(__dirname5, "../ui");
5308
5354
  }
5309
5355
  if (!fs21.existsSync(staticRoot)) {
5310
5356
  console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "feedeas",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.1.0-alpha.18",
5
+ "version": "0.1.0-alpha.19",
6
6
  "devDependencies": {
7
7
  "@tailwindcss/vite": "^4.1.18",
8
8
  "@types/bun": "latest",
@@ -28,6 +28,7 @@
28
28
  "hono": "^4.11.4",
29
29
  "image-size": "^2.0.2",
30
30
  "lucide-react": "^0.562.0",
31
+ "music-metadata": "^8.3.0",
31
32
  "open": "^11.0.0",
32
33
  "playwright-core": "^1.58.2",
33
34
  "react": "^19.2.3",