vidpipe 1.3.17 → 1.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -253,10 +253,10 @@ function closeFileDescriptor(fd) {
253
253
  closeSync(fd);
254
254
  }
255
255
  async function makeTempDir(prefix) {
256
- return new Promise((resolve3, reject) => {
256
+ return new Promise((resolve4, reject) => {
257
257
  tmp.dir({ prefix, mode: 448 }, (err, path) => {
258
258
  if (err) reject(err);
259
- else resolve3(path);
259
+ else resolve4(path);
260
260
  });
261
261
  });
262
262
  }
@@ -752,11 +752,12 @@ var init_types = __esm({
752
752
  { stage: "medium-clips" /* MediumClips */, name: "Medium Clips", stageNumber: 9 },
753
753
  { stage: "chapters" /* Chapters */, name: "Chapters", stageNumber: 10 },
754
754
  { stage: "summary" /* Summary */, name: "Summary", stageNumber: 11 },
755
- { stage: "social-media" /* SocialMedia */, name: "Social Media", stageNumber: 12 },
756
- { stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 13 },
757
- { stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 14 },
758
- { stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 15 },
759
- { stage: "blog" /* Blog */, name: "Blog", stageNumber: 16 }
755
+ { stage: "idea-discovery" /* IdeaDiscovery */, name: "Idea Discovery", stageNumber: 12 },
756
+ { stage: "social-media" /* SocialMedia */, name: "Social Media", stageNumber: 13 },
757
+ { stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 14 },
758
+ { stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 15 },
759
+ { stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 16 },
760
+ { stage: "blog" /* Blog */, name: "Blog", stageNumber: 17 }
760
761
  ];
761
762
  TOTAL_STAGES = PIPELINE_STAGES.length;
762
763
  PLATFORM_CHAR_LIMITS = {
@@ -782,12 +783,12 @@ var init_ffmpeg = __esm({
782
783
  import { execFile as nodeExecFile, execSync as nodeExecSync, spawnSync as nodeSpawnSync } from "child_process";
783
784
  import { createRequire } from "module";
784
785
  function execCommand(cmd, args, opts) {
785
- return new Promise((resolve3, reject) => {
786
+ return new Promise((resolve4, reject) => {
786
787
  nodeExecFile(cmd, args, { ...opts, encoding: "utf-8" }, (error, stdout, stderr) => {
787
788
  if (error) {
788
789
  reject(Object.assign(error, { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
789
790
  } else {
790
- resolve3({ stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
791
+ resolve4({ stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
791
792
  }
792
793
  });
793
794
  });
@@ -851,11 +852,11 @@ function createFFmpeg(input) {
851
852
  return cmd;
852
853
  }
853
854
  function ffprobe(filePath) {
854
- return new Promise((resolve3, reject) => {
855
+ return new Promise((resolve4, reject) => {
855
856
  default3.setFfprobePath(getFFprobePath());
856
857
  default3.ffprobe(filePath, (err, data) => {
857
858
  if (err) reject(err);
858
- else resolve3(data);
859
+ else resolve4(data);
859
860
  });
860
861
  });
861
862
  }
@@ -878,7 +879,7 @@ async function extractAudio(videoPath, outputPath, options = {}) {
878
879
  const outputDir = dirname(outputPath);
879
880
  await ensureDirectory(outputDir);
880
881
  logger_default.info(`Extracting audio (${format}): ${videoPath} \u2192 ${outputPath}`);
881
- return new Promise((resolve3, reject) => {
882
+ return new Promise((resolve4, reject) => {
882
883
  const command = createFFmpeg(videoPath).noVideo().audioChannels(1);
883
884
  if (format === "mp3") {
884
885
  command.audioCodec("libmp3lame").audioBitrate("64k").audioFrequency(16e3);
@@ -887,7 +888,7 @@ async function extractAudio(videoPath, outputPath, options = {}) {
887
888
  }
888
889
  command.output(outputPath).on("end", () => {
889
890
  logger_default.info(`Audio extraction complete: ${outputPath}`);
890
- resolve3(outputPath);
891
+ resolve4(outputPath);
891
892
  }).on("error", (err) => {
892
893
  logger_default.error(`Audio extraction failed: ${err.message}`);
893
894
  reject(new Error(`Audio extraction failed: ${err.message}`));
@@ -913,8 +914,8 @@ async function splitAudioIntoChunks(audioPath, maxChunkSizeMB = 24) {
913
914
  const startTime = i * chunkDuration;
914
915
  const chunkPath = `${base}_chunk${i}${ext}`;
915
916
  chunkPaths.push(chunkPath);
916
- await new Promise((resolve3, reject) => {
917
- const cmd = createFFmpeg(audioPath).setStartTime(startTime).setDuration(chunkDuration).audioCodec("copy").output(chunkPath).on("end", () => resolve3()).on("error", (err) => reject(new Error(`Chunk split failed: ${err.message}`)));
917
+ await new Promise((resolve4, reject) => {
918
+ const cmd = createFFmpeg(audioPath).setStartTime(startTime).setDuration(chunkDuration).audioCodec("copy").output(chunkPath).on("end", () => resolve4()).on("error", (err) => reject(new Error(`Chunk split failed: ${err.message}`)));
918
919
  cmd.run();
919
920
  });
920
921
  logger_default.info(`Created chunk ${i + 1}/${numChunks}: ${chunkPath}`);
@@ -941,19 +942,19 @@ var init_audioExtraction = __esm({
941
942
 
942
943
  // src/L2-clients/ffmpeg/clipExtraction.ts
943
944
  async function getVideoFps(videoPath) {
944
- return new Promise((resolve3) => {
945
+ return new Promise((resolve4) => {
945
946
  execFileRaw(
946
947
  ffprobePath,
947
948
  ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", "-of", "csv=p=0", videoPath],
948
949
  { timeout: 5e3 },
949
950
  (error, stdout) => {
950
951
  if (error || !stdout.trim()) {
951
- resolve3(DEFAULT_FPS);
952
+ resolve4(DEFAULT_FPS);
952
953
  return;
953
954
  }
954
955
  const parts = stdout.trim().split("/");
955
956
  const fps = parts.length === 2 ? parseInt(parts[0]) / parseInt(parts[1]) : parseFloat(stdout.trim());
956
- resolve3(isFinite(fps) && fps > 0 ? Math.round(fps) : DEFAULT_FPS);
957
+ resolve4(isFinite(fps) && fps > 0 ? Math.round(fps) : DEFAULT_FPS);
957
958
  }
958
959
  );
959
960
  });
@@ -965,10 +966,10 @@ async function extractClip(videoPath, start, end, outputPath, buffer = 1) {
965
966
  const bufferedEnd = end + buffer;
966
967
  const duration = bufferedEnd - bufferedStart;
967
968
  logger_default.info(`Extracting clip [${start}s\u2013${end}s] (buffered: ${bufferedStart.toFixed(2)}s\u2013${bufferedEnd.toFixed(2)}s) \u2192 ${outputPath}`);
968
- return new Promise((resolve3, reject) => {
969
+ return new Promise((resolve4, reject) => {
969
970
  createFFmpeg(videoPath).setStartTime(bufferedStart).setDuration(duration).outputOptions(["-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", "-threads", "4", "-c:a", "aac", "-b:a", "128k"]).output(outputPath).on("end", () => {
970
971
  logger_default.info(`Clip extraction complete: ${outputPath}`);
971
- resolve3(outputPath);
972
+ resolve4(outputPath);
972
973
  }).on("error", (err) => {
973
974
  logger_default.error(`Clip extraction failed: ${err.message}`);
974
975
  reject(new Error(`Clip extraction failed: ${err.message}`));
@@ -996,8 +997,8 @@ async function extractCompositeClip(videoPath, segments, outputPath, buffer = 1)
996
997
  const bufferedStart = Math.max(0, seg.start - buffer);
997
998
  const bufferedEnd = seg.end + buffer;
998
999
  logger_default.info(`Extracting segment ${i + 1}/${segments.length} [${seg.start}s\u2013${seg.end}s] (buffered: ${bufferedStart.toFixed(2)}s\u2013${bufferedEnd.toFixed(2)}s)`);
999
- await new Promise((resolve3, reject) => {
1000
- createFFmpeg(videoPath).setStartTime(bufferedStart).setDuration(bufferedEnd - bufferedStart).outputOptions(["-threads", "4", "-preset", "ultrafast"]).output(tempPath).on("end", () => resolve3()).on("error", (err) => reject(new Error(`Segment ${i} extraction failed: ${err.message}`))).run();
1000
+ await new Promise((resolve4, reject) => {
1001
+ createFFmpeg(videoPath).setStartTime(bufferedStart).setDuration(bufferedEnd - bufferedStart).outputOptions(["-threads", "4", "-preset", "ultrafast"]).output(tempPath).on("end", () => resolve4()).on("error", (err) => reject(new Error(`Segment ${i} extraction failed: ${err.message}`))).run();
1001
1002
  });
1002
1003
  }
1003
1004
  concatListFile = default2.fileSync({ dir: tempDir, postfix: ".txt", prefix: "concat-" });
@@ -1006,8 +1007,8 @@ async function extractCompositeClip(videoPath, segments, outputPath, buffer = 1)
1006
1007
  await writeTextFile(concatListPath, listContent);
1007
1008
  closeFileDescriptor(concatListFile.fd);
1008
1009
  logger_default.info(`Concatenating ${segments.length} segments \u2192 ${outputPath}`);
1009
- await new Promise((resolve3, reject) => {
1010
- createFFmpeg().input(concatListPath).inputOptions(["-f", "concat", "-safe", "0"]).outputOptions(["-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", "-threads", "4", "-c:a", "aac"]).output(outputPath).on("end", () => resolve3()).on("error", (err) => reject(new Error(`Concat failed: ${err.message}`))).run();
1010
+ await new Promise((resolve4, reject) => {
1011
+ createFFmpeg().input(concatListPath).inputOptions(["-f", "concat", "-safe", "0"]).outputOptions(["-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", "-threads", "4", "-c:a", "aac"]).output(outputPath).on("end", () => resolve4()).on("error", (err) => reject(new Error(`Concat failed: ${err.message}`))).run();
1011
1012
  });
1012
1013
  logger_default.info(`Composite clip complete: ${outputPath}`);
1013
1014
  return outputPath;
@@ -1097,7 +1098,7 @@ async function extractCompositeClipWithTransitions(videoPath, segments, outputPa
1097
1098
  outputPath
1098
1099
  ];
1099
1100
  logger_default.info(`[ClipExtraction] Compositing ${segments.length} segments with xfade transitions \u2192 ${outputPath}`);
1100
- return new Promise((resolve3, reject) => {
1101
+ return new Promise((resolve4, reject) => {
1101
1102
  execFileRaw(ffmpegPath, args, { maxBuffer: 50 * 1024 * 1024 }, (error, _stdout, stderr) => {
1102
1103
  if (error) {
1103
1104
  logger_default.error(`[ClipExtraction] xfade composite failed: ${stderr}`);
@@ -1105,7 +1106,7 @@ async function extractCompositeClipWithTransitions(videoPath, segments, outputPa
1105
1106
  return;
1106
1107
  }
1107
1108
  logger_default.info(`[ClipExtraction] xfade composite complete: ${outputPath}`);
1108
- resolve3(outputPath);
1109
+ resolve4(outputPath);
1109
1110
  });
1110
1111
  });
1111
1112
  }
@@ -1180,7 +1181,7 @@ async function singlePassEdit(inputPath, keepSegments, outputPath) {
1180
1181
  outputPath
1181
1182
  ];
1182
1183
  logger_default.info(`[SinglePassEdit] Editing ${keepSegments.length} segments \u2192 ${outputPath}`);
1183
- return new Promise((resolve3, reject) => {
1184
+ return new Promise((resolve4, reject) => {
1184
1185
  execFileRaw(ffmpegPath2, args, { maxBuffer: 50 * 1024 * 1024 }, (error, _stdout, stderr) => {
1185
1186
  if (error) {
1186
1187
  logger_default.error(`[SinglePassEdit] FFmpeg failed: ${stderr}`);
@@ -1188,7 +1189,7 @@ async function singlePassEdit(inputPath, keepSegments, outputPath) {
1188
1189
  return;
1189
1190
  }
1190
1191
  logger_default.info(`[SinglePassEdit] Complete: ${outputPath}`);
1191
- resolve3(outputPath);
1192
+ resolve4(outputPath);
1192
1193
  });
1193
1194
  });
1194
1195
  }
@@ -1247,7 +1248,7 @@ async function burnCaptions(videoPath, assPath, outputPath) {
1247
1248
  "4",
1248
1249
  tempOutput
1249
1250
  ];
1250
- return new Promise((resolve3, reject) => {
1251
+ return new Promise((resolve4, reject) => {
1251
1252
  execFileRaw(ffmpegPath3, args, { cwd: workDir, maxBuffer: 10 * 1024 * 1024 }, async (error, _stdout, stderr) => {
1252
1253
  const cleanup = async () => {
1253
1254
  const files = await listDirectory(workDir).catch(() => []);
@@ -1271,7 +1272,7 @@ async function burnCaptions(videoPath, assPath, outputPath) {
1271
1272
  }
1272
1273
  await cleanup();
1273
1274
  logger_default.info(`Captions burned: ${outputPath}`);
1274
- resolve3(outputPath);
1275
+ resolve4(outputPath);
1275
1276
  });
1276
1277
  });
1277
1278
  }
@@ -1292,7 +1293,7 @@ var init_captionBurning = __esm({
1292
1293
  // src/L2-clients/ffmpeg/silenceDetection.ts
1293
1294
  async function detectSilence(audioPath, minDuration = 1, noiseThreshold = "-30dB") {
1294
1295
  logger_default.info(`Detecting silence in: ${audioPath} (min=${minDuration}s, threshold=${noiseThreshold})`);
1295
- return new Promise((resolve3, reject) => {
1296
+ return new Promise((resolve4, reject) => {
1296
1297
  const regions = [];
1297
1298
  let stderr = "";
1298
1299
  createFFmpeg(audioPath).audioFilters(`silencedetect=noise=${noiseThreshold}:d=${minDuration}`).format("null").output("-").on("stderr", (line) => {
@@ -1322,7 +1323,7 @@ async function detectSilence(audioPath, minDuration = 1, noiseThreshold = "-30dB
1322
1323
  logger_default.info(`Sample silence regions: ${validRegions.slice(0, 3).map((r) => `${r.start.toFixed(1)}s-${r.end.toFixed(1)}s (${r.duration.toFixed(2)}s)`).join(", ")}`);
1323
1324
  }
1324
1325
  logger_default.info(`Detected ${validRegions.length} silence regions`);
1325
- resolve3(validRegions);
1326
+ resolve4(validRegions);
1326
1327
  }).on("error", (err) => {
1327
1328
  logger_default.error(`Silence detection failed: ${err.message}`);
1328
1329
  reject(new Error(`Silence detection failed: ${err.message}`));
@@ -1342,10 +1343,10 @@ async function captureFrame(videoPath, timestamp, outputPath) {
1342
1343
  const outputDir = dirname(outputPath);
1343
1344
  await ensureDirectory(outputDir);
1344
1345
  logger_default.info(`Capturing frame at ${timestamp}s \u2192 ${outputPath}`);
1345
- return new Promise((resolve3, reject) => {
1346
+ return new Promise((resolve4, reject) => {
1346
1347
  createFFmpeg(videoPath).seekInput(timestamp).frames(1).output(outputPath).on("end", () => {
1347
1348
  logger_default.info(`Frame captured: ${outputPath}`);
1348
- resolve3(outputPath);
1349
+ resolve4(outputPath);
1349
1350
  }).on("error", (err) => {
1350
1351
  logger_default.error(`Frame capture failed: ${err.message}`);
1351
1352
  reject(new Error(`Frame capture failed: ${err.message}`));
@@ -1373,7 +1374,7 @@ var init_media = __esm({
1373
1374
 
1374
1375
  // src/L2-clients/ffmpeg/faceDetection.ts
1375
1376
  async function getVideoDuration(videoPath) {
1376
- return new Promise((resolve3, reject) => {
1377
+ return new Promise((resolve4, reject) => {
1377
1378
  execFileRaw(
1378
1379
  ffprobePath2,
1379
1380
  ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", videoPath],
@@ -1383,13 +1384,13 @@ async function getVideoDuration(videoPath) {
1383
1384
  reject(new Error(`ffprobe failed: ${error.message}`));
1384
1385
  return;
1385
1386
  }
1386
- resolve3(parseFloat(stdout.trim()));
1387
+ resolve4(parseFloat(stdout.trim()));
1387
1388
  }
1388
1389
  );
1389
1390
  });
1390
1391
  }
1391
1392
  async function getVideoResolution(videoPath) {
1392
- return new Promise((resolve3, reject) => {
1393
+ return new Promise((resolve4, reject) => {
1393
1394
  execFileRaw(
1394
1395
  ffprobePath2,
1395
1396
  [
@@ -1410,7 +1411,7 @@ async function getVideoResolution(videoPath) {
1410
1411
  return;
1411
1412
  }
1412
1413
  const [w, h] = stdout.trim().split(",").map(Number);
1413
- resolve3({ width: w, height: h });
1414
+ resolve4({ width: w, height: h });
1414
1415
  }
1415
1416
  );
1416
1417
  });
@@ -1428,7 +1429,7 @@ async function extractSampleFrames(videoPath, tempDir) {
1428
1429
  for (let i = 0; i < timestamps.length; i++) {
1429
1430
  const framePath = join(tempDir, `frame_${i}.png`);
1430
1431
  framePaths.push(framePath);
1431
- await new Promise((resolve3, reject) => {
1432
+ await new Promise((resolve4, reject) => {
1432
1433
  execFileRaw(
1433
1434
  ffmpegPath4,
1434
1435
  [
@@ -1451,7 +1452,7 @@ async function extractSampleFrames(videoPath, tempDir) {
1451
1452
  reject(new Error(`Frame extraction failed at ${timestamps[i]}s: ${error.message}`));
1452
1453
  return;
1453
1454
  }
1454
- resolve3();
1455
+ resolve4();
1455
1456
  }
1456
1457
  );
1457
1458
  });
@@ -1816,7 +1817,7 @@ async function convertAspectRatio(inputPath, outputPath, targetRatio, options =
1816
1817
  "4",
1817
1818
  outputPath
1818
1819
  ];
1819
- return new Promise((resolve3, reject) => {
1820
+ return new Promise((resolve4, reject) => {
1820
1821
  execFileRaw(ffmpegPath5, args, { maxBuffer: 10 * 1024 * 1024 }, (error, _stdout, stderr) => {
1821
1822
  if (error) {
1822
1823
  logger_default.error(`Aspect ratio conversion failed: ${stderr || error.message}`);
@@ -1824,7 +1825,7 @@ async function convertAspectRatio(inputPath, outputPath, targetRatio, options =
1824
1825
  return;
1825
1826
  }
1826
1827
  logger_default.info(`Aspect ratio conversion complete: ${outputPath}`);
1827
- resolve3(outputPath);
1828
+ resolve4(outputPath);
1828
1829
  });
1829
1830
  });
1830
1831
  }
@@ -1892,7 +1893,7 @@ async function convertWithSmartLayout(inputPath, outputPath, config2, webcamOver
1892
1893
  "4",
1893
1894
  outputPath
1894
1895
  ];
1895
- return new Promise((resolve3, reject) => {
1896
+ return new Promise((resolve4, reject) => {
1896
1897
  execFileRaw(ffmpegPath5, args, { maxBuffer: 10 * 1024 * 1024 }, (error, _stdout, stderr) => {
1897
1898
  if (error) {
1898
1899
  logger_default.error(`[${label}] FFmpeg failed: ${stderr || error.message}`);
@@ -1900,7 +1901,7 @@ async function convertWithSmartLayout(inputPath, outputPath, config2, webcamOver
1900
1901
  return;
1901
1902
  }
1902
1903
  logger_default.info(`[${label}] Complete: ${outputPath}`);
1903
- resolve3(outputPath);
1904
+ resolve4(outputPath);
1904
1905
  });
1905
1906
  });
1906
1907
  }
@@ -2068,7 +2069,7 @@ async function compositeOverlays(videoPath, overlays, outputPath, videoWidth, vi
2068
2069
  outputPath
2069
2070
  );
2070
2071
  logger_default.info(`[OverlayCompositing] Compositing ${overlays.length} overlays \u2192 ${outputPath}`);
2071
- return new Promise((resolve3, reject) => {
2072
+ return new Promise((resolve4, reject) => {
2072
2073
  execFileRaw(ffmpegPath6, args, { maxBuffer: 50 * 1024 * 1024 }, (error, _stdout, stderr) => {
2073
2074
  if (error) {
2074
2075
  logger_default.error(`[OverlayCompositing] FFmpeg failed: ${stderr}`);
@@ -2076,7 +2077,7 @@ async function compositeOverlays(videoPath, overlays, outputPath, videoWidth, vi
2076
2077
  return;
2077
2078
  }
2078
2079
  logger_default.info(`[OverlayCompositing] Complete: ${outputPath}`);
2079
- resolve3(outputPath);
2080
+ resolve4(outputPath);
2080
2081
  });
2081
2082
  });
2082
2083
  }
@@ -2092,7 +2093,7 @@ var init_overlayCompositing = __esm({
2092
2093
  // src/L2-clients/ffmpeg/transcoding.ts
2093
2094
  function transcodeToMp4(inputPath, outputPath) {
2094
2095
  const outputDir = dirname(outputPath);
2095
- return new Promise((resolve3, reject) => {
2096
+ return new Promise((resolve4, reject) => {
2096
2097
  ensureDirectory(outputDir).then(() => {
2097
2098
  logger_default.info(`Transcoding to MP4: ${inputPath} \u2192 ${outputPath}`);
2098
2099
  createFFmpeg(inputPath).outputOptions([
@@ -2114,7 +2115,7 @@ function transcodeToMp4(inputPath, outputPath) {
2114
2115
  "+faststart"
2115
2116
  ]).output(outputPath).on("end", () => {
2116
2117
  logger_default.info(`Transcoding complete: ${outputPath}`);
2117
- resolve3(outputPath);
2118
+ resolve4(outputPath);
2118
2119
  }).on("error", (err) => {
2119
2120
  logger_default.error(`Transcoding failed: ${err.message}`);
2120
2121
  reject(new Error(`Transcoding to MP4 failed: ${err.message}`));
@@ -2190,6 +2191,13 @@ var init_videoOperations = __esm({
2190
2191
  });
2191
2192
 
2192
2193
  // src/L1-infra/config/brand.ts
2194
+ var brand_exports = {};
2195
+ __export(brand_exports, {
2196
+ getBrandConfig: () => getBrandConfig,
2197
+ getIntroOutroConfig: () => getIntroOutroConfig,
2198
+ getThumbnailConfig: () => getThumbnailConfig,
2199
+ getWhisperPrompt: () => getWhisperPrompt
2200
+ });
2193
2201
  function validateBrandConfig(brand) {
2194
2202
  const requiredStrings = ["name", "handle", "tagline"];
2195
2203
  for (const field of requiredStrings) {
@@ -3539,8 +3547,8 @@ var require_semaphore = __commonJS({
3539
3547
  this._waiting = [];
3540
3548
  }
3541
3549
  lock(thunk) {
3542
- return new Promise((resolve3, reject) => {
3543
- this._waiting.push({ thunk, resolve: resolve3, reject });
3550
+ return new Promise((resolve4, reject) => {
3551
+ this._waiting.push({ thunk, resolve: resolve4, reject });
3544
3552
  this.runNext();
3545
3553
  });
3546
3554
  }
@@ -5030,9 +5038,9 @@ ${JSON.stringify(message, null, 4)}`);
5030
5038
  if (typeof cancellationStrategy.sender.enableCancellation === "function") {
5031
5039
  cancellationStrategy.sender.enableCancellation(requestMessage);
5032
5040
  }
5033
- return new Promise(async (resolve3, reject) => {
5041
+ return new Promise(async (resolve4, reject) => {
5034
5042
  const resolveWithCleanup = (r) => {
5035
- resolve3(r);
5043
+ resolve4(r);
5036
5044
  cancellationStrategy.sender.cleanup(id);
5037
5045
  disposable?.dispose();
5038
5046
  };
@@ -5444,10 +5452,10 @@ var require_ril = __commonJS({
5444
5452
  return api_1.Disposable.create(() => this.stream.off("end", listener));
5445
5453
  }
5446
5454
  write(data, encoding) {
5447
- return new Promise((resolve3, reject) => {
5455
+ return new Promise((resolve4, reject) => {
5448
5456
  const callback = (error) => {
5449
5457
  if (error === void 0 || error === null) {
5450
- resolve3();
5458
+ resolve4();
5451
5459
  } else {
5452
5460
  reject(error);
5453
5461
  }
@@ -5699,10 +5707,10 @@ var require_main = __commonJS({
5699
5707
  exports.generateRandomPipeName = generateRandomPipeName;
5700
5708
  function createClientPipeTransport(pipeName, encoding = "utf-8") {
5701
5709
  let connectResolve;
5702
- const connected = new Promise((resolve3, _reject) => {
5703
- connectResolve = resolve3;
5710
+ const connected = new Promise((resolve4, _reject) => {
5711
+ connectResolve = resolve4;
5704
5712
  });
5705
- return new Promise((resolve3, reject) => {
5713
+ return new Promise((resolve4, reject) => {
5706
5714
  let server = (0, net_1.createServer)((socket) => {
5707
5715
  server.close();
5708
5716
  connectResolve([
@@ -5713,7 +5721,7 @@ var require_main = __commonJS({
5713
5721
  server.on("error", reject);
5714
5722
  server.listen(pipeName, () => {
5715
5723
  server.removeListener("error", reject);
5716
- resolve3({
5724
+ resolve4({
5717
5725
  onConnected: () => {
5718
5726
  return connected;
5719
5727
  }
@@ -5732,10 +5740,10 @@ var require_main = __commonJS({
5732
5740
  exports.createServerPipeTransport = createServerPipeTransport;
5733
5741
  function createClientSocketTransport(port, encoding = "utf-8") {
5734
5742
  let connectResolve;
5735
- const connected = new Promise((resolve3, _reject) => {
5736
- connectResolve = resolve3;
5743
+ const connected = new Promise((resolve4, _reject) => {
5744
+ connectResolve = resolve4;
5737
5745
  });
5738
- return new Promise((resolve3, reject) => {
5746
+ return new Promise((resolve4, reject) => {
5739
5747
  const server = (0, net_1.createServer)((socket) => {
5740
5748
  server.close();
5741
5749
  connectResolve([
@@ -5746,7 +5754,7 @@ var require_main = __commonJS({
5746
5754
  server.on("error", reject);
5747
5755
  server.listen(port, "127.0.0.1", () => {
5748
5756
  server.removeListener("error", reject);
5749
- resolve3({
5757
+ resolve4({
5750
5758
  onConnected: () => {
5751
5759
  return connected;
5752
5760
  }
@@ -5966,8 +5974,8 @@ var init_session = __esm({
5966
5974
  const effectiveTimeout = timeout ?? 6e4;
5967
5975
  let resolveIdle;
5968
5976
  let rejectWithError;
5969
- const idlePromise = new Promise((resolve3, reject) => {
5970
- resolveIdle = resolve3;
5977
+ const idlePromise = new Promise((resolve4, reject) => {
5978
+ resolveIdle = resolve4;
5971
5979
  rejectWithError = reject;
5972
5980
  });
5973
5981
  let lastAssistantMessage;
@@ -6605,7 +6613,7 @@ var init_client = __esm({
6605
6613
  lastError = error instanceof Error ? error : new Error(String(error));
6606
6614
  if (attempt < 3) {
6607
6615
  const delay = 100 * Math.pow(2, attempt - 1);
6608
- await new Promise((resolve3) => setTimeout(resolve3, delay));
6616
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
6609
6617
  }
6610
6618
  }
6611
6619
  }
@@ -6949,8 +6957,8 @@ var init_client = __esm({
6949
6957
  }
6950
6958
  await this.modelsCacheLock;
6951
6959
  let resolveLock;
6952
- this.modelsCacheLock = new Promise((resolve3) => {
6953
- resolveLock = resolve3;
6960
+ this.modelsCacheLock = new Promise((resolve4) => {
6961
+ resolveLock = resolve4;
6954
6962
  });
6955
6963
  try {
6956
6964
  if (this.modelsCache !== null) {
@@ -7147,7 +7155,7 @@ var init_client = __esm({
7147
7155
  * Start the CLI server process
7148
7156
  */
7149
7157
  async startCLIServer() {
7150
- return new Promise((resolve3, reject) => {
7158
+ return new Promise((resolve4, reject) => {
7151
7159
  this.stderrBuffer = "";
7152
7160
  const args = [
7153
7161
  ...this.options.cliArgs,
@@ -7198,7 +7206,7 @@ var init_client = __esm({
7198
7206
  let resolved = false;
7199
7207
  if (this.options.useStdio) {
7200
7208
  resolved = true;
7201
- resolve3();
7209
+ resolve4();
7202
7210
  } else {
7203
7211
  this.cliProcess.stdout?.on("data", (data) => {
7204
7212
  stdout += data.toString();
@@ -7206,7 +7214,7 @@ var init_client = __esm({
7206
7214
  if (match && !resolved) {
7207
7215
  this.actualPort = parseInt(match[1], 10);
7208
7216
  resolved = true;
7209
- resolve3();
7217
+ resolve4();
7210
7218
  }
7211
7219
  });
7212
7220
  }
@@ -7335,7 +7343,7 @@ stderr: ${stderrOutput}`
7335
7343
  if (!this.actualPort) {
7336
7344
  throw new Error("Server port not available");
7337
7345
  }
7338
- return new Promise((resolve3, reject) => {
7346
+ return new Promise((resolve4, reject) => {
7339
7347
  this.socket = new Socket();
7340
7348
  this.socket.connect(this.actualPort, this.actualHost, () => {
7341
7349
  this.connection = (0, import_node2.createMessageConnection)(
@@ -7344,7 +7352,7 @@ stderr: ${stderrOutput}`
7344
7352
  );
7345
7353
  this.attachConnectionHandlers();
7346
7354
  this.connection.listen();
7347
- resolve3();
7355
+ resolve4();
7348
7356
  });
7349
7357
  this.socket.on("error", (error) => {
7350
7358
  reject(new Error(`Failed to connect to CLI server: ${error.message}`));
@@ -7828,7 +7836,7 @@ var init_CopilotProvider = __esm({
7828
7836
  logger_default.info("[CopilotProvider] Creating session\u2026");
7829
7837
  let copilotSession;
7830
7838
  try {
7831
- copilotSession = await new Promise((resolve3, reject) => {
7839
+ copilotSession = await new Promise((resolve4, reject) => {
7832
7840
  const timeoutId = setTimeout(
7833
7841
  () => reject(new Error(
7834
7842
  `[CopilotProvider] createSession timed out after ${SESSION_CREATE_TIMEOUT_MS / 1e3}s \u2014 the Copilot SDK language server may not be reachable. Check GitHub authentication and network connectivity.`
@@ -7851,7 +7859,7 @@ var init_CopilotProvider = __esm({
7851
7859
  }).then(
7852
7860
  (session) => {
7853
7861
  clearTimeout(timeoutId);
7854
- resolve3(session);
7862
+ resolve4(session);
7855
7863
  },
7856
7864
  (err) => {
7857
7865
  clearTimeout(timeoutId);
@@ -7942,7 +7950,7 @@ var init_CopilotProvider = __esm({
7942
7950
  * would fire while the agent is legitimately waiting for the user.
7943
7951
  */
7944
7952
  sendAndWaitForIdle(message) {
7945
- return new Promise((resolve3, reject) => {
7953
+ return new Promise((resolve4, reject) => {
7946
7954
  let lastAssistantMessage;
7947
7955
  const unsubMessage = this.session.on("assistant.message", (event) => {
7948
7956
  lastAssistantMessage = event;
@@ -7951,7 +7959,7 @@ var init_CopilotProvider = __esm({
7951
7959
  unsubMessage();
7952
7960
  unsubIdle();
7953
7961
  unsubError();
7954
- resolve3(lastAssistantMessage);
7962
+ resolve4(lastAssistantMessage);
7955
7963
  });
7956
7964
  const unsubError = this.session.on("session.error", (event) => {
7957
7965
  unsubMessage();
@@ -8658,7 +8666,7 @@ var init_BaseAgent = __esm({
8658
8666
  this.resetForRetry();
8659
8667
  const delayMs = 2e3 * Math.pow(2, attempt - 1);
8660
8668
  logger_default.warn(`[${this.agentName}] Transient error (attempt ${attempt}/${_BaseAgent.MAX_RETRIES}), retrying in ${delayMs / 1e3}s: ${message}`);
8661
- await new Promise((resolve3) => setTimeout(resolve3, delayMs));
8669
+ await new Promise((resolve4) => setTimeout(resolve4, delayMs));
8662
8670
  }
8663
8671
  }
8664
8672
  throw lastError;
@@ -9811,22 +9819,6 @@ var init_ideaService = __esm({
9811
9819
  });
9812
9820
 
9813
9821
  // src/L3-services/postStore/postStore.ts
9814
- var postStore_exports = {};
9815
- __export(postStore_exports, {
9816
- approveBulk: () => approveBulk,
9817
- approveItem: () => approveItem,
9818
- createItem: () => createItem,
9819
- getGroupedPendingItems: () => getGroupedPendingItems,
9820
- getItem: () => getItem,
9821
- getPendingItems: () => getPendingItems,
9822
- getPublishedItemByLatePostId: () => getPublishedItemByLatePostId,
9823
- getPublishedItems: () => getPublishedItems,
9824
- getScheduledItemsByIdeaIds: () => getScheduledItemsByIdeaIds,
9825
- itemExists: () => itemExists,
9826
- rejectItem: () => rejectItem,
9827
- updateItem: () => updateItem,
9828
- updatePublishedItemSchedule: () => updatePublishedItemSchedule
9829
- });
9830
9822
  function getQueueDir() {
9831
9823
  const { OUTPUT_DIR } = getConfig();
9832
9824
  return join(OUTPUT_DIR, "publish-queue");
@@ -10207,10 +10199,6 @@ async function getScheduledItemsByIdeaIds(ideaIds) {
10207
10199
  (item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
10208
10200
  );
10209
10201
  }
10210
- async function getPublishedItemByLatePostId(latePostId) {
10211
- const publishedItems = await getPublishedItems();
10212
- return publishedItems.find((item) => item.metadata.latePostId === latePostId) ?? null;
10213
- }
10214
10202
  async function updatePublishedItemSchedule(id, scheduledFor) {
10215
10203
  if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
10216
10204
  throw new Error(`Invalid ID format: ${id}`);
@@ -10572,8 +10560,8 @@ function validateByClipType(byClipType, platformName) {
10572
10560
  return validated;
10573
10561
  }
10574
10562
  function validatePositiveNumber(value, fieldName) {
10575
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
10576
- throw new Error(`${fieldName} must be a positive number`);
10563
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
10564
+ throw new Error(`${fieldName} must be a non-negative number`);
10577
10565
  }
10578
10566
  return value;
10579
10567
  }
@@ -10717,9 +10705,6 @@ function getPlatformSchedule(platform, clipType) {
10717
10705
  function getIdeaSpacingConfig() {
10718
10706
  return cachedConfig?.ideaSpacing ?? { ...defaultIdeaSpacing };
10719
10707
  }
10720
- function getDisplacementConfig() {
10721
- return cachedConfig?.displacement ?? { ...defaultDisplacement };
10722
- }
10723
10708
  var VALID_DAYS, TIME_REGEX, defaultIdeaSpacing, defaultDisplacement, cachedConfig, PLATFORM_ALIASES;
10724
10709
  var init_scheduleConfig = __esm({
10725
10710
  "src/L3-services/scheduler/scheduleConfig.ts"() {
@@ -10813,9 +10798,11 @@ async function buildBookedMap(platform) {
10813
10798
  getPublishedItems()
10814
10799
  ]);
10815
10800
  const ideaLinkedPostIds = /* @__PURE__ */ new Set();
10801
+ const latePostIdToIdeaIds = /* @__PURE__ */ new Map();
10816
10802
  for (const item of publishedItems) {
10817
10803
  if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
10818
10804
  ideaLinkedPostIds.add(item.metadata.latePostId);
10805
+ latePostIdToIdeaIds.set(item.metadata.latePostId, item.metadata.ideaIds);
10819
10806
  }
10820
10807
  }
10821
10808
  const map = /* @__PURE__ */ new Map();
@@ -10830,7 +10817,8 @@ async function buildBookedMap(platform) {
10830
10817
  postId: post._id,
10831
10818
  platform: scheduledPlatform.platform,
10832
10819
  status: post.status,
10833
- ideaLinked: ideaLinkedPostIds.has(post._id)
10820
+ ideaLinked: ideaLinkedPostIds.has(post._id),
10821
+ ideaIds: latePostIdToIdeaIds.get(post._id)
10834
10822
  });
10835
10823
  }
10836
10824
  }
@@ -10839,28 +10827,20 @@ async function buildBookedMap(platform) {
10839
10827
  if (platform && item.metadata.platform !== platform) continue;
10840
10828
  if (!item.metadata.scheduledFor) continue;
10841
10829
  const ms = normalizeDateTime(item.metadata.scheduledFor);
10830
+ if (ms < Date.now()) continue;
10842
10831
  if (!map.has(ms)) {
10843
10832
  map.set(ms, {
10844
10833
  scheduledFor: item.metadata.scheduledFor,
10845
10834
  source: "local",
10846
10835
  itemId: item.id,
10847
10836
  platform: item.metadata.platform,
10848
- ideaLinked: Boolean(item.metadata.ideaIds?.length)
10837
+ ideaLinked: Boolean(item.metadata.ideaIds?.length),
10838
+ ideaIds: item.metadata.ideaIds
10849
10839
  });
10850
10840
  }
10851
10841
  }
10852
10842
  return map;
10853
10843
  }
10854
- async function getIdeaLinkedLatePostIds() {
10855
- const publishedItems = await getPublishedItems();
10856
- const ids = /* @__PURE__ */ new Set();
10857
- for (const item of publishedItems) {
10858
- if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
10859
- ids.add(item.metadata.latePostId);
10860
- }
10861
- }
10862
- return ids;
10863
- }
10864
10844
  function* generateTimeslots(platformConfig, timezone, fromMs, maxMs) {
10865
10845
  const baseDate = new Date(fromMs);
10866
10846
  const upperMs = maxMs ?? fromMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
@@ -10932,13 +10912,55 @@ async function getIdeaReferences(ideaIds, bookedMap) {
10932
10912
  }
10933
10913
  return refs;
10934
10914
  }
10935
- async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10915
+ function getBookedSlotPublishByMs(booked, ideaPublishByMap) {
10916
+ if (!booked.ideaIds?.length) return void 0;
10917
+ let earliest;
10918
+ for (const ideaId of booked.ideaIds) {
10919
+ const ms = ideaPublishByMap.get(ideaId);
10920
+ if (ms !== void 0 && (earliest === void 0 || ms < earliest)) {
10921
+ earliest = ms;
10922
+ }
10923
+ }
10924
+ return earliest;
10925
+ }
10926
+ async function buildIdeaPublishByMap(bookedMap, lookupIdeaPublishBy) {
10927
+ const allIdeaIds = /* @__PURE__ */ new Set();
10928
+ for (const slot of bookedMap.values()) {
10929
+ if (slot.ideaIds) {
10930
+ for (const id of slot.ideaIds) allIdeaIds.add(id);
10931
+ }
10932
+ }
10933
+ const map = /* @__PURE__ */ new Map();
10934
+ if (allIdeaIds.size === 0) return map;
10935
+ for (const ideaId of allIdeaIds) {
10936
+ const ms = await lookupIdeaPublishBy(ideaId);
10937
+ if (ms !== void 0) map.set(ideaId, ms);
10938
+ }
10939
+ return map;
10940
+ }
10941
+ async function createIdeaPublishByLookup() {
10942
+ try {
10943
+ const { getIdea: getIdea2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
10944
+ return async (ideaId) => {
10945
+ try {
10946
+ const idea = await getIdea2(parseInt(ideaId, 10));
10947
+ if (idea?.publishBy) return new Date(idea.publishBy).getTime();
10948
+ } catch {
10949
+ }
10950
+ return void 0;
10951
+ };
10952
+ } catch {
10953
+ return async () => void 0;
10954
+ }
10955
+ }
10956
+ async function findSlot(platformConfig, fromMs, isIdeaPost, ownPostId, label, ctx) {
10936
10957
  const indent = " ".repeat(ctx.depth);
10937
10958
  let checked = 0;
10938
10959
  let skippedBooked = 0;
10939
10960
  let skippedSpacing = 0;
10940
- logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}`);
10941
- for (const { datetime, ms } of generateTimeslots(platformConfig, ctx.timezone, fromMs)) {
10961
+ const maxMs = ctx.depth === 0 ? ctx.publishByMs : void 0;
10962
+ logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}${maxMs ? ` until ${new Date(maxMs).toISOString()}` : ""}`);
10963
+ for (const { datetime, ms } of generateTimeslots(platformConfig, ctx.timezone, fromMs, maxMs)) {
10942
10964
  checked++;
10943
10965
  const booked = ctx.bookedMap.get(ms);
10944
10966
  if (!booked) {
@@ -10949,10 +10971,14 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10949
10971
  }
10950
10972
  continue;
10951
10973
  }
10952
- logger_default.debug(`${indent}[schedulePost] \u2705 Found empty slot: ${datetime} (checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing)`);
10974
+ logger_default.info(`${indent}[schedulePost] \u2705 Found empty slot: ${datetime} (checked ${checked}, skipped ${skippedBooked} booked, ${skippedSpacing} spacing)`);
10975
+ return datetime;
10976
+ }
10977
+ if (ownPostId && booked.postId === ownPostId) {
10978
+ logger_default.info(`${indent}[schedulePost] \u2705 Keeping own slot: ${datetime} (checked ${checked})`);
10953
10979
  return datetime;
10954
10980
  }
10955
- if (isIdeaPost && ctx.displacementEnabled && !booked.ideaLinked && booked.source === "late" && booked.postId) {
10981
+ if (isIdeaPost && !booked.ideaLinked && booked.source === "late" && booked.postId) {
10956
10982
  if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
10957
10983
  skippedSpacing++;
10958
10984
  if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
@@ -10961,12 +10987,13 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10961
10987
  continue;
10962
10988
  }
10963
10989
  logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by non-idea post ${booked.postId} \u2014 displacing`);
10964
- const newHome = await schedulePost(
10990
+ const newHome = await findSlot(
10965
10991
  platformConfig,
10966
10992
  ms,
10967
10993
  false,
10994
+ booked.postId,
10968
10995
  `displaced:${booked.postId}`,
10969
- { ...ctx, depth: ctx.depth + 1 }
10996
+ { ...ctx, depth: ctx.depth + 1, publishByMs: void 0 }
10970
10997
  );
10971
10998
  if (newHome) {
10972
10999
  if (!ctx.dryRun) {
@@ -10982,12 +11009,51 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10982
11009
  ctx.bookedMap.delete(ms);
10983
11010
  const newMs = normalizeDateTime(newHome);
10984
11011
  ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
10985
- logger_default.debug(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked} candidates)`);
11012
+ logger_default.info(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked}, displaced ${booked.postId})`);
10986
11013
  return datetime;
10987
11014
  }
10988
11015
  logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace ${booked.postId} \u2014 no empty slot found after ${datetime}`);
10989
11016
  }
10990
- if (booked.ideaLinked) {
11017
+ if (isIdeaPost && booked.ideaLinked && booked.source === "late" && booked.postId) {
11018
+ if (ctx.publishByMs) {
11019
+ const incumbentPublishByMs = getBookedSlotPublishByMs(booked, ctx.ideaPublishByMap);
11020
+ if (!incumbentPublishByMs || ctx.publishByMs < incumbentPublishByMs) {
11021
+ if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
11022
+ skippedSpacing++;
11023
+ if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
11024
+ logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping (even though idea-displaceable)`);
11025
+ }
11026
+ continue;
11027
+ }
11028
+ logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by idea post ${booked.postId} with later deadline \u2014 displacing`);
11029
+ const newHome = await findSlot(
11030
+ platformConfig,
11031
+ ms,
11032
+ true,
11033
+ booked.postId,
11034
+ `displaced-idea:${booked.postId}`,
11035
+ { ...ctx, depth: ctx.depth + 1, publishByMs: incumbentPublishByMs }
11036
+ );
11037
+ if (newHome) {
11038
+ if (!ctx.dryRun) {
11039
+ try {
11040
+ await ctx.lateClient.schedulePost(booked.postId, newHome);
11041
+ } catch (err) {
11042
+ const msg = err instanceof Error ? err.message : String(err);
11043
+ logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Failed to displace idea ${booked.postId} via Late API: ${msg} \u2014 skipping slot`);
11044
+ continue;
11045
+ }
11046
+ }
11047
+ logger_default.info(`${indent}[schedulePost] \u{1F4E6} Displaced idea ${booked.postId}: ${datetime} \u2192 ${newHome}`);
11048
+ ctx.bookedMap.delete(ms);
11049
+ const newMs = normalizeDateTime(newHome);
11050
+ ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
11051
+ logger_default.info(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked}, displaced idea ${booked.postId})`);
11052
+ return datetime;
11053
+ }
11054
+ logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace idea ${booked.postId} \u2014 no slot found after ${datetime}`);
11055
+ }
11056
+ }
10991
11057
  skippedBooked++;
10992
11058
  if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
10993
11059
  logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken by idea post ${booked.postId ?? booked.itemId} \u2014 skipping`);
@@ -11002,135 +11068,187 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
11002
11068
  logger_default.warn(`[schedulePost] \u274C No slot found for ${label} \u2014 checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing`);
11003
11069
  return null;
11004
11070
  }
11005
- async function findNextSlot(platform, clipType, options) {
11071
+ async function schedulePost(platform, clipType, options) {
11006
11072
  const config2 = await loadScheduleConfig();
11007
11073
  const platformConfig = getPlatformSchedule(platform, clipType);
11008
11074
  if (!platformConfig) {
11009
- logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
11075
+ logger_default.warn(`[schedulePost] No schedule config found for platform "${sanitizeLogValue(platform)}"`);
11010
11076
  return null;
11011
11077
  }
11012
11078
  const { timezone } = config2;
11013
11079
  const nowMs = Date.now();
11014
11080
  const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
11015
- const isIdeaAware = ideaIds.length > 0;
11016
- const bookedMap = await buildBookedMap(platform);
11017
- const ideaLinkedPostIds = await getIdeaLinkedLatePostIds();
11081
+ const isIdeaPost = ideaIds.length > 0;
11082
+ const publishBy = options?.publishBy;
11083
+ const publishByMs = publishBy ? new Date(publishBy).getTime() : void 0;
11084
+ const postId = options?.postId;
11085
+ const dryRun = options?.dryRun ?? false;
11018
11086
  const label = `${platform}/${clipType ?? "default"}`;
11087
+ const bookedMap = options?._bookedMap ?? await buildBookedMap(platform);
11019
11088
  let ideaRefs = [];
11020
11089
  let samePlatformMs = 0;
11021
11090
  let crossPlatformMs = 0;
11022
- if (isIdeaAware) {
11023
- const allBookedMap = await buildBookedMap();
11091
+ if (isIdeaPost) {
11092
+ const allBookedMap = options?._bookedMap ?? await buildBookedMap();
11024
11093
  ideaRefs = await getIdeaReferences(ideaIds, allBookedMap);
11025
11094
  const spacingConfig = getIdeaSpacingConfig();
11026
11095
  samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
11027
11096
  crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
11028
11097
  }
11029
- logger_default.info(`[findNextSlot] Scheduling ${label} (idea=${isIdeaAware}, booked=${bookedMap.size} slots, spacingRefs=${ideaRefs.length})`);
11098
+ let ideaPublishByMap;
11099
+ if (options?._ideaPublishByMap) {
11100
+ ideaPublishByMap = options._ideaPublishByMap;
11101
+ } else if (publishByMs && Number.isFinite(publishByMs) && publishByMs > nowMs) {
11102
+ const lookup = await createIdeaPublishByLookup();
11103
+ ideaPublishByMap = await buildIdeaPublishByMap(bookedMap, lookup);
11104
+ } else {
11105
+ ideaPublishByMap = /* @__PURE__ */ new Map();
11106
+ }
11107
+ logger_default.info(`[schedulePost] Scheduling ${label} (idea=${isIdeaPost}, booked=${bookedMap.size} slots, spacingRefs=${ideaRefs.length}${publishBy ? `, publishBy=${publishBy}` : ""}${postId ? `, postId=${postId}` : ""})`);
11030
11108
  const ctx = {
11031
11109
  timezone,
11032
11110
  bookedMap,
11033
- ideaLinkedPostIds,
11034
11111
  lateClient: new LateApiClient(),
11035
- displacementEnabled: getDisplacementConfig().enabled,
11036
- dryRun: false,
11112
+ dryRun,
11037
11113
  depth: 0,
11038
11114
  ideaRefs,
11039
11115
  samePlatformMs,
11040
11116
  crossPlatformMs,
11041
- platform
11117
+ platform,
11118
+ ideaPublishByMap
11042
11119
  };
11043
- const result = await schedulePost(platformConfig, nowMs, isIdeaAware, label, ctx);
11120
+ let result = null;
11121
+ if (publishByMs && Number.isFinite(publishByMs) && publishByMs > nowMs) {
11122
+ result = await findSlot(platformConfig, nowMs, isIdeaPost, postId, label, { ...ctx, publishByMs });
11123
+ if (!result) {
11124
+ logger_default.warn(`[schedulePost] \u26A0\uFE0F No slot for ${label} before publishBy ${publishBy} \u2014 searching past deadline`);
11125
+ result = await findSlot(platformConfig, nowMs, isIdeaPost, postId, label, { ...ctx, publishByMs: void 0 });
11126
+ }
11127
+ } else {
11128
+ result = await findSlot(platformConfig, nowMs, isIdeaPost, postId, label, ctx);
11129
+ }
11044
11130
  if (!result) {
11045
- logger_default.warn(`[findNextSlot] No available slot for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
11131
+ logger_default.warn(`[schedulePost] \u274C No available slot for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
11132
+ } else if (publishByMs && Number.isFinite(publishByMs)) {
11133
+ const slotMs = new Date(result).getTime();
11134
+ if (slotMs > publishByMs) {
11135
+ const daysLate = Math.ceil((slotMs - publishByMs) / (24 * 60 * 60 * 1e3));
11136
+ logger_default.warn(`[schedulePost] \u26A0\uFE0F ${label} scheduled for ${result} \u2014 ${daysLate} day(s) AFTER publishBy deadline ${publishBy}`);
11137
+ } else {
11138
+ logger_default.info(`[schedulePost] \u2705 ${label} \u2192 ${result} (within publishBy ${publishBy})`);
11139
+ }
11140
+ } else {
11141
+ logger_default.info(`[schedulePost] \u2705 ${label} \u2192 ${result}`);
11046
11142
  }
11047
11143
  return result;
11048
11144
  }
11049
- async function rescheduleIdeaPosts(options) {
11145
+ async function findNextSlot(platform, clipType, options) {
11146
+ return schedulePost(platform, clipType, options);
11147
+ }
11148
+ async function rescheduleAllPosts(options) {
11050
11149
  const dryRun = options?.dryRun ?? false;
11051
- const { updatePublishedItemSchedule: updatePublishedItemSchedule2 } = await Promise.resolve().then(() => (init_postStore(), postStore_exports));
11052
11150
  const config2 = await loadScheduleConfig();
11053
11151
  const { timezone } = config2;
11054
11152
  const publishedItems = await getPublishedItems();
11055
- const ideaPosts = publishedItems.filter(
11056
- (item) => item.metadata.ideaIds?.length && item.metadata.latePostId
11057
- );
11058
- if (ideaPosts.length === 0) {
11059
- logger_default.info("No idea-linked posts to reschedule");
11153
+ const postsWithLateId = publishedItems.filter((item) => item.metadata.latePostId);
11154
+ if (postsWithLateId.length === 0) {
11155
+ logger_default.info("No posts to reschedule");
11060
11156
  return { rescheduled: 0, unchanged: 0, failed: 0, details: [] };
11061
11157
  }
11062
- logger_default.info(`Found ${ideaPosts.length} idea-linked posts to reschedule`);
11063
- const ideaLatePostIds = new Set(ideaPosts.map((item) => item.metadata.latePostId));
11064
- const fullBookedMap = await buildBookedMap();
11065
- for (const [ms, slot] of fullBookedMap) {
11066
- if (slot.postId && ideaLatePostIds.has(slot.postId)) {
11067
- fullBookedMap.delete(ms);
11068
- }
11069
- }
11070
- const { getIdea: getIdea2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
11071
- const ideaPublishByMap = /* @__PURE__ */ new Map();
11158
+ const ideaPosts = postsWithLateId.filter((item) => item.metadata.ideaIds?.length);
11159
+ const nonIdeaPosts = postsWithLateId.filter((item) => !item.metadata.ideaIds?.length);
11160
+ const lookup = await createIdeaPublishByLookup();
11161
+ const ideaPublishByStringMap = /* @__PURE__ */ new Map();
11072
11162
  for (const item of ideaPosts) {
11073
11163
  const ideaId = item.metadata.ideaIds?.[0];
11074
- if (ideaId && !ideaPublishByMap.has(ideaId)) {
11075
- try {
11076
- const idea = await getIdea2(parseInt(ideaId, 10));
11077
- if (idea?.publishBy) ideaPublishByMap.set(ideaId, idea.publishBy);
11078
- } catch {
11164
+ if (ideaId && !ideaPublishByStringMap.has(ideaId)) {
11165
+ const publishByMs = await lookup(ideaId);
11166
+ if (publishByMs !== void 0) {
11167
+ ideaPublishByStringMap.set(ideaId, new Date(publishByMs).toISOString());
11079
11168
  }
11080
11169
  }
11081
11170
  }
11082
11171
  ideaPosts.sort((a, b) => {
11083
11172
  const aId = a.metadata.ideaIds?.[0];
11084
11173
  const bId = b.metadata.ideaIds?.[0];
11085
- const aDate = aId ? ideaPublishByMap.get(aId) ?? "9999" : "9999";
11086
- const bDate = bId ? ideaPublishByMap.get(bId) ?? "9999" : "9999";
11174
+ const aDate = aId ? ideaPublishByStringMap.get(aId) ?? "9999" : "9999";
11175
+ const bDate = bId ? ideaPublishByStringMap.get(bId) ?? "9999" : "9999";
11087
11176
  return aDate.localeCompare(bDate);
11088
11177
  });
11178
+ const allPosts = [...ideaPosts, ...nonIdeaPosts];
11179
+ logger_default.info(`Rescheduling ${allPosts.length} posts (${ideaPosts.length} idea, ${nonIdeaPosts.length} non-idea)`);
11180
+ const bookedMap = await buildBookedMap();
11181
+ const ideaPublishByMsMap = /* @__PURE__ */ new Map();
11182
+ for (const [ideaId, dateStr] of ideaPublishByStringMap) {
11183
+ ideaPublishByMsMap.set(ideaId, new Date(dateStr).getTime());
11184
+ }
11089
11185
  const lateClient = new LateApiClient();
11090
11186
  const result = { rescheduled: 0, unchanged: 0, failed: 0, details: [] };
11091
11187
  const nowMs = Date.now();
11092
- const ctx = {
11093
- timezone,
11094
- bookedMap: fullBookedMap,
11095
- ideaLinkedPostIds: /* @__PURE__ */ new Set(),
11096
- lateClient,
11097
- displacementEnabled: getDisplacementConfig().enabled,
11098
- dryRun,
11099
- depth: 0,
11100
- ideaRefs: [],
11101
- samePlatformMs: 0,
11102
- crossPlatformMs: 0,
11103
- platform: ""
11104
- };
11105
- for (const item of ideaPosts) {
11106
- const platform = item.metadata.platform;
11188
+ for (const item of allPosts) {
11189
+ const itemPlatform = item.metadata.platform;
11107
11190
  const clipType = item.metadata.clipType;
11108
11191
  const latePostId = item.metadata.latePostId;
11109
11192
  const oldSlot = item.metadata.scheduledFor;
11110
- const label = `${item.id} (${platform}/${clipType})`;
11193
+ const isIdea = Boolean(item.metadata.ideaIds?.length);
11194
+ const label = `${item.id} (${itemPlatform}/${clipType})`;
11111
11195
  try {
11112
- const platformConfig = getPlatformSchedule(platform, clipType);
11196
+ const platformConfig = getPlatformSchedule(itemPlatform, clipType);
11113
11197
  if (!platformConfig) {
11114
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: "No schedule config" });
11198
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "No schedule config" });
11115
11199
  result.failed++;
11116
11200
  continue;
11117
11201
  }
11118
- const newSlotDatetime = await schedulePost(platformConfig, nowMs, true, label, ctx);
11202
+ let ideaRefs = [];
11203
+ let samePlatformMs = 0;
11204
+ let crossPlatformMs = 0;
11205
+ if (isIdea) {
11206
+ ideaRefs = await getIdeaReferences(item.metadata.ideaIds, bookedMap);
11207
+ const spacingConfig = getIdeaSpacingConfig();
11208
+ samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
11209
+ crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
11210
+ }
11211
+ const publishBy = isIdea && item.metadata.ideaIds?.[0] ? ideaPublishByStringMap.get(item.metadata.ideaIds[0]) : void 0;
11212
+ const publishByMs = publishBy ? new Date(publishBy).getTime() : void 0;
11213
+ const ctx = {
11214
+ timezone,
11215
+ bookedMap,
11216
+ lateClient,
11217
+ dryRun,
11218
+ depth: 0,
11219
+ ideaRefs,
11220
+ samePlatformMs,
11221
+ crossPlatformMs,
11222
+ platform: itemPlatform,
11223
+ publishByMs: void 0,
11224
+ ideaPublishByMap: ideaPublishByMsMap
11225
+ };
11226
+ let newSlotDatetime = null;
11227
+ if (publishByMs && Number.isFinite(publishByMs) && publishByMs > nowMs) {
11228
+ newSlotDatetime = await findSlot(platformConfig, nowMs, isIdea, latePostId, label, { ...ctx, publishByMs });
11229
+ if (!newSlotDatetime) {
11230
+ logger_default.warn(`[reschedule] \u26A0\uFE0F No slot for ${label} before publishBy \u2014 searching past deadline`);
11231
+ newSlotDatetime = await findSlot(platformConfig, nowMs, isIdea, latePostId, label, ctx);
11232
+ }
11233
+ } else {
11234
+ newSlotDatetime = await findSlot(platformConfig, nowMs, isIdea, latePostId, label, ctx);
11235
+ }
11119
11236
  if (!newSlotDatetime) {
11120
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: "No slot found" });
11237
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "No slot found" });
11121
11238
  result.failed++;
11122
11239
  continue;
11123
11240
  }
11124
11241
  const newSlotMs = normalizeDateTime(newSlotDatetime);
11125
11242
  if (oldSlot && normalizeDateTime(oldSlot) === newSlotMs) {
11126
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: newSlotDatetime });
11243
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
11127
11244
  result.unchanged++;
11128
- ctx.bookedMap.set(newSlotMs, {
11245
+ bookedMap.set(newSlotMs, {
11129
11246
  scheduledFor: newSlotDatetime,
11130
11247
  source: "late",
11131
11248
  postId: latePostId,
11132
- platform,
11133
- ideaLinked: true
11249
+ platform: itemPlatform,
11250
+ ideaLinked: isIdea,
11251
+ ideaIds: item.metadata.ideaIds
11134
11252
  });
11135
11253
  continue;
11136
11254
  }
@@ -11141,34 +11259,45 @@ async function rescheduleIdeaPosts(options) {
11141
11259
  const errMsg = scheduleErr instanceof Error ? scheduleErr.message : String(scheduleErr);
11142
11260
  if (errMsg.includes("Published posts can only have their recycling config updated")) {
11143
11261
  logger_default.info(`Skipping ${label}: post already published on platform`);
11144
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: "Already published \u2014 skipped" });
11262
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "Already published \u2014 skipped" });
11145
11263
  result.unchanged++;
11146
11264
  continue;
11147
11265
  }
11148
11266
  throw scheduleErr;
11149
11267
  }
11150
- await updatePublishedItemSchedule2(item.id, newSlotDatetime);
11268
+ await updatePublishedItemSchedule(item.id, newSlotDatetime);
11151
11269
  }
11152
- ctx.bookedMap.set(newSlotMs, {
11270
+ if (oldSlot) {
11271
+ const oldMs = normalizeDateTime(oldSlot);
11272
+ const oldBooked = bookedMap.get(oldMs);
11273
+ if (oldBooked?.postId === latePostId) {
11274
+ bookedMap.delete(oldMs);
11275
+ }
11276
+ }
11277
+ bookedMap.set(newSlotMs, {
11153
11278
  scheduledFor: newSlotDatetime,
11154
11279
  source: "late",
11155
11280
  postId: latePostId,
11156
- platform,
11157
- ideaLinked: true
11281
+ platform: itemPlatform,
11282
+ ideaLinked: isIdea,
11283
+ ideaIds: item.metadata.ideaIds
11158
11284
  });
11159
11285
  logger_default.info(`Rescheduled ${label}: ${oldSlot ?? "unscheduled"} \u2192 ${newSlotDatetime}`);
11160
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: newSlotDatetime });
11286
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
11161
11287
  result.rescheduled++;
11162
11288
  } catch (err) {
11163
11289
  const msg = err instanceof Error ? err.message : String(err);
11164
11290
  logger_default.error(`Failed to reschedule ${label}: ${msg}`);
11165
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: msg });
11291
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: msg });
11166
11292
  result.failed++;
11167
11293
  }
11168
11294
  }
11169
11295
  logger_default.info(`Reschedule complete: ${result.rescheduled} moved, ${result.unchanged} unchanged, ${result.failed} failed`);
11170
11296
  return result;
11171
11297
  }
11298
+ async function rescheduleIdeaPosts(options) {
11299
+ return rescheduleAllPosts(options);
11300
+ }
11172
11301
  async function getScheduleCalendar(startDate, endDate) {
11173
11302
  const bookedMap = await buildBookedMap();
11174
11303
  let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
@@ -11470,7 +11599,7 @@ var FileWatcher = class _FileWatcher extends EventEmitter {
11470
11599
  async isFileStable(filePath) {
11471
11600
  try {
11472
11601
  const sizeBefore = getFileStatsSync(filePath).size;
11473
- await new Promise((resolve3) => setTimeout(resolve3, _FileWatcher.EXTRA_STABILITY_DELAY));
11602
+ await new Promise((resolve4) => setTimeout(resolve4, _FileWatcher.EXTRA_STABILITY_DELAY));
11474
11603
  const sizeAfter = getFileStatsSync(filePath).size;
11475
11604
  return sizeBefore === sizeAfter;
11476
11605
  } catch {
@@ -12314,7 +12443,7 @@ async function analyzeVideoEditorial(videoPath, durationSeconds, model) {
12314
12443
  logger_default.info(`[Gemini] Waiting for file processing to complete...`);
12315
12444
  let fileState = file.state;
12316
12445
  while (fileState === "PROCESSING") {
12317
- await new Promise((resolve3) => setTimeout(resolve3, 2e3));
12446
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
12318
12447
  const updated = await ai.files.get({ name: file.name });
12319
12448
  fileState = updated.state;
12320
12449
  logger_default.debug(`[Gemini] File state: ${fileState}`);
@@ -12358,7 +12487,7 @@ async function analyzeVideoClipDirection(videoPath, durationSeconds, model) {
12358
12487
  logger_default.info(`[Gemini] Waiting for file processing to complete...`);
12359
12488
  let fileState = file.state;
12360
12489
  while (fileState === "PROCESSING") {
12361
- await new Promise((resolve3) => setTimeout(resolve3, 2e3));
12490
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
12362
12491
  const updated = await ai.files.get({ name: file.name });
12363
12492
  fileState = updated.state;
12364
12493
  logger_default.debug(`[Gemini] File state: ${fileState}`);
@@ -12431,7 +12560,7 @@ async function analyzeVideoForEnhancements(videoPath, durationSeconds, transcrip
12431
12560
  logger_default.info(`[Gemini] Waiting for file processing to complete...`);
12432
12561
  let fileState = file.state;
12433
12562
  while (fileState === "PROCESSING") {
12434
- await new Promise((resolve3) => setTimeout(resolve3, 2e3));
12563
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
12435
12564
  const updated = await ai.files.get({ name: file.name });
12436
12565
  fileState = updated.state;
12437
12566
  logger_default.debug(`[Gemini] File state: ${fileState}`);
@@ -12542,7 +12671,7 @@ async function transcribeAudio(audioPath) {
12542
12671
  if (attempt === MAX_RETRIES) throw retryError;
12543
12672
  const msg = retryError instanceof Error ? retryError.message : String(retryError);
12544
12673
  logger_default.warn(`Whisper attempt ${attempt}/${MAX_RETRIES} failed: ${msg} \u2014 retrying in ${RETRY_DELAY_MS / 1e3}s`);
12545
- await new Promise((resolve3) => setTimeout(resolve3, RETRY_DELAY_MS));
12674
+ await new Promise((resolve4) => setTimeout(resolve4, RETRY_DELAY_MS));
12546
12675
  }
12547
12676
  }
12548
12677
  if (!response) throw new Error("Whisper transcription failed after all retries");
@@ -16101,6 +16230,7 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
16101
16230
  let mediaPath = null;
16102
16231
  let sourceClip = null;
16103
16232
  let thumbnailPath = null;
16233
+ let clipIdeaIssueNumber;
16104
16234
  if (frontmatter.shortSlug) {
16105
16235
  const short = shorts.find((s) => s.slug === frontmatter.shortSlug);
16106
16236
  const medium = mediumClips.find((m) => m.slug === frontmatter.shortSlug);
@@ -16110,12 +16240,14 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
16110
16240
  sourceClip = dirname(short.outputPath);
16111
16241
  mediaPath = resolveShortMedia(short, post.platform);
16112
16242
  thumbnailPath = short.thumbnailPath ?? null;
16243
+ clipIdeaIssueNumber = short.ideaIssueNumber;
16113
16244
  } else if (medium) {
16114
16245
  clipSlug = medium.slug;
16115
16246
  clipType = "medium-clip";
16116
16247
  sourceClip = dirname(medium.outputPath);
16117
16248
  mediaPath = resolveMediumMedia(medium, post.platform);
16118
16249
  thumbnailPath = medium.thumbnailPath ?? null;
16250
+ clipIdeaIssueNumber = medium.ideaIssueNumber;
16119
16251
  } else {
16120
16252
  clipSlug = frontmatter.shortSlug;
16121
16253
  clipType = "short";
@@ -16172,7 +16304,7 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
16172
16304
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16173
16305
  reviewedAt: null,
16174
16306
  publishedAt: null,
16175
- ideaIds: ideaIds && ideaIds.length > 0 ? ideaIds : void 0,
16307
+ ideaIds: clipIdeaIssueNumber ? [String(clipIdeaIssueNumber)] : ideaIds && ideaIds.length > 0 ? ideaIds : void 0,
16176
16308
  thumbnailPath
16177
16309
  };
16178
16310
  const stripped = stripFrontmatter(post.content);
@@ -16232,13 +16364,393 @@ function buildPublishQueue2(...args) {
16232
16364
  return buildPublishQueue(...args);
16233
16365
  }
16234
16366
 
16367
+ // src/L4-agents/IdeaDiscoveryAgent.ts
16368
+ init_environment();
16369
+ init_configLogger();
16370
+ init_ideaService();
16371
+ init_BaseAgent();
16372
+ var SYSTEM_PROMPT6 = `You are an Idea Discovery agent for a video content pipeline. Your job is to analyze video clips and match them to existing content ideas \u2014 or create new ideas when no match exists.
16373
+
16374
+ ## Two-Phase Workflow
16375
+
16376
+ ### Phase A \u2014 Match Existing Ideas
16377
+ For each clip (short and medium), determine if it covers a topic that DIRECTLY matches an existing idea.
16378
+ - A match requires the clip's core topic to be the SAME topic as the idea's topic and talking points.
16379
+ - Loose thematic connections are NOT matches (e.g., both about "coding" is too vague).
16380
+ - When in doubt, DO NOT match \u2014 create a new idea instead.
16381
+
16382
+ ### Phase B \u2014 Create New Ideas
16383
+ For unmatched clips, create a new idea derived from the video content:
16384
+ - The topic, hook, key takeaway, and talking points come from what the creator ACTUALLY SAID in the transcript.
16385
+ - Do NOT invent content the creator didn't discuss.
16386
+ - Use web research (if available) to augment with trend context \u2014 why this topic matters right now, related articles, supporting data.
16387
+ - Set the publishBy date to the configured deadline.
16388
+
16389
+ ## Quality Rules
16390
+ - Every clip MUST end up with an idea assignment (either matched or newly created).
16391
+ - Each new idea must have a clear, specific hook (not generic like "Learn about AI").
16392
+ - New idea talking points must come from the actual transcript content.
16393
+ - Use get_clip_transcript to read what the creator said in each clip's time range before matching or creating.
16394
+ - Call finalize_assignments exactly once when all clips are assigned.
16395
+
16396
+ ## Platform Targeting
16397
+ - Short-form clips (TikTok, YouTube Shorts, Instagram Reels): Hook-first, single concept
16398
+ - Medium clips (YouTube, LinkedIn): Deep dives, tutorials, story arcs`;
16399
+ function computeDuration(segments) {
16400
+ return segments.reduce((sum, s) => sum + (s.end - s.start), 0);
16401
+ }
16402
+ function clipsToInfo(shorts, mediumClips) {
16403
+ const clips = [];
16404
+ for (let i = 0; i < shorts.length; i++) {
16405
+ const short = shorts[i];
16406
+ const segments = (short.segments ?? []).map((s) => ({ start: s.start, end: s.end, description: s.description ?? "" }));
16407
+ clips.push({
16408
+ id: short.id ?? `short-${i + 1}`,
16409
+ type: "short",
16410
+ title: short.title ?? "",
16411
+ description: short.description ?? "",
16412
+ tags: short.tags ?? [],
16413
+ segments,
16414
+ totalDuration: short.totalDuration ?? computeDuration(segments)
16415
+ });
16416
+ }
16417
+ for (let i = 0; i < mediumClips.length; i++) {
16418
+ const medium = mediumClips[i];
16419
+ const segments = (medium.segments ?? []).map((s) => ({ start: s.start, end: s.end, description: s.description ?? "" }));
16420
+ clips.push({
16421
+ id: medium.id ?? `medium-${i + 1}`,
16422
+ type: "medium-clip",
16423
+ title: medium.title ?? "",
16424
+ description: medium.description ?? "",
16425
+ tags: medium.tags ?? [],
16426
+ topic: medium.topic,
16427
+ segments,
16428
+ totalDuration: medium.totalDuration ?? computeDuration(segments)
16429
+ });
16430
+ }
16431
+ return clips;
16432
+ }
16433
+ function getTranscriptForTimeRange(transcript, start, end) {
16434
+ return transcript.filter((seg) => seg.end > start && seg.start < end).map((seg) => seg.text).join(" ").trim();
16435
+ }
16436
+ function summarizeIdeas(ideas) {
16437
+ if (ideas.length === 0) return "No existing ideas found.";
16438
+ return ideas.map((idea) => [
16439
+ `- #${idea.issueNumber}: "${idea.topic}"`,
16440
+ ` Hook: ${idea.hook}`,
16441
+ ` Tags: ${idea.tags.join(", ")}`,
16442
+ ` Talking points: ${idea.talkingPoints.join("; ")}`,
16443
+ ` Status: ${idea.status}`
16444
+ ].join("\n")).join("\n");
16445
+ }
16446
+ function summarizeClips(clips) {
16447
+ return clips.map((clip) => [
16448
+ `- ${clip.id} (${clip.type}, ${clip.totalDuration.toFixed(0)}s): "${clip.title}"`,
16449
+ ` Description: ${clip.description}`,
16450
+ ` Tags: ${clip.tags.join(", ")}`,
16451
+ clip.topic ? ` Topic: ${clip.topic}` : "",
16452
+ ` Segments: ${clip.segments.map((s) => `${s.start.toFixed(1)}-${s.end.toFixed(1)}s`).join(", ")}`
16453
+ ].filter(Boolean).join("\n")).join("\n");
16454
+ }
16455
+ function buildUserMessage(clips, ideas, summary, publishBy, hasMcpServers) {
16456
+ const sections = [
16457
+ `## Video Summary
16458
+ ${summary.substring(0, 2e3)}`,
16459
+ `
16460
+ ## Clips to Assign (${clips.length} total)
16461
+ ${summarizeClips(clips)}`,
16462
+ `
16463
+ ## Existing Ideas (${ideas.length} total)
16464
+ ${summarizeIdeas(ideas)}`,
16465
+ `
16466
+ ## Default publishBy for new ideas: ${publishBy}`
16467
+ ];
16468
+ const steps = [
16469
+ "\n## Your Steps:",
16470
+ "1. For each clip, call get_clip_transcript to read what the creator said.",
16471
+ "2. Compare each clip's content against the existing ideas above.",
16472
+ "3. For strong matches, call assign_idea_to_clip with the existing idea's issue number."
16473
+ ];
16474
+ if (hasMcpServers) {
16475
+ steps.push(
16476
+ "4. For unmatched clips, use web search tools to research trending context for the topic.",
16477
+ "5. Call create_idea_for_clip with the topic derived from transcript + trend context from research."
16478
+ );
16479
+ } else {
16480
+ steps.push(
16481
+ "4. For unmatched clips, call create_idea_for_clip with the topic derived from the transcript."
16482
+ );
16483
+ }
16484
+ steps.push(
16485
+ `${hasMcpServers ? "6" : "5"}. Once ALL clips have assignments, call finalize_assignments.`
16486
+ );
16487
+ sections.push(steps.join("\n"));
16488
+ return sections.join("\n");
16489
+ }
16490
+ var IdeaDiscoveryAgent = class extends BaseAgent {
16491
+ constructor(input) {
16492
+ super("IdeaDiscoveryAgent", SYSTEM_PROMPT6);
16493
+ this.input = input;
16494
+ this.clips = clipsToInfo(input.shorts, input.mediumClips);
16495
+ this.transcript = input.transcript;
16496
+ this.publishBy = input.publishBy;
16497
+ this.defaultPlatforms = input.defaultPlatforms;
16498
+ }
16499
+ assignments = [];
16500
+ newIdeas = [];
16501
+ finalized = false;
16502
+ clips;
16503
+ transcript;
16504
+ publishBy;
16505
+ defaultPlatforms;
16506
+ getTimeoutMs() {
16507
+ return 0;
16508
+ }
16509
+ async discover() {
16510
+ if (this.clips.length === 0) {
16511
+ return { assignments: [], newIdeas: [], matchedCount: 0, createdCount: 0 };
16512
+ }
16513
+ const allIdeas = await this.loadIdeas();
16514
+ const hasMcp = this.getMcpServers() !== void 0;
16515
+ const userMessage = buildUserMessage(
16516
+ this.clips,
16517
+ allIdeas,
16518
+ this.input.summary,
16519
+ this.publishBy,
16520
+ hasMcp
16521
+ );
16522
+ await this.run(userMessage);
16523
+ return {
16524
+ assignments: [...this.assignments],
16525
+ newIdeas: [...this.newIdeas],
16526
+ matchedCount: this.assignments.filter(
16527
+ (a) => !this.newIdeas.some((idea) => idea.issueNumber === a.ideaIssueNumber)
16528
+ ).length,
16529
+ createdCount: this.newIdeas.length
16530
+ };
16531
+ }
16532
+ async loadIdeas() {
16533
+ if (this.input.providedIdeas && this.input.providedIdeas.length > 0) {
16534
+ return [...this.input.providedIdeas];
16535
+ }
16536
+ try {
16537
+ const readyIdeas = await listIdeas({ status: "ready" });
16538
+ const draftIdeas = await listIdeas({ status: "draft" });
16539
+ return [...readyIdeas, ...draftIdeas];
16540
+ } catch (err) {
16541
+ logger_default.warn(`[IdeaDiscoveryAgent] Failed to fetch ideas: ${err instanceof Error ? err.message : String(err)}`);
16542
+ return this.input.existingIdeas ? [...this.input.existingIdeas] : [];
16543
+ }
16544
+ }
16545
+ resetForRetry() {
16546
+ this.assignments = [];
16547
+ this.newIdeas = [];
16548
+ this.finalized = false;
16549
+ }
16550
+ getMcpServers() {
16551
+ const config2 = getConfig();
16552
+ const servers = {};
16553
+ if (config2.EXA_API_KEY) {
16554
+ servers.exa = {
16555
+ type: "http",
16556
+ url: `${config2.EXA_MCP_URL}?exaApiKey=${config2.EXA_API_KEY}&tools=web_search_exa`,
16557
+ headers: {},
16558
+ tools: ["*"]
16559
+ };
16560
+ }
16561
+ if (config2.PERPLEXITY_API_KEY) {
16562
+ servers.perplexity = {
16563
+ type: "local",
16564
+ command: "npx",
16565
+ args: ["-y", "perplexity-mcp"],
16566
+ env: { PERPLEXITY_API_KEY: config2.PERPLEXITY_API_KEY },
16567
+ tools: ["*"]
16568
+ };
16569
+ }
16570
+ return Object.keys(servers).length > 0 ? servers : void 0;
16571
+ }
16572
+ getTools() {
16573
+ return [
16574
+ {
16575
+ name: "get_clip_transcript",
16576
+ description: "Get the transcript text for a specific clip by its ID. Returns the text spoken in that time range.",
16577
+ parameters: {
16578
+ type: "object",
16579
+ properties: {
16580
+ clipId: { type: "string", description: 'The clip ID (e.g., "short-1" or "medium-1")' }
16581
+ },
16582
+ required: ["clipId"]
16583
+ },
16584
+ handler: async (args) => this.handleToolCall("get_clip_transcript", args)
16585
+ },
16586
+ {
16587
+ name: "assign_idea_to_clip",
16588
+ description: "Assign an existing idea to a clip. Only use when there is a STRONG topical match between the clip content and the idea.",
16589
+ parameters: {
16590
+ type: "object",
16591
+ properties: {
16592
+ clipId: { type: "string", description: "The clip ID to assign" },
16593
+ ideaIssueNumber: { type: "number", description: "The GitHub Issue number of the matching idea" },
16594
+ reason: { type: "string", description: "Brief explanation of why this is a strong match" }
16595
+ },
16596
+ required: ["clipId", "ideaIssueNumber", "reason"]
16597
+ },
16598
+ handler: async (args) => this.handleToolCall("assign_idea_to_clip", args)
16599
+ },
16600
+ {
16601
+ name: "create_idea_for_clip",
16602
+ description: "Create a new idea from the clip content and assign it. Use when no existing idea matches the clip topic.",
16603
+ parameters: {
16604
+ type: "object",
16605
+ properties: {
16606
+ clipId: { type: "string", description: "The clip ID to create an idea for" },
16607
+ topic: { type: "string", description: "Main topic (derived from transcript content)" },
16608
+ hook: { type: "string", description: "Attention-grabbing angle (\u226480 chars, from what the creator actually said)" },
16609
+ audience: { type: "string", description: "Target audience for this content" },
16610
+ keyTakeaway: { type: "string", description: "The one thing viewers should remember (from transcript)" },
16611
+ talkingPoints: {
16612
+ type: "array",
16613
+ items: { type: "string" },
16614
+ description: "Key points covered (from actual transcript content)"
16615
+ },
16616
+ tags: {
16617
+ type: "array",
16618
+ items: { type: "string" },
16619
+ description: "Categorization tags (lowercase)"
16620
+ },
16621
+ trendContext: {
16622
+ type: "string",
16623
+ description: "Optional: why this topic is timely NOW (from web research if available)"
16624
+ }
16625
+ },
16626
+ required: ["clipId", "topic", "hook", "audience", "keyTakeaway", "talkingPoints", "tags"]
16627
+ },
16628
+ handler: async (args) => this.handleToolCall("create_idea_for_clip", args)
16629
+ },
16630
+ {
16631
+ name: "finalize_assignments",
16632
+ description: "Confirm all clip-to-idea assignments are complete. Call exactly once when every clip has been assigned.",
16633
+ parameters: {
16634
+ type: "object",
16635
+ properties: {
16636
+ summary: { type: "string", description: "Brief summary of assignments made" }
16637
+ },
16638
+ required: ["summary"]
16639
+ },
16640
+ handler: async (args) => this.handleToolCall("finalize_assignments", args)
16641
+ }
16642
+ ];
16643
+ }
16644
+ async handleToolCall(toolName, args) {
16645
+ switch (toolName) {
16646
+ case "get_clip_transcript":
16647
+ return this.handleGetClipTranscript(args);
16648
+ case "assign_idea_to_clip":
16649
+ return this.handleAssignIdea(args);
16650
+ case "create_idea_for_clip":
16651
+ return this.handleCreateIdeaForClip(args);
16652
+ case "finalize_assignments":
16653
+ return this.handleFinalize(args);
16654
+ default:
16655
+ throw new Error(`Unknown tool: ${toolName}`);
16656
+ }
16657
+ }
16658
+ handleGetClipTranscript(args) {
16659
+ const clipId = String(args.clipId ?? "");
16660
+ const clip = this.clips.find((c) => c.id === clipId);
16661
+ if (!clip) {
16662
+ throw new Error(`Clip not found: ${clipId}. Available: ${this.clips.map((c) => c.id).join(", ")}`);
16663
+ }
16664
+ const texts = [];
16665
+ for (const seg of clip.segments) {
16666
+ const text = getTranscriptForTimeRange(this.transcript, seg.start, seg.end);
16667
+ if (text) texts.push(text);
16668
+ }
16669
+ return { clipId, transcript: texts.join("\n\n") || "(No transcript found for this time range)" };
16670
+ }
16671
+ handleAssignIdea(args) {
16672
+ const clipId = String(args.clipId ?? "");
16673
+ const ideaIssueNumber = Number(args.ideaIssueNumber);
16674
+ const reason = String(args.reason ?? "");
16675
+ if (!clipId || !this.clips.find((c) => c.id === clipId)) {
16676
+ throw new Error(`Invalid clipId: ${clipId}`);
16677
+ }
16678
+ if (!Number.isInteger(ideaIssueNumber) || ideaIssueNumber <= 0) {
16679
+ throw new Error(`Invalid ideaIssueNumber: ${ideaIssueNumber}`);
16680
+ }
16681
+ if (this.assignments.some((a) => a.clipId === clipId)) {
16682
+ throw new Error(`Clip ${clipId} already has an assignment`);
16683
+ }
16684
+ this.assignments.push({ clipId, ideaIssueNumber });
16685
+ logger_default.info(`[IdeaDiscoveryAgent] Matched ${clipId} \u2192 idea #${ideaIssueNumber}: ${reason}`);
16686
+ return { clipId, ideaIssueNumber, status: "assigned" };
16687
+ }
16688
+ async handleCreateIdeaForClip(args) {
16689
+ const clipId = String(args.clipId ?? "");
16690
+ if (!clipId || !this.clips.find((c) => c.id === clipId)) {
16691
+ throw new Error(`Invalid clipId: ${clipId}`);
16692
+ }
16693
+ if (this.assignments.some((a) => a.clipId === clipId)) {
16694
+ throw new Error(`Clip ${clipId} already has an assignment`);
16695
+ }
16696
+ const hook = String(args.hook ?? "").trim();
16697
+ if (hook.length > 80) {
16698
+ throw new Error(`Hook must be 80 characters or fewer: ${hook}`);
16699
+ }
16700
+ const talkingPoints = Array.isArray(args.talkingPoints) ? args.talkingPoints.map((tp) => String(tp).trim()).filter((tp) => tp.length > 0) : [];
16701
+ if (talkingPoints.length === 0) {
16702
+ throw new Error("talkingPoints must be a non-empty array of strings");
16703
+ }
16704
+ const tags = Array.isArray(args.tags) ? args.tags.map((t) => String(t).trim().toLowerCase()).filter((t) => t.length > 0) : [];
16705
+ const input = {
16706
+ topic: String(args.topic ?? "").trim(),
16707
+ hook,
16708
+ audience: String(args.audience ?? "").trim(),
16709
+ keyTakeaway: String(args.keyTakeaway ?? "").trim(),
16710
+ talkingPoints,
16711
+ platforms: [...this.defaultPlatforms],
16712
+ tags,
16713
+ publishBy: this.publishBy,
16714
+ trendContext: typeof args.trendContext === "string" ? args.trendContext.trim() || void 0 : void 0
16715
+ };
16716
+ const idea = await createIdea(input);
16717
+ this.newIdeas.push(idea);
16718
+ this.assignments.push({ clipId, ideaIssueNumber: idea.issueNumber });
16719
+ logger_default.info(`[IdeaDiscoveryAgent] Created idea #${idea.issueNumber} ("${idea.topic}") for ${clipId}`);
16720
+ return { clipId, ideaIssueNumber: idea.issueNumber, status: "created" };
16721
+ }
16722
+ handleFinalize(args) {
16723
+ this.finalized = true;
16724
+ const summary = String(args.summary ?? "");
16725
+ const assignedIds = new Set(this.assignments.map((a) => a.clipId));
16726
+ const unassigned = this.clips.filter((c) => !assignedIds.has(c.id)).map((c) => c.id);
16727
+ const matchedCount = this.assignments.filter(
16728
+ (a) => !this.newIdeas.some((idea) => idea.issueNumber === a.ideaIssueNumber)
16729
+ ).length;
16730
+ logger_default.info(`[IdeaDiscoveryAgent] Finalized: ${this.assignments.length}/${this.clips.length} clips assigned (${matchedCount} matched, ${this.newIdeas.length} created). ${summary}`);
16731
+ if (unassigned.length > 0) {
16732
+ logger_default.warn(`[IdeaDiscoveryAgent] ${unassigned.length} clips unassigned: ${unassigned.join(", ")}`);
16733
+ }
16734
+ return {
16735
+ totalClips: this.clips.length,
16736
+ assigned: this.assignments.length,
16737
+ matched: matchedCount,
16738
+ created: this.newIdeas.length,
16739
+ unassigned
16740
+ };
16741
+ }
16742
+ async destroy() {
16743
+ await super.destroy();
16744
+ }
16745
+ };
16746
+
16235
16747
  // src/L4-agents/GraphicsAgent.ts
16236
16748
  init_BaseAgent();
16237
16749
  init_paths();
16238
16750
  init_fileSystem();
16239
16751
  init_configLogger();
16240
16752
  import sharp from "sharp";
16241
- var SYSTEM_PROMPT6 = `You are a visual content designer and editorial director for educational video content. You are given an editorial report from a video analyst describing moments in a video where AI-generated image overlays could enhance viewer comprehension.
16753
+ var SYSTEM_PROMPT7 = `You are a visual content designer and editorial director for educational video content. You are given an editorial report from a video analyst describing moments in a video where AI-generated image overlays could enhance viewer comprehension.
16242
16754
 
16243
16755
  Your job is to make the FINAL editorial decision for each opportunity:
16244
16756
  1. Decide whether to generate an image or skip the opportunity
@@ -16321,7 +16833,7 @@ var GraphicsAgent = class extends BaseAgent {
16321
16833
  enhancementsDir = "";
16322
16834
  imageIndex = 0;
16323
16835
  constructor(model) {
16324
- super("GraphicsAgent", SYSTEM_PROMPT6, void 0, model);
16836
+ super("GraphicsAgent", SYSTEM_PROMPT7, void 0, model);
16325
16837
  }
16326
16838
  resetForRetry() {
16327
16839
  this.overlays = [];
@@ -16486,6 +16998,8 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
16486
16998
  slug;
16487
16999
  /** Content ideas linked to this video for editorial direction */
16488
17000
  _ideas = [];
17001
+ /** Per-clip idea assignments from idea discovery (clipId → ideaIssueNumber) */
17002
+ _clipIdeaMap = /* @__PURE__ */ new Map();
16489
17003
  /** Set ideas for editorial direction */
16490
17004
  setIdeas(ideas) {
16491
17005
  this._ideas = ideas;
@@ -16494,6 +17008,10 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
16494
17008
  get ideas() {
16495
17009
  return this._ideas;
16496
17010
  }
17011
+ /** Get per-clip idea assignments */
17012
+ get clipIdeaMap() {
17013
+ return this._clipIdeaMap;
17014
+ }
16497
17015
  constructor(sourcePath, videoDir, slug) {
16498
17016
  super();
16499
17017
  this.sourcePath = sourcePath;
@@ -16654,12 +17172,12 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
16654
17172
  await transcodeToMp42(sourcePath, destPath);
16655
17173
  logger_default.info(`Transcoded video to ${destPath}`);
16656
17174
  } else {
16657
- await new Promise((resolve3, reject) => {
17175
+ await new Promise((resolve4, reject) => {
16658
17176
  const readStream = openReadStream(sourcePath);
16659
17177
  const writeStream = openWriteStream(destPath);
16660
17178
  readStream.on("error", reject);
16661
17179
  writeStream.on("error", reject);
16662
- writeStream.on("finish", resolve3);
17180
+ writeStream.on("finish", resolve4);
16663
17181
  readStream.pipe(writeStream);
16664
17182
  });
16665
17183
  logger_default.info(`Copied video to ${destPath}`);
@@ -17413,6 +17931,47 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
17413
17931
  }
17414
17932
  return posts;
17415
17933
  }
17934
+ /**
17935
+ * Run idea discovery: match clips to existing ideas, create new ones for unmatched.
17936
+ * Updates clip objects with ideaIssueNumber and adds newly created ideas to _ideas.
17937
+ */
17938
+ async discoverIdeas(shorts, mediumClips, publishBy) {
17939
+ const transcript = await this.getTranscript();
17940
+ const summary = await this.getSummary();
17941
+ const brand = (await Promise.resolve().then(() => (init_brand(), brand_exports))).getBrandConfig();
17942
+ const defaultPlatforms = brand.hashtags?.platforms ? Object.keys(brand.hashtags.platforms).map((p) => p) : ["youtube" /* YouTube */, "tiktok" /* TikTok */, "instagram" /* Instagram */, "linkedin" /* LinkedIn */, "x" /* X */];
17943
+ const agent = new IdeaDiscoveryAgent({
17944
+ shorts,
17945
+ mediumClips,
17946
+ transcript: transcript.segments,
17947
+ summary: typeof summary === "string" ? summary : summary?.overview ?? "",
17948
+ providedIdeas: this._ideas.length > 0 ? this._ideas : void 0,
17949
+ publishBy,
17950
+ defaultPlatforms
17951
+ });
17952
+ const result = await agent.discover();
17953
+ for (const assignment of result.assignments) {
17954
+ this._clipIdeaMap.set(assignment.clipId, assignment.ideaIssueNumber);
17955
+ const short = shorts.find((s) => s.id === assignment.clipId);
17956
+ if (short) {
17957
+ short.ideaIssueNumber = assignment.ideaIssueNumber;
17958
+ continue;
17959
+ }
17960
+ const medium = mediumClips.find((m) => m.id === assignment.clipId);
17961
+ if (medium) {
17962
+ medium.ideaIssueNumber = assignment.ideaIssueNumber;
17963
+ }
17964
+ }
17965
+ if (result.newIdeas.length > 0) {
17966
+ const existingIds = new Set(this._ideas.map((i) => i.issueNumber));
17967
+ for (const idea of result.newIdeas) {
17968
+ if (!existingIds.has(idea.issueNumber)) {
17969
+ this._ideas.push(idea);
17970
+ }
17971
+ }
17972
+ }
17973
+ return result;
17974
+ }
17416
17975
  /**
17417
17976
  * Build the publish queue via the queue builder service.
17418
17977
  */
@@ -17525,33 +18084,20 @@ async function buildRealignPlan(options = {}) {
17525
18084
  tagged.push({ post, platform, clipType });
17526
18085
  }
17527
18086
  const bookedMap = await buildBookedMap();
17528
- const ctx = {
17529
- timezone,
17530
- bookedMap,
17531
- ideaLinkedPostIds: /* @__PURE__ */ new Set(),
17532
- lateClient: client,
17533
- displacementEnabled: getDisplacementConfig().enabled,
17534
- dryRun: true,
17535
- depth: 0,
17536
- ideaRefs: [],
17537
- samePlatformMs: 0,
17538
- crossPlatformMs: 0,
17539
- platform: ""
17540
- };
18087
+ const ideaLinkedPostIds = /* @__PURE__ */ new Set();
17541
18088
  for (const [, slot] of bookedMap) {
17542
18089
  if (slot.ideaLinked && slot.postId) {
17543
- ctx.ideaLinkedPostIds.add(slot.postId);
18090
+ ideaLinkedPostIds.add(slot.postId);
17544
18091
  }
17545
18092
  }
17546
18093
  const result = [];
17547
18094
  const toCancel = [];
17548
18095
  let skipped = 0;
17549
18096
  tagged.sort((a, b) => {
17550
- const aIdea = ctx.ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
17551
- const bIdea = ctx.ideaLinkedPostIds.has(b.post._id) ? 0 : 1;
18097
+ const aIdea = ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
18098
+ const bIdea = ideaLinkedPostIds.has(b.post._id) ? 0 : 1;
17552
18099
  return aIdea - bIdea;
17553
18100
  });
17554
- const nowMs = Date.now();
17555
18101
  for (const { post, platform, clipType } of tagged) {
17556
18102
  const schedulePlatform = normalizeSchedulePlatform(platform);
17557
18103
  const platformConfig = getPlatformSchedule(schedulePlatform, clipType);
@@ -17572,9 +18118,13 @@ async function buildRealignPlan(options = {}) {
17572
18118
  bookedMap.delete(currentMs2);
17573
18119
  }
17574
18120
  }
17575
- const isIdea = ctx.ideaLinkedPostIds.has(post._id);
17576
- const label = `${schedulePlatform}/${clipType}:${post._id.slice(-6)}`;
17577
- const newSlot = await schedulePost(platformConfig, nowMs, isIdea, label, ctx);
18121
+ const isIdea = ideaLinkedPostIds.has(post._id);
18122
+ const newSlot = await schedulePost(schedulePlatform, clipType, {
18123
+ postId: post._id,
18124
+ ideaIds: isIdea ? bookedMap.get(new Date(post.scheduledFor ?? 0).getTime())?.ideaIds ?? [] : void 0,
18125
+ dryRun: true,
18126
+ _bookedMap: bookedMap
18127
+ });
17578
18128
  if (!newSlot) {
17579
18129
  if (post.status !== "cancelled") {
17580
18130
  toCancel.push({ post, platform, clipType, reason: `No available slot for ${schedulePlatform}/${clipType}` });
@@ -17582,7 +18132,7 @@ async function buildRealignPlan(options = {}) {
17582
18132
  continue;
17583
18133
  }
17584
18134
  const newMs = new Date(newSlot).getTime();
17585
- ctx.bookedMap.set(newMs, {
18135
+ bookedMap.set(newMs, {
17586
18136
  scheduledFor: newSlot,
17587
18137
  source: "late",
17588
18138
  postId: post._id,
@@ -17662,7 +18212,7 @@ var TOOL_LABELS = {
17662
18212
  check_realign_status: "\u{1F4CA} Checking realignment progress",
17663
18213
  ask_user: "\u{1F4AC} Asking for your input"
17664
18214
  };
17665
- var SYSTEM_PROMPT7 = `You are a schedule management assistant for Late.co social media posts.
18215
+ var SYSTEM_PROMPT8 = `You are a schedule management assistant for Late.co social media posts.
17666
18216
 
17667
18217
  You help the user view, analyze, and reprioritize their posting schedule across platforms.
17668
18218
 
@@ -17685,7 +18235,7 @@ var ScheduleAgent = class extends BaseAgent {
17685
18235
  chatOutput;
17686
18236
  realignJobs = /* @__PURE__ */ new Map();
17687
18237
  constructor(userInputHandler, model) {
17688
- super("ScheduleAgent", SYSTEM_PROMPT7, void 0, model);
18238
+ super("ScheduleAgent", SYSTEM_PROMPT8, void 0, model);
17689
18239
  this.userInputHandler = userInputHandler;
17690
18240
  }
17691
18241
  /** Set a callback for chat-friendly status messages (tool starts, progress). */
@@ -18286,7 +18836,7 @@ function buildSystemPrompt4(brand, existingIdeas, seedTopics, count, ideaRepo) {
18286
18836
  }
18287
18837
  return promptSections.join("\n");
18288
18838
  }
18289
- function buildUserMessage(count, seedTopics, hasMcpServers, userPrompt) {
18839
+ function buildUserMessage2(count, seedTopics, hasMcpServers, userPrompt) {
18290
18840
  const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
18291
18841
  const steps = [
18292
18842
  "1. Call get_brand_context to load the creator profile.",
@@ -18835,7 +19385,7 @@ async function generateIdeas(options = {}) {
18835
19385
  });
18836
19386
  try {
18837
19387
  const hasMcpServers = !!(config2.EXA_API_KEY || config2.YOUTUBE_API_KEY || config2.PERPLEXITY_API_KEY);
18838
- const userMessage = buildUserMessage(count, seedTopics, hasMcpServers, options.prompt);
19388
+ const userMessage = buildUserMessage2(count, seedTopics, hasMcpServers, options.prompt);
18839
19389
  await agent.run(userMessage);
18840
19390
  const ideas = agent.getGeneratedIdeas();
18841
19391
  if (!agent.isFinalized()) {
@@ -18894,7 +19444,7 @@ var interviewEmitter = new InterviewEmitter();
18894
19444
 
18895
19445
  // src/L4-agents/InterviewAgent.ts
18896
19446
  init_configLogger();
18897
- var SYSTEM_PROMPT8 = `You are a Socratic interview coach helping a content creator sharpen their video idea. Ask ONE short question at a time (1 sentence max).
19447
+ var SYSTEM_PROMPT9 = `You are a Socratic interview coach helping a content creator sharpen their video idea. Ask ONE short question at a time (1 sentence max).
18898
19448
 
18899
19449
  ## Rules
18900
19450
  - Every question must be a SINGLE sentence. No multi-part questions. No preamble. No encouragement filler.
@@ -18923,7 +19473,7 @@ var InterviewAgent = class extends BaseAgent {
18923
19473
  ended = false;
18924
19474
  idea = null;
18925
19475
  constructor(model) {
18926
- super("InterviewAgent", SYSTEM_PROMPT8, void 0, model);
19476
+ super("InterviewAgent", SYSTEM_PROMPT9, void 0, model);
18927
19477
  }
18928
19478
  getTimeoutMs() {
18929
19479
  return 0;
@@ -19194,6 +19744,296 @@ var InterviewAgent = class extends BaseAgent {
19194
19744
  }
19195
19745
  };
19196
19746
 
19747
+ // src/L4-agents/AgendaAgent.ts
19748
+ init_brand();
19749
+ init_configLogger();
19750
+ init_BaseAgent();
19751
+ var SYSTEM_PROMPT10 = `You are a recording agenda planner for a content creator. You take multiple video ideas and structure them into a single cohesive recording agenda with natural transitions, time estimates, and recording notes.
19752
+
19753
+ ## Rules
19754
+ - You MUST use tools for ALL output. Never respond with plain text.
19755
+ - Structure ideas into a logical recording flow that feels natural, not disjointed.
19756
+ - Estimate ~2 minutes per talking point as a baseline, then adjust for complexity.
19757
+ - Write an engaging intro hook that previews what the video will cover.
19758
+ - Write a CTA outro (subscribe, like, follow).
19759
+ - Include recording notes for each section: energy cues, visual references, key phrases to emphasize.
19760
+ - Transitions between sections should feel conversational, not abrupt.
19761
+
19762
+ ## Process
19763
+ 1. Call get_brand_context to load the creator's voice, style, and content pillars.
19764
+ 2. Call get_idea_details for each idea to inspect the full content.
19765
+ 3. Call add_section for each idea in the order you want them recorded. Set the order field sequentially starting at 1.
19766
+ 4. Call set_intro with an engaging opening hook.
19767
+ 5. Call set_outro with a closing CTA.
19768
+ 6. Call finalize_agenda with a brief summary of the agenda.
19769
+
19770
+ ## Ordering Strategy
19771
+ - Lead with the most attention-grabbing idea (strongest hook).
19772
+ - Group related topics so transitions feel natural.
19773
+ - End with the most forward-looking or actionable topic (leaves viewers motivated).
19774
+ - Alternate energy levels: high-energy topic \u2192 reflective topic \u2192 high-energy.`;
19775
+ var AgendaAgent = class extends BaseAgent {
19776
+ ideas = [];
19777
+ sections = [];
19778
+ intro = "";
19779
+ outro = "";
19780
+ finalized = false;
19781
+ constructor(ideas, model) {
19782
+ super("AgendaAgent", SYSTEM_PROMPT10, void 0, model);
19783
+ this.ideas = ideas;
19784
+ }
19785
+ resetForRetry() {
19786
+ this.sections = [];
19787
+ this.intro = "";
19788
+ this.outro = "";
19789
+ this.finalized = false;
19790
+ }
19791
+ getTools() {
19792
+ return [
19793
+ {
19794
+ name: "get_brand_context",
19795
+ description: "Return the creator brand context including voice, style, and content pillars.",
19796
+ parameters: {
19797
+ type: "object",
19798
+ properties: {}
19799
+ },
19800
+ handler: async (args) => this.handleToolCall("get_brand_context", args)
19801
+ },
19802
+ {
19803
+ name: "get_idea_details",
19804
+ description: "Return the full details of an idea by its index in the provided array.",
19805
+ parameters: {
19806
+ type: "object",
19807
+ properties: {
19808
+ ideaIndex: {
19809
+ type: "number",
19810
+ description: "Zero-based index of the idea in the provided array."
19811
+ }
19812
+ },
19813
+ required: ["ideaIndex"],
19814
+ additionalProperties: false
19815
+ },
19816
+ handler: async (args) => this.handleToolCall("get_idea_details", args)
19817
+ },
19818
+ {
19819
+ name: "add_section",
19820
+ description: "Add a recording section to the agenda. Call once per idea, in the order they should be recorded.",
19821
+ parameters: {
19822
+ type: "object",
19823
+ properties: {
19824
+ title: {
19825
+ type: "string",
19826
+ description: "Section title for the recording outline."
19827
+ },
19828
+ ideaIssueNumber: {
19829
+ type: "number",
19830
+ description: "GitHub Issue number of the idea this section covers."
19831
+ },
19832
+ estimatedMinutes: {
19833
+ type: "number",
19834
+ description: "Estimated recording time in minutes for this section."
19835
+ },
19836
+ talkingPoints: {
19837
+ type: "array",
19838
+ items: { type: "string" },
19839
+ description: "Talking points to cover in this section."
19840
+ },
19841
+ transition: {
19842
+ type: "string",
19843
+ description: "Transition phrase to lead into the next section. Empty string for the last section."
19844
+ },
19845
+ notes: {
19846
+ type: "string",
19847
+ description: "Recording notes: energy cues, visual references, key phrases to emphasize."
19848
+ }
19849
+ },
19850
+ required: ["title", "ideaIssueNumber", "estimatedMinutes", "talkingPoints", "transition", "notes"],
19851
+ additionalProperties: false
19852
+ },
19853
+ handler: async (args) => this.handleToolCall("add_section", args)
19854
+ },
19855
+ {
19856
+ name: "set_intro",
19857
+ description: "Set the opening hook text for the recording.",
19858
+ parameters: {
19859
+ type: "object",
19860
+ properties: {
19861
+ text: {
19862
+ type: "string",
19863
+ description: "Opening hook text that previews what the video will cover."
19864
+ }
19865
+ },
19866
+ required: ["text"],
19867
+ additionalProperties: false
19868
+ },
19869
+ handler: async (args) => this.handleToolCall("set_intro", args)
19870
+ },
19871
+ {
19872
+ name: "set_outro",
19873
+ description: "Set the closing CTA text for the recording.",
19874
+ parameters: {
19875
+ type: "object",
19876
+ properties: {
19877
+ text: {
19878
+ type: "string",
19879
+ description: "Closing call-to-action text (subscribe, like, follow)."
19880
+ }
19881
+ },
19882
+ required: ["text"],
19883
+ additionalProperties: false
19884
+ },
19885
+ handler: async (args) => this.handleToolCall("set_outro", args)
19886
+ },
19887
+ {
19888
+ name: "finalize_agenda",
19889
+ description: "Confirm that the agenda is complete. Call this after all sections, intro, and outro are set.",
19890
+ parameters: {
19891
+ type: "object",
19892
+ properties: {
19893
+ summary: {
19894
+ type: "string",
19895
+ description: "Brief summary of the agenda structure and reasoning."
19896
+ }
19897
+ },
19898
+ required: ["summary"],
19899
+ additionalProperties: false
19900
+ },
19901
+ handler: async (args) => this.handleToolCall("finalize_agenda", args)
19902
+ }
19903
+ ];
19904
+ }
19905
+ async handleToolCall(toolName, args) {
19906
+ switch (toolName) {
19907
+ case "get_brand_context":
19908
+ return getBrandConfig();
19909
+ case "get_idea_details":
19910
+ return this.handleGetIdeaDetails(args);
19911
+ case "add_section":
19912
+ return this.handleAddSection(args);
19913
+ case "set_intro":
19914
+ return this.handleSetIntro(args);
19915
+ case "set_outro":
19916
+ return this.handleSetOutro(args);
19917
+ case "finalize_agenda":
19918
+ return this.handleFinalizeAgenda(args);
19919
+ default:
19920
+ return { error: `Unknown tool: ${toolName}` };
19921
+ }
19922
+ }
19923
+ handleGetIdeaDetails(args) {
19924
+ const ideaIndex = Number(args.ideaIndex ?? -1);
19925
+ if (ideaIndex < 0 || ideaIndex >= this.ideas.length) {
19926
+ return { error: `Invalid ideaIndex ${ideaIndex}. Must be 0\u2013${this.ideas.length - 1}.` };
19927
+ }
19928
+ return this.ideas[ideaIndex];
19929
+ }
19930
+ handleAddSection(args) {
19931
+ const section = {
19932
+ order: this.sections.length + 1,
19933
+ title: String(args.title ?? ""),
19934
+ ideaIssueNumber: Number(args.ideaIssueNumber ?? 0),
19935
+ estimatedMinutes: Number(args.estimatedMinutes ?? 2),
19936
+ talkingPoints: args.talkingPoints ?? [],
19937
+ transition: String(args.transition ?? ""),
19938
+ notes: String(args.notes ?? "")
19939
+ };
19940
+ this.sections.push(section);
19941
+ logger_default.info(`[AgendaAgent] Added section ${section.order}: ${section.title}`);
19942
+ return { added: true, order: section.order };
19943
+ }
19944
+ handleSetIntro(args) {
19945
+ this.intro = String(args.text ?? "");
19946
+ logger_default.info(`[AgendaAgent] Set intro (${this.intro.length} chars)`);
19947
+ return { set: true, field: "intro" };
19948
+ }
19949
+ handleSetOutro(args) {
19950
+ this.outro = String(args.text ?? "");
19951
+ logger_default.info(`[AgendaAgent] Set outro (${this.outro.length} chars)`);
19952
+ return { set: true, field: "outro" };
19953
+ }
19954
+ handleFinalizeAgenda(args) {
19955
+ this.finalized = true;
19956
+ const summary = String(args.summary ?? "");
19957
+ logger_default.info(`[AgendaAgent] Agenda finalized: ${summary}`);
19958
+ return { finalized: true, summary };
19959
+ }
19960
+ async generateAgenda(ideas) {
19961
+ this.ideas = ideas;
19962
+ this.resetForRetry();
19963
+ const startTime = Date.now();
19964
+ const ideaList = ideas.map(
19965
+ (idea, i) => `${i}. **#${idea.issueNumber} \u2014 ${idea.topic}**
19966
+ Hook: ${idea.hook}
19967
+ Talking points: ${idea.talkingPoints.join(", ")}`
19968
+ ).join("\n");
19969
+ const userMessage = [
19970
+ `Create a recording agenda from these ${ideas.length} ideas:`,
19971
+ "",
19972
+ ideaList,
19973
+ "",
19974
+ "Structure them into a cohesive recording flow. Call get_brand_context first, then get_idea_details for each, then add_section for each in recording order, then set_intro, set_outro, and finalize_agenda."
19975
+ ].join("\n");
19976
+ await this.run(userMessage);
19977
+ const estimatedDuration = this.sections.reduce((sum, s) => sum + s.estimatedMinutes, 0);
19978
+ const markdown = this.buildMarkdown(estimatedDuration);
19979
+ return {
19980
+ sections: this.sections,
19981
+ intro: this.intro,
19982
+ outro: this.outro,
19983
+ estimatedDuration,
19984
+ markdown,
19985
+ durationMs: Date.now() - startTime
19986
+ };
19987
+ }
19988
+ buildMarkdown(estimatedDuration) {
19989
+ const lines = [
19990
+ "# Recording Agenda",
19991
+ "",
19992
+ `**Estimated Duration:** ${estimatedDuration} minutes`,
19993
+ `**Ideas Covered:** ${this.sections.length}`,
19994
+ "",
19995
+ "## Opening",
19996
+ "",
19997
+ this.intro
19998
+ ];
19999
+ for (const section of this.sections) {
20000
+ lines.push(
20001
+ "",
20002
+ "---",
20003
+ "",
20004
+ `## Section ${section.order}: ${section.title}`,
20005
+ `**Idea:** #${section.ideaIssueNumber} | **Time:** ~${section.estimatedMinutes} min`,
20006
+ "",
20007
+ "### Talking Points",
20008
+ ...section.talkingPoints.map((p) => `- ${p}`),
20009
+ "",
20010
+ "### Notes",
20011
+ section.notes
20012
+ );
20013
+ if (section.transition) {
20014
+ lines.push(
20015
+ "",
20016
+ "### Transition",
20017
+ `> ${section.transition}`
20018
+ );
20019
+ }
20020
+ }
20021
+ lines.push(
20022
+ "",
20023
+ "---",
20024
+ "",
20025
+ "## Closing",
20026
+ "",
20027
+ this.outro,
20028
+ ""
20029
+ );
20030
+ return lines.join("\n");
20031
+ }
20032
+ async destroy() {
20033
+ await super.destroy();
20034
+ }
20035
+ };
20036
+
19197
20037
  // src/L5-assets/pipelineServices.ts
19198
20038
  var costTracker3 = {
19199
20039
  reset: (...args) => costTracker2.reset(...args),
@@ -19223,6 +20063,12 @@ function createInterviewAgent(...args) {
19223
20063
  function createScheduleAgent(...args) {
19224
20064
  return new ScheduleAgent(...args);
19225
20065
  }
20066
+ function createAgendaAgent(...args) {
20067
+ return new AgendaAgent(...args);
20068
+ }
20069
+ function createIdeaDiscoveryAgent(...args) {
20070
+ return new IdeaDiscoveryAgent(...args);
20071
+ }
19226
20072
 
19227
20073
  // src/L6-pipeline/pipeline.ts
19228
20074
  init_types();
@@ -19279,7 +20125,7 @@ async function runStage(stageName, fn, stageResults) {
19279
20125
  return void 0;
19280
20126
  }
19281
20127
  }
19282
- async function processVideo(videoPath, ideas) {
20128
+ async function processVideo(videoPath, ideas, publishBy) {
19283
20129
  const pipelineStart = Date.now();
19284
20130
  const stageResults = [];
19285
20131
  const cfg = getConfig();
@@ -19446,6 +20292,15 @@ async function processVideo(videoPath, ideas) {
19446
20292
  } catch (err) {
19447
20293
  logger_default.warn(`[Pipeline] Failed to generate main video thumbnail: ${err instanceof Error ? err.message : String(err)}`);
19448
20294
  }
20295
+ const defaultPublishBy2 = publishBy ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
20296
+ const hasClips = shorts.length > 0 || mediumClips.length > 0;
20297
+ if (hasClips) {
20298
+ await trackStage("idea-discovery" /* IdeaDiscovery */, async () => {
20299
+ await asset.discoverIdeas(shorts, mediumClips, defaultPublishBy2);
20300
+ });
20301
+ } else {
20302
+ skipStage("idea-discovery" /* IdeaDiscovery */, "NO_CLIPS");
20303
+ }
19449
20304
  let socialPosts = [];
19450
20305
  if (!cfg.SKIP_SOCIAL) {
19451
20306
  const mainPosts = await trackStage("social-media" /* SocialMedia */, () => asset.getSocialPosts()) ?? [];
@@ -19570,13 +20425,13 @@ function generateCostMarkdown(report) {
19570
20425
  }
19571
20426
  return md;
19572
20427
  }
19573
- async function processVideoSafe(videoPath, ideas) {
20428
+ async function processVideoSafe(videoPath, ideas, publishBy) {
19574
20429
  const filename = basename(videoPath);
19575
20430
  const slug = filename.replace(/\.(mp4|mov|webm|avi|mkv)$/i, "");
19576
20431
  await markPending3(slug, videoPath);
19577
20432
  await markProcessing3(slug);
19578
20433
  try {
19579
- const result = await processVideo(videoPath, ideas);
20434
+ const result = await processVideo(videoPath, ideas, publishBy);
19580
20435
  await markCompleted3(slug);
19581
20436
  return result;
19582
20437
  } catch (err) {
@@ -19882,8 +20737,8 @@ function getFFprobePath3(...args) {
19882
20737
  init_scheduleConfig();
19883
20738
  var rl = createReadlineInterface({ input: process.stdin, output: process.stdout });
19884
20739
  function ask(question) {
19885
- return new Promise((resolve3) => {
19886
- rl.question(question, (answer) => resolve3(answer));
20740
+ return new Promise((resolve4) => {
20741
+ rl.question(question, (answer) => resolve4(answer));
19887
20742
  });
19888
20743
  }
19889
20744
  async function runInit() {
@@ -20264,18 +21119,18 @@ function ChatApp({ controller }) {
20264
21119
  useInput((_input, key) => {
20265
21120
  if (key.ctrl && _input === "c") {
20266
21121
  controller.interrupted = true;
20267
- const resolve3 = controller._pendingResolve;
21122
+ const resolve4 = controller._pendingResolve;
20268
21123
  controller._pendingResolve = null;
20269
- if (resolve3) resolve3("");
21124
+ if (resolve4) resolve4("");
20270
21125
  exit();
20271
21126
  }
20272
21127
  });
20273
21128
  const handleSubmit = useCallback((value) => {
20274
21129
  setInputValue("");
20275
21130
  setInputActive(false);
20276
- const resolve3 = controller._pendingResolve;
21131
+ const resolve4 = controller._pendingResolve;
20277
21132
  controller._pendingResolve = null;
20278
- if (resolve3) resolve3(value.trim());
21133
+ if (resolve4) resolve4(value.trim());
20279
21134
  }, [controller]);
20280
21135
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: "100%", children: [
20281
21136
  /* @__PURE__ */ jsx(
@@ -20386,8 +21241,8 @@ var AltScreenChat = class {
20386
21241
  }
20387
21242
  /** Prompt for user input. Returns their trimmed text. */
20388
21243
  promptInput(_prompt) {
20389
- return new Promise((resolve3) => {
20390
- this._pendingResolve = resolve3;
21244
+ return new Promise((resolve4) => {
21245
+ this._pendingResolve = resolve4;
20391
21246
  this.bridge?.setInputActive(true);
20392
21247
  this.bridge?.forceRender();
20393
21248
  });
@@ -20414,18 +21269,18 @@ async function runChat() {
20414
21269
  const choiceText = request.choices.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
20415
21270
  chat.addMessage("system", choiceText + (request.allowFreeform !== false ? "\n (or type a custom answer)" : ""));
20416
21271
  }
20417
- return new Promise((resolve3) => {
21272
+ return new Promise((resolve4) => {
20418
21273
  chat.promptInput("> ").then((answer) => {
20419
21274
  const trimmed = answer.trim();
20420
21275
  chat.addMessage("user", trimmed);
20421
21276
  if (request.choices && request.choices.length > 0) {
20422
21277
  const num = parseInt(trimmed, 10);
20423
21278
  if (num >= 1 && num <= request.choices.length) {
20424
- resolve3({ answer: request.choices[num - 1], wasFreeform: false });
21279
+ resolve4({ answer: request.choices[num - 1], wasFreeform: false });
20425
21280
  return;
20426
21281
  }
20427
21282
  }
20428
- resolve3({ answer: trimmed, wasFreeform: true });
21283
+ resolve4({ answer: trimmed, wasFreeform: true });
20429
21284
  });
20430
21285
  });
20431
21286
  };
@@ -20483,6 +21338,22 @@ async function startInterview(idea, answerProvider, onEvent) {
20483
21338
  if (onEvent) interviewEmitter.removeListener(onEvent);
20484
21339
  }
20485
21340
  }
21341
+ async function generateAgenda(ideas) {
21342
+ const agent = createAgendaAgent(ideas);
21343
+ try {
21344
+ return await agent.generateAgenda(ideas);
21345
+ } finally {
21346
+ await agent.destroy();
21347
+ }
21348
+ }
21349
+ async function discoverIdeas(input) {
21350
+ const agent = createIdeaDiscoveryAgent(input);
21351
+ try {
21352
+ return await agent.discover();
21353
+ } finally {
21354
+ await agent.destroy();
21355
+ }
21356
+ }
20486
21357
 
20487
21358
  // src/L7-app/commands/ideate.ts
20488
21359
  init_types();
@@ -20838,8 +21709,160 @@ async function saveResults(result, chat, issueNumber) {
20838
21709
  if (response.toLowerCase().startsWith("y")) {
20839
21710
  await updateIdea(issueNumber, { status: "ready" });
20840
21711
  chat.showInsight(`\u2705 Idea #${issueNumber} marked as ready`);
20841
- await new Promise((resolve3) => setTimeout(resolve3, 1500));
21712
+ await new Promise((resolve4) => setTimeout(resolve4, 1500));
21713
+ }
21714
+ }
21715
+
21716
+ // src/L7-app/commands/agenda.ts
21717
+ init_ideaService2();
21718
+ import { resolve as resolve3 } from "path";
21719
+ init_fileSystem();
21720
+ init_configLogger();
21721
+ async function runAgenda(issueNumbers, options) {
21722
+ if (issueNumbers.length === 0) {
21723
+ logger_default.error("At least one idea issue number is required.");
21724
+ process.exit(1);
21725
+ }
21726
+ const ids = issueNumbers.flatMap((n) => n.split(",")).map((s) => s.trim()).filter(Boolean);
21727
+ if (ids.length === 0) {
21728
+ logger_default.error("No valid idea issue numbers provided.");
21729
+ process.exit(1);
21730
+ }
21731
+ let ideas;
21732
+ try {
21733
+ ideas = await getIdeasByIds(ids);
21734
+ } catch (err) {
21735
+ const msg = err instanceof Error ? err.message : String(err);
21736
+ logger_default.error(`Failed to resolve ideas: ${msg}`);
21737
+ process.exit(1);
21738
+ }
21739
+ if (ideas.length === 0) {
21740
+ logger_default.error("No ideas found for the provided issue numbers.");
21741
+ process.exit(1);
21742
+ }
21743
+ logger_default.info(`Generating agenda for ${ideas.length} idea(s): ${ideas.map((i) => `#${i.issueNumber} "${i.topic}"`).join(", ")}`);
21744
+ const result = await generateAgenda(ideas);
21745
+ const outputPath = options.output ? resolve3(options.output) : resolve3(`agenda-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.md`);
21746
+ await writeTextFile(outputPath, result.markdown);
21747
+ logger_default.info(`Agenda saved to ${outputPath}`);
21748
+ console.log(`
21749
+ \u2705 Agenda generated (${result.estimatedDuration} min, ${result.sections.length} sections)`);
21750
+ console.log(` Saved to: ${outputPath}
21751
+ `);
21752
+ for (const section of result.sections) {
21753
+ console.log(` ${section.order}. ${section.title} (~${section.estimatedMinutes} min) \u2014 idea #${section.ideaIssueNumber}`);
21754
+ }
21755
+ console.log();
21756
+ }
21757
+
21758
+ // src/L7-app/commands/discoverIdeas.ts
21759
+ init_postStore();
21760
+ init_fileSystem();
21761
+ init_brand();
21762
+ init_paths();
21763
+ init_types();
21764
+ init_configLogger();
21765
+ async function runDiscoverIdeas(options) {
21766
+ const pendingItems = await getPendingItems();
21767
+ if (pendingItems.length === 0) {
21768
+ console.log("No pending items in the publish queue.");
21769
+ return;
21770
+ }
21771
+ const untagged = pendingItems.filter((item) => !item.metadata.ideaIds?.length);
21772
+ if (untagged.length === 0) {
21773
+ console.log(`All ${pendingItems.length} pending items already have ideas assigned.`);
21774
+ return;
21775
+ }
21776
+ console.log(`Found ${untagged.length} untagged items (of ${pendingItems.length} total pending).
21777
+ `);
21778
+ const byVideo = /* @__PURE__ */ new Map();
21779
+ for (const item of untagged) {
21780
+ const key = item.metadata.sourceVideo;
21781
+ const group = byVideo.get(key) ?? [];
21782
+ group.push(item);
21783
+ byVideo.set(key, group);
21784
+ }
21785
+ const defaultPublishBy2 = options.publishBy ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
21786
+ let totalUpdated = 0;
21787
+ let totalFailed = 0;
21788
+ for (const [videoDir, items] of byVideo) {
21789
+ console.log(`
21790
+ \u{1F4F9} ${videoDir} (${items.length} untagged items)`);
21791
+ const transcriptPath = join(videoDir, "transcript.json");
21792
+ const shortsPlanPath = join(videoDir, "shorts-plan.json");
21793
+ const mediumPlanPath = join(videoDir, "medium-clips-plan.json");
21794
+ const summaryPath = join(videoDir, "summary.json");
21795
+ if (!await fileExists(transcriptPath)) {
21796
+ logger_default.warn(`No transcript.json in ${videoDir} \u2014 skipping`);
21797
+ totalFailed += items.length;
21798
+ continue;
21799
+ }
21800
+ const transcript = await readJsonFile(transcriptPath);
21801
+ const shorts = await fileExists(shortsPlanPath) ? await readJsonFile(shortsPlanPath) : [];
21802
+ const mediumClips = await fileExists(mediumPlanPath) ? await readJsonFile(mediumPlanPath) : [];
21803
+ let summaryText = "";
21804
+ if (await fileExists(summaryPath)) {
21805
+ const summary = await readJsonFile(summaryPath);
21806
+ summaryText = summary.overview ?? "";
21807
+ }
21808
+ if (shorts.length === 0 && mediumClips.length === 0) {
21809
+ logger_default.warn(`No shorts or medium clips found in ${videoDir} \u2014 skipping`);
21810
+ totalFailed += items.length;
21811
+ continue;
21812
+ }
21813
+ const brand = getBrandConfig();
21814
+ const defaultPlatforms = ["youtube" /* YouTube */, "tiktok" /* TikTok */, "instagram" /* Instagram */, "linkedin" /* LinkedIn */, "x" /* X */];
21815
+ console.log(` Running idea discovery on ${shorts.length} shorts + ${mediumClips.length} medium clips...`);
21816
+ try {
21817
+ const result = await discoverIdeas({
21818
+ shorts,
21819
+ mediumClips,
21820
+ transcript: transcript.segments,
21821
+ summary: summaryText,
21822
+ publishBy: defaultPublishBy2,
21823
+ defaultPlatforms
21824
+ });
21825
+ console.log(` \u2705 ${result.matchedCount} matched, ${result.createdCount} created
21826
+ `);
21827
+ const clipIdeaMap = /* @__PURE__ */ new Map();
21828
+ for (const assignment of result.assignments) {
21829
+ clipIdeaMap.set(assignment.clipId, assignment.ideaIssueNumber);
21830
+ }
21831
+ const allIdeaIds = [...new Set(result.assignments.map((a) => String(a.ideaIssueNumber)))];
21832
+ for (const item of items) {
21833
+ if (options.dryRun) {
21834
+ console.log(` [dry-run] Would update ${item.metadata.id}`);
21835
+ continue;
21836
+ }
21837
+ let ideaIds;
21838
+ if (item.metadata.sourceClip) {
21839
+ const matchedShort = shorts.find((s) => s.slug === item.metadata.sourceClip);
21840
+ const matchedMedium = mediumClips.find((m) => m.slug === item.metadata.sourceClip);
21841
+ const clipId = matchedShort?.id ?? matchedMedium?.id;
21842
+ if (clipId && clipIdeaMap.has(clipId)) {
21843
+ ideaIds = [String(clipIdeaMap.get(clipId))];
21844
+ }
21845
+ }
21846
+ if (!ideaIds && allIdeaIds.length > 0) {
21847
+ ideaIds = allIdeaIds;
21848
+ }
21849
+ if (ideaIds) {
21850
+ await updateItem(item.metadata.id, { metadata: { ideaIds } });
21851
+ console.log(` \u{1F4CC} ${item.metadata.id} \u2192 idea(s) ${ideaIds.join(", ")}`);
21852
+ totalUpdated++;
21853
+ } else {
21854
+ console.log(` \u26A0\uFE0F ${item.metadata.id} \u2014 no idea match found`);
21855
+ totalFailed++;
21856
+ }
21857
+ }
21858
+ } catch (err) {
21859
+ const msg = err instanceof Error ? err.message : String(err);
21860
+ logger_default.error(`Idea discovery failed for ${videoDir}: ${msg}`);
21861
+ totalFailed += items.length;
21862
+ }
20842
21863
  }
21864
+ console.log(`
21865
+ \u{1F3C1} Done: ${totalUpdated} items updated, ${totalFailed} skipped/failed`);
20843
21866
  }
20844
21867
 
20845
21868
  // src/L1-infra/readline/readlinePromises.ts
@@ -21985,8 +23008,8 @@ init_configLogger();
21985
23008
  var queue = [];
21986
23009
  var processing = false;
21987
23010
  function enqueueApproval(itemIds) {
21988
- return new Promise((resolve3) => {
21989
- queue.push({ itemIds, resolve: resolve3 });
23011
+ return new Promise((resolve4) => {
23012
+ queue.push({ itemIds, resolve: resolve4 });
21990
23013
  if (!processing) drain();
21991
23014
  });
21992
23015
  }
@@ -22040,23 +23063,26 @@ async function processApprovalBatch(itemIds) {
22040
23063
  }
22041
23064
  }
22042
23065
  const enriched = loadedItems.map(({ id, item }) => {
23066
+ const createdAt = item?.metadata.createdAt ?? null;
22043
23067
  if (!item?.metadata.ideaIds?.length) {
22044
- return { id, publishBy: null, hasIdeas: false };
23068
+ return { id, publishBy: null, hasIdeas: false, createdAt };
22045
23069
  }
22046
23070
  const dates = item.metadata.ideaIds.map((ideaId) => ideaMap.get(ideaId)?.publishBy).filter((publishBy) => Boolean(publishBy)).sort();
22047
- return { id, publishBy: dates[0] ?? null, hasIdeas: true };
23071
+ return { id, publishBy: dates[0] ?? null, hasIdeas: true, createdAt };
22048
23072
  });
22049
- const now = Date.now();
22050
- const sevenDays = 7 * 24 * 60 * 60 * 1e3;
22051
23073
  enriched.sort((a, b) => {
22052
- const aPublishByTime = a.publishBy ? new Date(a.publishBy).getTime() : Number.NaN;
22053
- const bPublishByTime = b.publishBy ? new Date(b.publishBy).getTime() : Number.NaN;
22054
- const aUrgent = a.hasIdeas && Number.isFinite(aPublishByTime) && aPublishByTime - now < sevenDays;
22055
- const bUrgent = b.hasIdeas && Number.isFinite(bPublishByTime) && bPublishByTime - now < sevenDays;
22056
- if (aUrgent && !bUrgent) return -1;
22057
- if (!aUrgent && bUrgent) return 1;
22058
23074
  if (a.hasIdeas && !b.hasIdeas) return -1;
22059
23075
  if (!a.hasIdeas && b.hasIdeas) return 1;
23076
+ if (a.hasIdeas && b.hasIdeas) {
23077
+ const aTime = a.publishBy ? new Date(a.publishBy).getTime() : Infinity;
23078
+ const bTime = b.publishBy ? new Date(b.publishBy).getTime() : Infinity;
23079
+ if (aTime !== bTime) return aTime - bTime;
23080
+ if (a.createdAt && b.createdAt) {
23081
+ const aCreated = new Date(a.createdAt).getTime();
23082
+ const bCreated = new Date(b.createdAt).getTime();
23083
+ if (aCreated !== bCreated) return aCreated - bCreated;
23084
+ }
23085
+ }
22060
23086
  return 0;
22061
23087
  });
22062
23088
  const sortedIds = enriched.map((entry) => entry.id);
@@ -22404,7 +23430,7 @@ async function startReviewServer(options = {}) {
22404
23430
  res.sendFile(join(publicDir, "index.html"));
22405
23431
  }
22406
23432
  });
22407
- return new Promise((resolve3, reject) => {
23433
+ return new Promise((resolve4, reject) => {
22408
23434
  const tryPort = (p, attempts) => {
22409
23435
  const server = app.listen(p, "127.0.0.1", () => {
22410
23436
  logger_default.info(`Review server running at http://localhost:${p}`);
@@ -22413,7 +23439,7 @@ async function startReviewServer(options = {}) {
22413
23439
  connections.add(conn);
22414
23440
  conn.on("close", () => connections.delete(conn));
22415
23441
  });
22416
- resolve3({
23442
+ resolve4({
22417
23443
  port: p,
22418
23444
  close: () => new Promise((res) => {
22419
23445
  let done = false;
@@ -22448,6 +23474,28 @@ async function startReviewServer(options = {}) {
22448
23474
  });
22449
23475
  }
22450
23476
 
23477
+ // src/L7-app/parsePublishBy.ts
23478
+ function parsePublishBy(raw) {
23479
+ const trimmed = raw.trim();
23480
+ const relativeMatch = trimmed.match(/^\+(\d+)d$/i);
23481
+ if (relativeMatch) {
23482
+ return new Date(Date.now() + parseInt(relativeMatch[1], 10) * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
23483
+ }
23484
+ const isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
23485
+ if (!isoDatePattern.test(trimmed)) {
23486
+ throw new Error(
23487
+ `Invalid --publish-by value "${trimmed}". Expected "+Nd" (e.g., +7d) or ISO date "YYYY-MM-DD".`
23488
+ );
23489
+ }
23490
+ const parsed = new Date(trimmed);
23491
+ if (Number.isNaN(parsed.getTime())) {
23492
+ throw new Error(
23493
+ `Invalid --publish-by date "${trimmed}". Provide a valid calendar date in "YYYY-MM-DD" format or use "+Nd".`
23494
+ );
23495
+ }
23496
+ return parsed.toISOString().split("T")[0];
23497
+ }
23498
+
22451
23499
  // src/L7-app/cli.ts
22452
23500
  init_fileSystem();
22453
23501
  init_paths();
@@ -22517,6 +23565,24 @@ program.command("ideate-start <issue-number>").description("Start an interactive
22517
23565
  await runIdeateStart(issueNumber, opts);
22518
23566
  process.exit(0);
22519
23567
  });
23568
+ program.command("agenda <issue-numbers...>").description("Generate a structured recording agenda from multiple ideas").option("--output <path>", "Output file path for the agenda markdown").action(async (issueNumbers, opts) => {
23569
+ initConfig({});
23570
+ await runAgenda(issueNumbers, opts);
23571
+ process.exit(0);
23572
+ });
23573
+ program.command("discover-ideas").description("Retroactively run idea discovery on pending publish queue items that have no ideas assigned").option("--publish-by <date>", "Publish-by deadline for new ideas (ISO date or +Nd, default: +7d)").option("--dry-run", "Preview what would be updated without making changes").action(async (opts) => {
23574
+ initConfig({});
23575
+ if (opts.publishBy) {
23576
+ try {
23577
+ opts.publishBy = parsePublishBy(String(opts.publishBy));
23578
+ } catch (err) {
23579
+ logger_default.error(err.message);
23580
+ process.exit(1);
23581
+ }
23582
+ }
23583
+ await runDiscoverIdeas(opts);
23584
+ process.exit(0);
23585
+ });
22520
23586
  program.command("ideate").description("Generate AI-powered content ideas using trend research").option("--topics <topics>", "Comma-separated seed topics").option("--count <n>", "Number of ideas to generate (default: 5)", "5").option("--output <dir>", "Ideas directory (default: ./ideas)").option("--brand <path>", "Brand config path (default: ./brand.json)").option("--list", "List existing ideas instead of generating").option("--status <status>", "Filter by status when listing (draft|ready|recorded|published)").option("--format <format>", "Output format: table (default) or json").option("--add", "Add a single idea (AI-researched by default, or --no-ai for direct)").option("--topic <topic>", "Idea topic/title (required with --add)").option("--hook <hook>", "Attention-grabbing hook (default: topic, --no-ai only)").option("--audience <audience>", "Target audience (default: developers, --no-ai only)").option("--platforms <platforms>", "Comma-separated platforms: tiktok,youtube,instagram,linkedin,x (--no-ai only)").option("--key-takeaway <takeaway>", "Core message the viewer should remember (--no-ai only)").option("--talking-points <points>", "Comma-separated talking points (--no-ai only)").option("--tags <tags>", "Comma-separated categorization tags (--no-ai only)").option("--publish-by <date>", "Publish deadline (ISO 8601 date, default: 14 days from now, --no-ai only)").option("--trend-context <context>", "Why this topic is timely (--no-ai only)").option("--no-ai", "Skip AI research agent \u2014 create directly from CLI flags + defaults").option("-p, --prompt <prompt>", 'Free-form prompt to guide idea generation (e.g., "Cover this article: https://...")').action(async (opts) => {
22521
23587
  initConfig();
22522
23588
  await runIdeate(opts);
@@ -22561,7 +23627,7 @@ program.command("thumbnail").description("Generate a thumbnail for a recording f
22561
23627
  });
22562
23628
  process.exit(0);
22563
23629
  });
22564
- var defaultCmd = program.command("process", { isDefault: true }).argument("[video-path]", "Path to a video file to process (implies --once)").option("--watch-dir <path>", "Folder to watch for new recordings (default: env WATCH_FOLDER)").option("--output-dir <path>", "Output directory for processed videos (default: ./recordings)").option("--openai-key <key>", "OpenAI API key (default: env OPENAI_API_KEY)").option("--exa-key <key>", "Exa AI API key for web search (default: env EXA_API_KEY)").option("--youtube-key <key>", "YouTube API key (default: env YOUTUBE_API_KEY)").option("--perplexity-key <key>", "Perplexity API key (default: env PERPLEXITY_API_KEY)").option("--once", "Process a single video and exit (no watching)").option("--brand <path>", "Path to brand.json config (default: ./brand.json)").option("--no-silence-removal", "Skip silence removal stage").option("--no-shorts", "Skip shorts generation").option("--no-medium-clips", "Skip medium clip generation").option("--no-social", "Skip social media post generation").option("--no-captions", "Skip caption generation/burning").option("--no-visual-enhancement", "Skip visual enhancement (AI image overlays)").option("--no-intro-outro", "Skip intro/outro concatenation").option("--no-social-publish", "Skip social media publishing/queue-build stage").option("--late-api-key <key>", "Late API key (default: env LATE_API_KEY)").option("--late-profile-id <id>", "Late profile ID (default: env LATE_PROFILE_ID)").option("--ideas <ids>", "Comma-separated idea IDs to link to this video").option("-v, --verbose", "Verbose logging").option("--progress", "Emit structured JSON progress events to stderr").option("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
23630
+ var defaultCmd = program.command("process", { isDefault: true }).argument("[video-path]", "Path to a video file to process (implies --once)").option("--watch-dir <path>", "Folder to watch for new recordings (default: env WATCH_FOLDER)").option("--output-dir <path>", "Output directory for processed videos (default: ./recordings)").option("--openai-key <key>", "OpenAI API key (default: env OPENAI_API_KEY)").option("--exa-key <key>", "Exa AI API key for web search (default: env EXA_API_KEY)").option("--youtube-key <key>", "YouTube API key (default: env YOUTUBE_API_KEY)").option("--perplexity-key <key>", "Perplexity API key (default: env PERPLEXITY_API_KEY)").option("--once", "Process a single video and exit (no watching)").option("--brand <path>", "Path to brand.json config (default: ./brand.json)").option("--no-silence-removal", "Skip silence removal stage").option("--no-shorts", "Skip shorts generation").option("--no-medium-clips", "Skip medium clip generation").option("--no-social", "Skip social media post generation").option("--no-captions", "Skip caption generation/burning").option("--no-visual-enhancement", "Skip visual enhancement (AI image overlays)").option("--no-intro-outro", "Skip intro/outro concatenation").option("--no-social-publish", "Skip social media publishing/queue-build stage").option("--late-api-key <key>", "Late API key (default: env LATE_API_KEY)").option("--late-profile-id <id>", "Late profile ID (default: env LATE_PROFILE_ID)").option("--ideas <ids>", "Comma-separated idea IDs to link to this video").option("--publish-by <date>", "Publish-by deadline for auto-created ideas (ISO date or +Nd for relative, default: +7d)").option("-v, --verbose", "Verbose logging").option("--progress", "Emit structured JSON progress events to stderr").option("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
22565
23631
  const opts = defaultCmd.opts();
22566
23632
  if (opts.doctor) {
22567
23633
  await runDoctor();
@@ -22612,7 +23678,21 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
22612
23678
  if (videoPath) {
22613
23679
  const resolvedPath = resolve(videoPath);
22614
23680
  logger_default.info(`Processing single video: ${resolvedPath}`);
22615
- await processVideoSafe(resolvedPath, ideas);
23681
+ let publishBy;
23682
+ if (opts.publishBy) {
23683
+ const raw = String(opts.publishBy).trim();
23684
+ const relativeMatch = raw.match(/^\+(\d+)d$/i);
23685
+ if (relativeMatch) {
23686
+ const days = parseInt(relativeMatch[1], 10);
23687
+ publishBy = new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
23688
+ } else if (!Number.isNaN(new Date(raw).getTime())) {
23689
+ publishBy = raw;
23690
+ } else {
23691
+ logger_default.error(`Invalid --publish-by format: "${raw}". Use ISO date (2026-04-01) or relative (+7d).`);
23692
+ process.exit(1);
23693
+ }
23694
+ }
23695
+ await processVideoSafe(resolvedPath, ideas, publishBy);
22616
23696
  if (ideas && ideas.length > 0) {
22617
23697
  try {
22618
23698
  const { markRecorded: markRecorded3 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));