vidpipe 1.3.16 → 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);
@@ -7904,10 +7912,14 @@ var init_CopilotProvider = __esm({
7904
7912
  let response;
7905
7913
  let sdkError;
7906
7914
  try {
7907
- response = await this.session.sendAndWait(
7908
- { prompt: message },
7909
- this.timeoutMs
7910
- );
7915
+ if (this.timeoutMs === 0) {
7916
+ response = await this.sendAndWaitForIdle(message);
7917
+ } else {
7918
+ response = await this.session.sendAndWait(
7919
+ { prompt: message },
7920
+ this.timeoutMs
7921
+ );
7922
+ }
7911
7923
  } catch (err) {
7912
7924
  sdkError = err instanceof Error ? err : new Error(String(err));
7913
7925
  if (sdkError.message.includes("missing finish_reason")) {
@@ -7931,6 +7943,38 @@ var init_CopilotProvider = __esm({
7931
7943
  durationMs: Date.now() - start
7932
7944
  };
7933
7945
  }
7946
+ /**
7947
+ * Send a message and wait for session.idle without any timeout.
7948
+ * Used by interactive agents (interview, chat) where tool handlers
7949
+ * block waiting for human input — the SDK's sendAndWait() timeout
7950
+ * would fire while the agent is legitimately waiting for the user.
7951
+ */
7952
+ sendAndWaitForIdle(message) {
7953
+ return new Promise((resolve4, reject) => {
7954
+ let lastAssistantMessage;
7955
+ const unsubMessage = this.session.on("assistant.message", (event) => {
7956
+ lastAssistantMessage = event;
7957
+ });
7958
+ const unsubIdle = this.session.on("session.idle", () => {
7959
+ unsubMessage();
7960
+ unsubIdle();
7961
+ unsubError();
7962
+ resolve4(lastAssistantMessage);
7963
+ });
7964
+ const unsubError = this.session.on("session.error", (event) => {
7965
+ unsubMessage();
7966
+ unsubIdle();
7967
+ unsubError();
7968
+ reject(new Error(event.data?.message ?? "Unknown session error"));
7969
+ });
7970
+ this.session.send({ prompt: message }).catch((err) => {
7971
+ unsubMessage();
7972
+ unsubIdle();
7973
+ unsubError();
7974
+ reject(err instanceof Error ? err : new Error(String(err)));
7975
+ });
7976
+ });
7977
+ }
7934
7978
  on(event, handler) {
7935
7979
  const handlers = this.eventHandlers.get(event) ?? [];
7936
7980
  handlers.push(handler);
@@ -8622,7 +8666,7 @@ var init_BaseAgent = __esm({
8622
8666
  this.resetForRetry();
8623
8667
  const delayMs = 2e3 * Math.pow(2, attempt - 1);
8624
8668
  logger_default.warn(`[${this.agentName}] Transient error (attempt ${attempt}/${_BaseAgent.MAX_RETRIES}), retrying in ${delayMs / 1e3}s: ${message}`);
8625
- await new Promise((resolve3) => setTimeout(resolve3, delayMs));
8669
+ await new Promise((resolve4) => setTimeout(resolve4, delayMs));
8626
8670
  }
8627
8671
  }
8628
8672
  throw lastError;
@@ -9775,22 +9819,6 @@ var init_ideaService = __esm({
9775
9819
  });
9776
9820
 
9777
9821
  // src/L3-services/postStore/postStore.ts
9778
- var postStore_exports = {};
9779
- __export(postStore_exports, {
9780
- approveBulk: () => approveBulk,
9781
- approveItem: () => approveItem,
9782
- createItem: () => createItem,
9783
- getGroupedPendingItems: () => getGroupedPendingItems,
9784
- getItem: () => getItem,
9785
- getPendingItems: () => getPendingItems,
9786
- getPublishedItemByLatePostId: () => getPublishedItemByLatePostId,
9787
- getPublishedItems: () => getPublishedItems,
9788
- getScheduledItemsByIdeaIds: () => getScheduledItemsByIdeaIds,
9789
- itemExists: () => itemExists,
9790
- rejectItem: () => rejectItem,
9791
- updateItem: () => updateItem,
9792
- updatePublishedItemSchedule: () => updatePublishedItemSchedule
9793
- });
9794
9822
  function getQueueDir() {
9795
9823
  const { OUTPUT_DIR } = getConfig();
9796
9824
  return join(OUTPUT_DIR, "publish-queue");
@@ -10171,10 +10199,6 @@ async function getScheduledItemsByIdeaIds(ideaIds) {
10171
10199
  (item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
10172
10200
  );
10173
10201
  }
10174
- async function getPublishedItemByLatePostId(latePostId) {
10175
- const publishedItems = await getPublishedItems();
10176
- return publishedItems.find((item) => item.metadata.latePostId === latePostId) ?? null;
10177
- }
10178
10202
  async function updatePublishedItemSchedule(id, scheduledFor) {
10179
10203
  if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
10180
10204
  throw new Error(`Invalid ID format: ${id}`);
@@ -10536,8 +10560,8 @@ function validateByClipType(byClipType, platformName) {
10536
10560
  return validated;
10537
10561
  }
10538
10562
  function validatePositiveNumber(value, fieldName) {
10539
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
10540
- 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`);
10541
10565
  }
10542
10566
  return value;
10543
10567
  }
@@ -10681,9 +10705,6 @@ function getPlatformSchedule(platform, clipType) {
10681
10705
  function getIdeaSpacingConfig() {
10682
10706
  return cachedConfig?.ideaSpacing ?? { ...defaultIdeaSpacing };
10683
10707
  }
10684
- function getDisplacementConfig() {
10685
- return cachedConfig?.displacement ?? { ...defaultDisplacement };
10686
- }
10687
10708
  var VALID_DAYS, TIME_REGEX, defaultIdeaSpacing, defaultDisplacement, cachedConfig, PLATFORM_ALIASES;
10688
10709
  var init_scheduleConfig = __esm({
10689
10710
  "src/L3-services/scheduler/scheduleConfig.ts"() {
@@ -10777,9 +10798,11 @@ async function buildBookedMap(platform) {
10777
10798
  getPublishedItems()
10778
10799
  ]);
10779
10800
  const ideaLinkedPostIds = /* @__PURE__ */ new Set();
10801
+ const latePostIdToIdeaIds = /* @__PURE__ */ new Map();
10780
10802
  for (const item of publishedItems) {
10781
10803
  if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
10782
10804
  ideaLinkedPostIds.add(item.metadata.latePostId);
10805
+ latePostIdToIdeaIds.set(item.metadata.latePostId, item.metadata.ideaIds);
10783
10806
  }
10784
10807
  }
10785
10808
  const map = /* @__PURE__ */ new Map();
@@ -10794,7 +10817,8 @@ async function buildBookedMap(platform) {
10794
10817
  postId: post._id,
10795
10818
  platform: scheduledPlatform.platform,
10796
10819
  status: post.status,
10797
- ideaLinked: ideaLinkedPostIds.has(post._id)
10820
+ ideaLinked: ideaLinkedPostIds.has(post._id),
10821
+ ideaIds: latePostIdToIdeaIds.get(post._id)
10798
10822
  });
10799
10823
  }
10800
10824
  }
@@ -10803,28 +10827,20 @@ async function buildBookedMap(platform) {
10803
10827
  if (platform && item.metadata.platform !== platform) continue;
10804
10828
  if (!item.metadata.scheduledFor) continue;
10805
10829
  const ms = normalizeDateTime(item.metadata.scheduledFor);
10830
+ if (ms < Date.now()) continue;
10806
10831
  if (!map.has(ms)) {
10807
10832
  map.set(ms, {
10808
10833
  scheduledFor: item.metadata.scheduledFor,
10809
10834
  source: "local",
10810
10835
  itemId: item.id,
10811
10836
  platform: item.metadata.platform,
10812
- ideaLinked: Boolean(item.metadata.ideaIds?.length)
10837
+ ideaLinked: Boolean(item.metadata.ideaIds?.length),
10838
+ ideaIds: item.metadata.ideaIds
10813
10839
  });
10814
10840
  }
10815
10841
  }
10816
10842
  return map;
10817
10843
  }
10818
- async function getIdeaLinkedLatePostIds() {
10819
- const publishedItems = await getPublishedItems();
10820
- const ids = /* @__PURE__ */ new Set();
10821
- for (const item of publishedItems) {
10822
- if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
10823
- ids.add(item.metadata.latePostId);
10824
- }
10825
- }
10826
- return ids;
10827
- }
10828
10844
  function* generateTimeslots(platformConfig, timezone, fromMs, maxMs) {
10829
10845
  const baseDate = new Date(fromMs);
10830
10846
  const upperMs = maxMs ?? fromMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
@@ -10896,13 +10912,55 @@ async function getIdeaReferences(ideaIds, bookedMap) {
10896
10912
  }
10897
10913
  return refs;
10898
10914
  }
10899
- 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) {
10900
10957
  const indent = " ".repeat(ctx.depth);
10901
10958
  let checked = 0;
10902
10959
  let skippedBooked = 0;
10903
10960
  let skippedSpacing = 0;
10904
- logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}`);
10905
- 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)) {
10906
10964
  checked++;
10907
10965
  const booked = ctx.bookedMap.get(ms);
10908
10966
  if (!booked) {
@@ -10913,10 +10971,14 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10913
10971
  }
10914
10972
  continue;
10915
10973
  }
10916
- 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})`);
10917
10979
  return datetime;
10918
10980
  }
10919
- if (isIdeaPost && ctx.displacementEnabled && !booked.ideaLinked && booked.source === "late" && booked.postId) {
10981
+ if (isIdeaPost && !booked.ideaLinked && booked.source === "late" && booked.postId) {
10920
10982
  if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
10921
10983
  skippedSpacing++;
10922
10984
  if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
@@ -10925,12 +10987,13 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10925
10987
  continue;
10926
10988
  }
10927
10989
  logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by non-idea post ${booked.postId} \u2014 displacing`);
10928
- const newHome = await schedulePost(
10990
+ const newHome = await findSlot(
10929
10991
  platformConfig,
10930
10992
  ms,
10931
10993
  false,
10994
+ booked.postId,
10932
10995
  `displaced:${booked.postId}`,
10933
- { ...ctx, depth: ctx.depth + 1 }
10996
+ { ...ctx, depth: ctx.depth + 1, publishByMs: void 0 }
10934
10997
  );
10935
10998
  if (newHome) {
10936
10999
  if (!ctx.dryRun) {
@@ -10946,12 +11009,51 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10946
11009
  ctx.bookedMap.delete(ms);
10947
11010
  const newMs = normalizeDateTime(newHome);
10948
11011
  ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
10949
- 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})`);
10950
11013
  return datetime;
10951
11014
  }
10952
11015
  logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace ${booked.postId} \u2014 no empty slot found after ${datetime}`);
10953
11016
  }
10954
- 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
+ }
10955
11057
  skippedBooked++;
10956
11058
  if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
10957
11059
  logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken by idea post ${booked.postId ?? booked.itemId} \u2014 skipping`);
@@ -10966,135 +11068,187 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
10966
11068
  logger_default.warn(`[schedulePost] \u274C No slot found for ${label} \u2014 checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing`);
10967
11069
  return null;
10968
11070
  }
10969
- async function findNextSlot(platform, clipType, options) {
11071
+ async function schedulePost(platform, clipType, options) {
10970
11072
  const config2 = await loadScheduleConfig();
10971
11073
  const platformConfig = getPlatformSchedule(platform, clipType);
10972
11074
  if (!platformConfig) {
10973
- logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
11075
+ logger_default.warn(`[schedulePost] No schedule config found for platform "${sanitizeLogValue(platform)}"`);
10974
11076
  return null;
10975
11077
  }
10976
11078
  const { timezone } = config2;
10977
11079
  const nowMs = Date.now();
10978
11080
  const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
10979
- const isIdeaAware = ideaIds.length > 0;
10980
- const bookedMap = await buildBookedMap(platform);
10981
- 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;
10982
11086
  const label = `${platform}/${clipType ?? "default"}`;
11087
+ const bookedMap = options?._bookedMap ?? await buildBookedMap(platform);
10983
11088
  let ideaRefs = [];
10984
11089
  let samePlatformMs = 0;
10985
11090
  let crossPlatformMs = 0;
10986
- if (isIdeaAware) {
10987
- const allBookedMap = await buildBookedMap();
11091
+ if (isIdeaPost) {
11092
+ const allBookedMap = options?._bookedMap ?? await buildBookedMap();
10988
11093
  ideaRefs = await getIdeaReferences(ideaIds, allBookedMap);
10989
11094
  const spacingConfig = getIdeaSpacingConfig();
10990
11095
  samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
10991
11096
  crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
10992
11097
  }
10993
- 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}` : ""})`);
10994
11108
  const ctx = {
10995
11109
  timezone,
10996
11110
  bookedMap,
10997
- ideaLinkedPostIds,
10998
11111
  lateClient: new LateApiClient(),
10999
- displacementEnabled: getDisplacementConfig().enabled,
11000
- dryRun: false,
11112
+ dryRun,
11001
11113
  depth: 0,
11002
11114
  ideaRefs,
11003
11115
  samePlatformMs,
11004
11116
  crossPlatformMs,
11005
- platform
11117
+ platform,
11118
+ ideaPublishByMap
11006
11119
  };
11007
- 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
+ }
11008
11130
  if (!result) {
11009
- 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}`);
11010
11142
  }
11011
11143
  return result;
11012
11144
  }
11013
- async function rescheduleIdeaPosts(options) {
11145
+ async function findNextSlot(platform, clipType, options) {
11146
+ return schedulePost(platform, clipType, options);
11147
+ }
11148
+ async function rescheduleAllPosts(options) {
11014
11149
  const dryRun = options?.dryRun ?? false;
11015
- const { updatePublishedItemSchedule: updatePublishedItemSchedule2 } = await Promise.resolve().then(() => (init_postStore(), postStore_exports));
11016
11150
  const config2 = await loadScheduleConfig();
11017
11151
  const { timezone } = config2;
11018
11152
  const publishedItems = await getPublishedItems();
11019
- const ideaPosts = publishedItems.filter(
11020
- (item) => item.metadata.ideaIds?.length && item.metadata.latePostId
11021
- );
11022
- if (ideaPosts.length === 0) {
11023
- 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");
11024
11156
  return { rescheduled: 0, unchanged: 0, failed: 0, details: [] };
11025
11157
  }
11026
- logger_default.info(`Found ${ideaPosts.length} idea-linked posts to reschedule`);
11027
- const ideaLatePostIds = new Set(ideaPosts.map((item) => item.metadata.latePostId));
11028
- const fullBookedMap = await buildBookedMap();
11029
- for (const [ms, slot] of fullBookedMap) {
11030
- if (slot.postId && ideaLatePostIds.has(slot.postId)) {
11031
- fullBookedMap.delete(ms);
11032
- }
11033
- }
11034
- const { getIdea: getIdea2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
11035
- 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();
11036
11162
  for (const item of ideaPosts) {
11037
11163
  const ideaId = item.metadata.ideaIds?.[0];
11038
- if (ideaId && !ideaPublishByMap.has(ideaId)) {
11039
- try {
11040
- const idea = await getIdea2(parseInt(ideaId, 10));
11041
- if (idea?.publishBy) ideaPublishByMap.set(ideaId, idea.publishBy);
11042
- } 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());
11043
11168
  }
11044
11169
  }
11045
11170
  }
11046
11171
  ideaPosts.sort((a, b) => {
11047
11172
  const aId = a.metadata.ideaIds?.[0];
11048
11173
  const bId = b.metadata.ideaIds?.[0];
11049
- const aDate = aId ? ideaPublishByMap.get(aId) ?? "9999" : "9999";
11050
- 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";
11051
11176
  return aDate.localeCompare(bDate);
11052
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
+ }
11053
11185
  const lateClient = new LateApiClient();
11054
11186
  const result = { rescheduled: 0, unchanged: 0, failed: 0, details: [] };
11055
11187
  const nowMs = Date.now();
11056
- const ctx = {
11057
- timezone,
11058
- bookedMap: fullBookedMap,
11059
- ideaLinkedPostIds: /* @__PURE__ */ new Set(),
11060
- lateClient,
11061
- displacementEnabled: getDisplacementConfig().enabled,
11062
- dryRun,
11063
- depth: 0,
11064
- ideaRefs: [],
11065
- samePlatformMs: 0,
11066
- crossPlatformMs: 0,
11067
- platform: ""
11068
- };
11069
- for (const item of ideaPosts) {
11070
- const platform = item.metadata.platform;
11188
+ for (const item of allPosts) {
11189
+ const itemPlatform = item.metadata.platform;
11071
11190
  const clipType = item.metadata.clipType;
11072
11191
  const latePostId = item.metadata.latePostId;
11073
11192
  const oldSlot = item.metadata.scheduledFor;
11074
- const label = `${item.id} (${platform}/${clipType})`;
11193
+ const isIdea = Boolean(item.metadata.ideaIds?.length);
11194
+ const label = `${item.id} (${itemPlatform}/${clipType})`;
11075
11195
  try {
11076
- const platformConfig = getPlatformSchedule(platform, clipType);
11196
+ const platformConfig = getPlatformSchedule(itemPlatform, clipType);
11077
11197
  if (!platformConfig) {
11078
- 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" });
11079
11199
  result.failed++;
11080
11200
  continue;
11081
11201
  }
11082
- 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
+ }
11083
11236
  if (!newSlotDatetime) {
11084
- 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" });
11085
11238
  result.failed++;
11086
11239
  continue;
11087
11240
  }
11088
11241
  const newSlotMs = normalizeDateTime(newSlotDatetime);
11089
11242
  if (oldSlot && normalizeDateTime(oldSlot) === newSlotMs) {
11090
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: newSlotDatetime });
11243
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
11091
11244
  result.unchanged++;
11092
- ctx.bookedMap.set(newSlotMs, {
11245
+ bookedMap.set(newSlotMs, {
11093
11246
  scheduledFor: newSlotDatetime,
11094
11247
  source: "late",
11095
11248
  postId: latePostId,
11096
- platform,
11097
- ideaLinked: true
11249
+ platform: itemPlatform,
11250
+ ideaLinked: isIdea,
11251
+ ideaIds: item.metadata.ideaIds
11098
11252
  });
11099
11253
  continue;
11100
11254
  }
@@ -11105,34 +11259,45 @@ async function rescheduleIdeaPosts(options) {
11105
11259
  const errMsg = scheduleErr instanceof Error ? scheduleErr.message : String(scheduleErr);
11106
11260
  if (errMsg.includes("Published posts can only have their recycling config updated")) {
11107
11261
  logger_default.info(`Skipping ${label}: post already published on platform`);
11108
- 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" });
11109
11263
  result.unchanged++;
11110
11264
  continue;
11111
11265
  }
11112
11266
  throw scheduleErr;
11113
11267
  }
11114
- await updatePublishedItemSchedule2(item.id, newSlotDatetime);
11268
+ await updatePublishedItemSchedule(item.id, newSlotDatetime);
11269
+ }
11270
+ if (oldSlot) {
11271
+ const oldMs = normalizeDateTime(oldSlot);
11272
+ const oldBooked = bookedMap.get(oldMs);
11273
+ if (oldBooked?.postId === latePostId) {
11274
+ bookedMap.delete(oldMs);
11275
+ }
11115
11276
  }
11116
- ctx.bookedMap.set(newSlotMs, {
11277
+ bookedMap.set(newSlotMs, {
11117
11278
  scheduledFor: newSlotDatetime,
11118
11279
  source: "late",
11119
11280
  postId: latePostId,
11120
- platform,
11121
- ideaLinked: true
11281
+ platform: itemPlatform,
11282
+ ideaLinked: isIdea,
11283
+ ideaIds: item.metadata.ideaIds
11122
11284
  });
11123
11285
  logger_default.info(`Rescheduled ${label}: ${oldSlot ?? "unscheduled"} \u2192 ${newSlotDatetime}`);
11124
- result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: newSlotDatetime });
11286
+ result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
11125
11287
  result.rescheduled++;
11126
11288
  } catch (err) {
11127
11289
  const msg = err instanceof Error ? err.message : String(err);
11128
11290
  logger_default.error(`Failed to reschedule ${label}: ${msg}`);
11129
- 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 });
11130
11292
  result.failed++;
11131
11293
  }
11132
11294
  }
11133
11295
  logger_default.info(`Reschedule complete: ${result.rescheduled} moved, ${result.unchanged} unchanged, ${result.failed} failed`);
11134
11296
  return result;
11135
11297
  }
11298
+ async function rescheduleIdeaPosts(options) {
11299
+ return rescheduleAllPosts(options);
11300
+ }
11136
11301
  async function getScheduleCalendar(startDate, endDate) {
11137
11302
  const bookedMap = await buildBookedMap();
11138
11303
  let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
@@ -11434,7 +11599,7 @@ var FileWatcher = class _FileWatcher extends EventEmitter {
11434
11599
  async isFileStable(filePath) {
11435
11600
  try {
11436
11601
  const sizeBefore = getFileStatsSync(filePath).size;
11437
- await new Promise((resolve3) => setTimeout(resolve3, _FileWatcher.EXTRA_STABILITY_DELAY));
11602
+ await new Promise((resolve4) => setTimeout(resolve4, _FileWatcher.EXTRA_STABILITY_DELAY));
11438
11603
  const sizeAfter = getFileStatsSync(filePath).size;
11439
11604
  return sizeBefore === sizeAfter;
11440
11605
  } catch {
@@ -12278,7 +12443,7 @@ async function analyzeVideoEditorial(videoPath, durationSeconds, model) {
12278
12443
  logger_default.info(`[Gemini] Waiting for file processing to complete...`);
12279
12444
  let fileState = file.state;
12280
12445
  while (fileState === "PROCESSING") {
12281
- await new Promise((resolve3) => setTimeout(resolve3, 2e3));
12446
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
12282
12447
  const updated = await ai.files.get({ name: file.name });
12283
12448
  fileState = updated.state;
12284
12449
  logger_default.debug(`[Gemini] File state: ${fileState}`);
@@ -12322,7 +12487,7 @@ async function analyzeVideoClipDirection(videoPath, durationSeconds, model) {
12322
12487
  logger_default.info(`[Gemini] Waiting for file processing to complete...`);
12323
12488
  let fileState = file.state;
12324
12489
  while (fileState === "PROCESSING") {
12325
- await new Promise((resolve3) => setTimeout(resolve3, 2e3));
12490
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
12326
12491
  const updated = await ai.files.get({ name: file.name });
12327
12492
  fileState = updated.state;
12328
12493
  logger_default.debug(`[Gemini] File state: ${fileState}`);
@@ -12395,7 +12560,7 @@ async function analyzeVideoForEnhancements(videoPath, durationSeconds, transcrip
12395
12560
  logger_default.info(`[Gemini] Waiting for file processing to complete...`);
12396
12561
  let fileState = file.state;
12397
12562
  while (fileState === "PROCESSING") {
12398
- await new Promise((resolve3) => setTimeout(resolve3, 2e3));
12563
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
12399
12564
  const updated = await ai.files.get({ name: file.name });
12400
12565
  fileState = updated.state;
12401
12566
  logger_default.debug(`[Gemini] File state: ${fileState}`);
@@ -12506,7 +12671,7 @@ async function transcribeAudio(audioPath) {
12506
12671
  if (attempt === MAX_RETRIES) throw retryError;
12507
12672
  const msg = retryError instanceof Error ? retryError.message : String(retryError);
12508
12673
  logger_default.warn(`Whisper attempt ${attempt}/${MAX_RETRIES} failed: ${msg} \u2014 retrying in ${RETRY_DELAY_MS / 1e3}s`);
12509
- await new Promise((resolve3) => setTimeout(resolve3, RETRY_DELAY_MS));
12674
+ await new Promise((resolve4) => setTimeout(resolve4, RETRY_DELAY_MS));
12510
12675
  }
12511
12676
  }
12512
12677
  if (!response) throw new Error("Whisper transcription failed after all retries");
@@ -16065,6 +16230,7 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
16065
16230
  let mediaPath = null;
16066
16231
  let sourceClip = null;
16067
16232
  let thumbnailPath = null;
16233
+ let clipIdeaIssueNumber;
16068
16234
  if (frontmatter.shortSlug) {
16069
16235
  const short = shorts.find((s) => s.slug === frontmatter.shortSlug);
16070
16236
  const medium = mediumClips.find((m) => m.slug === frontmatter.shortSlug);
@@ -16074,12 +16240,14 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
16074
16240
  sourceClip = dirname(short.outputPath);
16075
16241
  mediaPath = resolveShortMedia(short, post.platform);
16076
16242
  thumbnailPath = short.thumbnailPath ?? null;
16243
+ clipIdeaIssueNumber = short.ideaIssueNumber;
16077
16244
  } else if (medium) {
16078
16245
  clipSlug = medium.slug;
16079
16246
  clipType = "medium-clip";
16080
16247
  sourceClip = dirname(medium.outputPath);
16081
16248
  mediaPath = resolveMediumMedia(medium, post.platform);
16082
16249
  thumbnailPath = medium.thumbnailPath ?? null;
16250
+ clipIdeaIssueNumber = medium.ideaIssueNumber;
16083
16251
  } else {
16084
16252
  clipSlug = frontmatter.shortSlug;
16085
16253
  clipType = "short";
@@ -16136,7 +16304,7 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
16136
16304
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16137
16305
  reviewedAt: null,
16138
16306
  publishedAt: null,
16139
- ideaIds: ideaIds && ideaIds.length > 0 ? ideaIds : void 0,
16307
+ ideaIds: clipIdeaIssueNumber ? [String(clipIdeaIssueNumber)] : ideaIds && ideaIds.length > 0 ? ideaIds : void 0,
16140
16308
  thumbnailPath
16141
16309
  };
16142
16310
  const stripped = stripFrontmatter(post.content);
@@ -16196,13 +16364,393 @@ function buildPublishQueue2(...args) {
16196
16364
  return buildPublishQueue(...args);
16197
16365
  }
16198
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
+
16199
16747
  // src/L4-agents/GraphicsAgent.ts
16200
16748
  init_BaseAgent();
16201
16749
  init_paths();
16202
16750
  init_fileSystem();
16203
16751
  init_configLogger();
16204
16752
  import sharp from "sharp";
16205
- 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.
16206
16754
 
16207
16755
  Your job is to make the FINAL editorial decision for each opportunity:
16208
16756
  1. Decide whether to generate an image or skip the opportunity
@@ -16285,7 +16833,7 @@ var GraphicsAgent = class extends BaseAgent {
16285
16833
  enhancementsDir = "";
16286
16834
  imageIndex = 0;
16287
16835
  constructor(model) {
16288
- super("GraphicsAgent", SYSTEM_PROMPT6, void 0, model);
16836
+ super("GraphicsAgent", SYSTEM_PROMPT7, void 0, model);
16289
16837
  }
16290
16838
  resetForRetry() {
16291
16839
  this.overlays = [];
@@ -16450,6 +16998,8 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
16450
16998
  slug;
16451
16999
  /** Content ideas linked to this video for editorial direction */
16452
17000
  _ideas = [];
17001
+ /** Per-clip idea assignments from idea discovery (clipId → ideaIssueNumber) */
17002
+ _clipIdeaMap = /* @__PURE__ */ new Map();
16453
17003
  /** Set ideas for editorial direction */
16454
17004
  setIdeas(ideas) {
16455
17005
  this._ideas = ideas;
@@ -16458,6 +17008,10 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
16458
17008
  get ideas() {
16459
17009
  return this._ideas;
16460
17010
  }
17011
+ /** Get per-clip idea assignments */
17012
+ get clipIdeaMap() {
17013
+ return this._clipIdeaMap;
17014
+ }
16461
17015
  constructor(sourcePath, videoDir, slug) {
16462
17016
  super();
16463
17017
  this.sourcePath = sourcePath;
@@ -16618,12 +17172,12 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
16618
17172
  await transcodeToMp42(sourcePath, destPath);
16619
17173
  logger_default.info(`Transcoded video to ${destPath}`);
16620
17174
  } else {
16621
- await new Promise((resolve3, reject) => {
17175
+ await new Promise((resolve4, reject) => {
16622
17176
  const readStream = openReadStream(sourcePath);
16623
17177
  const writeStream = openWriteStream(destPath);
16624
17178
  readStream.on("error", reject);
16625
17179
  writeStream.on("error", reject);
16626
- writeStream.on("finish", resolve3);
17180
+ writeStream.on("finish", resolve4);
16627
17181
  readStream.pipe(writeStream);
16628
17182
  });
16629
17183
  logger_default.info(`Copied video to ${destPath}`);
@@ -17377,6 +17931,47 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
17377
17931
  }
17378
17932
  return posts;
17379
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
+ }
17380
17975
  /**
17381
17976
  * Build the publish queue via the queue builder service.
17382
17977
  */
@@ -17489,33 +18084,20 @@ async function buildRealignPlan(options = {}) {
17489
18084
  tagged.push({ post, platform, clipType });
17490
18085
  }
17491
18086
  const bookedMap = await buildBookedMap();
17492
- const ctx = {
17493
- timezone,
17494
- bookedMap,
17495
- ideaLinkedPostIds: /* @__PURE__ */ new Set(),
17496
- lateClient: client,
17497
- displacementEnabled: getDisplacementConfig().enabled,
17498
- dryRun: true,
17499
- depth: 0,
17500
- ideaRefs: [],
17501
- samePlatformMs: 0,
17502
- crossPlatformMs: 0,
17503
- platform: ""
17504
- };
18087
+ const ideaLinkedPostIds = /* @__PURE__ */ new Set();
17505
18088
  for (const [, slot] of bookedMap) {
17506
18089
  if (slot.ideaLinked && slot.postId) {
17507
- ctx.ideaLinkedPostIds.add(slot.postId);
18090
+ ideaLinkedPostIds.add(slot.postId);
17508
18091
  }
17509
18092
  }
17510
18093
  const result = [];
17511
18094
  const toCancel = [];
17512
18095
  let skipped = 0;
17513
18096
  tagged.sort((a, b) => {
17514
- const aIdea = ctx.ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
17515
- 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;
17516
18099
  return aIdea - bIdea;
17517
18100
  });
17518
- const nowMs = Date.now();
17519
18101
  for (const { post, platform, clipType } of tagged) {
17520
18102
  const schedulePlatform = normalizeSchedulePlatform(platform);
17521
18103
  const platformConfig = getPlatformSchedule(schedulePlatform, clipType);
@@ -17536,9 +18118,13 @@ async function buildRealignPlan(options = {}) {
17536
18118
  bookedMap.delete(currentMs2);
17537
18119
  }
17538
18120
  }
17539
- const isIdea = ctx.ideaLinkedPostIds.has(post._id);
17540
- const label = `${schedulePlatform}/${clipType}:${post._id.slice(-6)}`;
17541
- 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
+ });
17542
18128
  if (!newSlot) {
17543
18129
  if (post.status !== "cancelled") {
17544
18130
  toCancel.push({ post, platform, clipType, reason: `No available slot for ${schedulePlatform}/${clipType}` });
@@ -17546,7 +18132,7 @@ async function buildRealignPlan(options = {}) {
17546
18132
  continue;
17547
18133
  }
17548
18134
  const newMs = new Date(newSlot).getTime();
17549
- ctx.bookedMap.set(newMs, {
18135
+ bookedMap.set(newMs, {
17550
18136
  scheduledFor: newSlot,
17551
18137
  source: "late",
17552
18138
  postId: post._id,
@@ -17626,7 +18212,7 @@ var TOOL_LABELS = {
17626
18212
  check_realign_status: "\u{1F4CA} Checking realignment progress",
17627
18213
  ask_user: "\u{1F4AC} Asking for your input"
17628
18214
  };
17629
- 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.
17630
18216
 
17631
18217
  You help the user view, analyze, and reprioritize their posting schedule across platforms.
17632
18218
 
@@ -17649,7 +18235,7 @@ var ScheduleAgent = class extends BaseAgent {
17649
18235
  chatOutput;
17650
18236
  realignJobs = /* @__PURE__ */ new Map();
17651
18237
  constructor(userInputHandler, model) {
17652
- super("ScheduleAgent", SYSTEM_PROMPT7, void 0, model);
18238
+ super("ScheduleAgent", SYSTEM_PROMPT8, void 0, model);
17653
18239
  this.userInputHandler = userInputHandler;
17654
18240
  }
17655
18241
  /** Set a callback for chat-friendly status messages (tool starts, progress). */
@@ -18250,7 +18836,7 @@ function buildSystemPrompt4(brand, existingIdeas, seedTopics, count, ideaRepo) {
18250
18836
  }
18251
18837
  return promptSections.join("\n");
18252
18838
  }
18253
- function buildUserMessage(count, seedTopics, hasMcpServers, userPrompt) {
18839
+ function buildUserMessage2(count, seedTopics, hasMcpServers, userPrompt) {
18254
18840
  const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
18255
18841
  const steps = [
18256
18842
  "1. Call get_brand_context to load the creator profile.",
@@ -18799,7 +19385,7 @@ async function generateIdeas(options = {}) {
18799
19385
  });
18800
19386
  try {
18801
19387
  const hasMcpServers = !!(config2.EXA_API_KEY || config2.YOUTUBE_API_KEY || config2.PERPLEXITY_API_KEY);
18802
- const userMessage = buildUserMessage(count, seedTopics, hasMcpServers, options.prompt);
19388
+ const userMessage = buildUserMessage2(count, seedTopics, hasMcpServers, options.prompt);
18803
19389
  await agent.run(userMessage);
18804
19390
  const ideas = agent.getGeneratedIdeas();
18805
19391
  if (!agent.isFinalized()) {
@@ -18812,8 +19398,644 @@ async function generateIdeas(options = {}) {
18812
19398
  }
18813
19399
  }
18814
19400
 
18815
- // src/L5-assets/pipelineServices.ts
18816
- var costTracker3 = {
19401
+ // src/L4-agents/InterviewAgent.ts
19402
+ init_BaseAgent();
19403
+
19404
+ // src/L1-infra/progress/interviewEmitter.ts
19405
+ var InterviewEmitter = class {
19406
+ enabled = false;
19407
+ listeners = /* @__PURE__ */ new Set();
19408
+ /** Turn on interview event output to stderr. */
19409
+ enable() {
19410
+ this.enabled = true;
19411
+ }
19412
+ /** Turn off interview event output. */
19413
+ disable() {
19414
+ this.enabled = false;
19415
+ }
19416
+ /** Whether the emitter is currently active (stderr or listeners). */
19417
+ isEnabled() {
19418
+ return this.enabled || this.listeners.size > 0;
19419
+ }
19420
+ /** Register a programmatic listener for interview events. */
19421
+ addListener(fn) {
19422
+ this.listeners.add(fn);
19423
+ }
19424
+ /** Remove a previously registered listener. */
19425
+ removeListener(fn) {
19426
+ this.listeners.delete(fn);
19427
+ }
19428
+ /**
19429
+ * Write an interview event as a single JSON line to stderr (if enabled)
19430
+ * and dispatch to all registered listeners.
19431
+ * No-op when neither stderr output nor listeners are active.
19432
+ */
19433
+ emit(event) {
19434
+ if (!this.enabled && this.listeners.size === 0) return;
19435
+ if (this.enabled) {
19436
+ process.stderr.write(JSON.stringify(event) + "\n");
19437
+ }
19438
+ for (const listener of this.listeners) {
19439
+ listener(event);
19440
+ }
19441
+ }
19442
+ };
19443
+ var interviewEmitter = new InterviewEmitter();
19444
+
19445
+ // src/L4-agents/InterviewAgent.ts
19446
+ init_configLogger();
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).
19448
+
19449
+ ## Rules
19450
+ - Every question must be a SINGLE sentence. No multi-part questions. No preamble. No encouragement filler.
19451
+ - Build on the previous answer \u2014 reference what the user said.
19452
+ - Push on weak spots: vague audience, generic hooks, surface-level talking points.
19453
+ - If the user responds with "/end", call end_interview immediately.
19454
+
19455
+ ## Focus (pick one per question)
19456
+ - Problem clarity \u2014 what specific pain does this solve?
19457
+ - Audience \u2014 who exactly, what skill level?
19458
+ - Key takeaway \u2014 what's the ONE thing to remember?
19459
+ - Hook \u2014 would you click this? Be specific.
19460
+ - Talking points \u2014 substantive or surface-level?
19461
+ - Trend relevance \u2014 why now?
19462
+
19463
+ ## Tools
19464
+ - ask_question: EVERY question goes through this tool. Include a 1-sentence rationale and the target field.
19465
+ - update_field: When the conversation reveals a better value for a field, DIRECTLY SET the new value. For scalar fields (hook, audience, keyTakeaway, trendContext), provide the complete replacement text. For array fields (talkingPoints), provide the FULL updated list \u2014 not just the new item. Write the actual content, not a description of the change.
19466
+ - end_interview: After 5\u201310 productive questions, wrap up with a brief summary.
19467
+ - NEVER output text outside of tool calls.`;
19468
+ var InterviewAgent = class extends BaseAgent {
19469
+ answerProvider = null;
19470
+ transcript = [];
19471
+ insights = {};
19472
+ questionNumber = 0;
19473
+ ended = false;
19474
+ idea = null;
19475
+ constructor(model) {
19476
+ super("InterviewAgent", SYSTEM_PROMPT9, void 0, model);
19477
+ }
19478
+ getTimeoutMs() {
19479
+ return 0;
19480
+ }
19481
+ resetForRetry() {
19482
+ this.transcript = [];
19483
+ this.insights = {};
19484
+ this.questionNumber = 0;
19485
+ this.ended = false;
19486
+ }
19487
+ getTools() {
19488
+ return [
19489
+ {
19490
+ name: "ask_question",
19491
+ description: "Ask the user a single Socratic question to explore and develop the idea. This is the primary way you communicate \u2014 every question MUST go through this tool.",
19492
+ parameters: {
19493
+ type: "object",
19494
+ properties: {
19495
+ question: {
19496
+ type: "string",
19497
+ description: "The question to ask the user. Must be a single, focused question."
19498
+ },
19499
+ rationale: {
19500
+ type: "string",
19501
+ description: "Why you are asking this question \u2014 what gap or opportunity it explores."
19502
+ },
19503
+ targetField: {
19504
+ type: "string",
19505
+ description: "Which idea field this question explores (e.g. hook, audience, keyTakeaway, talkingPoints, trendContext).",
19506
+ enum: ["topic", "hook", "audience", "keyTakeaway", "talkingPoints", "platforms", "tags", "publishBy", "trendContext"]
19507
+ }
19508
+ },
19509
+ required: ["question", "rationale"],
19510
+ additionalProperties: false
19511
+ },
19512
+ handler: async (args) => this.handleToolCall("ask_question", args)
19513
+ },
19514
+ {
19515
+ name: "update_field",
19516
+ description: "Directly update an idea field with new content discovered during the interview. For scalar fields, provide the complete replacement text. For talkingPoints, provide the FULL updated list (all points, not just new ones).",
19517
+ parameters: {
19518
+ type: "object",
19519
+ properties: {
19520
+ field: {
19521
+ type: "string",
19522
+ description: "Which idea field to update.",
19523
+ enum: ["topic", "hook", "audience", "keyTakeaway", "talkingPoints", "tags", "trendContext"]
19524
+ },
19525
+ value: {
19526
+ type: "string",
19527
+ description: "The new value for scalar fields (hook, audience, keyTakeaway, trendContext, topic)."
19528
+ },
19529
+ values: {
19530
+ type: "array",
19531
+ items: { type: "string" },
19532
+ description: "The full updated list for array fields (talkingPoints, tags). Include ALL items, not just new ones."
19533
+ }
19534
+ },
19535
+ required: ["field"],
19536
+ additionalProperties: false
19537
+ },
19538
+ handler: async (args) => this.handleToolCall("update_field", args)
19539
+ },
19540
+ {
19541
+ name: "end_interview",
19542
+ description: "Signal that the interview is complete. Use when you have gathered sufficient insights (typically after 5\u201310 questions) to meaningfully improve the idea.",
19543
+ parameters: {
19544
+ type: "object",
19545
+ properties: {
19546
+ summary: {
19547
+ type: "string",
19548
+ description: "A summary of what was learned and how the idea has been refined."
19549
+ }
19550
+ },
19551
+ required: ["summary"],
19552
+ additionalProperties: false
19553
+ },
19554
+ handler: async (args) => this.handleToolCall("end_interview", args)
19555
+ }
19556
+ ];
19557
+ }
19558
+ async handleToolCall(toolName, args) {
19559
+ switch (toolName) {
19560
+ case "ask_question":
19561
+ return this.handleAskQuestion(args);
19562
+ case "update_field":
19563
+ return this.handleUpdateField(args);
19564
+ case "end_interview":
19565
+ return this.handleEndInterview(args);
19566
+ default:
19567
+ return { error: `Unknown tool: ${toolName}` };
19568
+ }
19569
+ }
19570
+ async handleAskQuestion(args) {
19571
+ const question = String(args.question ?? "");
19572
+ const rationale = String(args.rationale ?? "");
19573
+ const targetField = args.targetField;
19574
+ this.questionNumber++;
19575
+ const context = {
19576
+ rationale,
19577
+ targetField,
19578
+ questionNumber: this.questionNumber
19579
+ };
19580
+ interviewEmitter.emit({
19581
+ event: "question:asked",
19582
+ question,
19583
+ context,
19584
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19585
+ });
19586
+ logger_default.info(`[InterviewAgent] Q${this.questionNumber}: ${question}`);
19587
+ return this.waitForAnswer(question, context);
19588
+ }
19589
+ handleUpdateField(args) {
19590
+ const field = String(args.field ?? "");
19591
+ if (field === "talkingPoints" || field === "tags") {
19592
+ const values = args.values;
19593
+ if (values && values.length > 0) {
19594
+ this.insights[field] = values;
19595
+ }
19596
+ } else {
19597
+ const value = String(args.value ?? "");
19598
+ if (value) {
19599
+ this.insights[field] = value;
19600
+ }
19601
+ }
19602
+ const displayValue = field === "talkingPoints" || field === "tags" ? `[${(args.values ?? []).length} items]` : String(args.value ?? "").slice(0, 60);
19603
+ interviewEmitter.emit({
19604
+ event: "insight:discovered",
19605
+ insight: displayValue,
19606
+ field,
19607
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19608
+ });
19609
+ logger_default.info(`[InterviewAgent] Updated [${field}]: ${displayValue}`);
19610
+ return { updated: true, field };
19611
+ }
19612
+ handleEndInterview(args) {
19613
+ const summary = String(args.summary ?? "");
19614
+ this.ended = true;
19615
+ logger_default.info(`[InterviewAgent] Interview ended: ${summary}`);
19616
+ return { ended: true, summary };
19617
+ }
19618
+ async waitForAnswer(question, context) {
19619
+ if (!this.answerProvider) {
19620
+ throw new Error("No answer provider configured \u2014 cannot ask questions");
19621
+ }
19622
+ const askedAt = (/* @__PURE__ */ new Date()).toISOString();
19623
+ const answer = await this.answerProvider(question, context);
19624
+ const answeredAt = (/* @__PURE__ */ new Date()).toISOString();
19625
+ const pair = {
19626
+ question,
19627
+ answer,
19628
+ askedAt,
19629
+ answeredAt,
19630
+ questionNumber: context.questionNumber
19631
+ };
19632
+ this.transcript.push(pair);
19633
+ interviewEmitter.emit({
19634
+ event: "answer:received",
19635
+ questionNumber: context.questionNumber,
19636
+ answer,
19637
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19638
+ });
19639
+ return answer;
19640
+ }
19641
+ /**
19642
+ * Run a Socratic interview session for the given idea.
19643
+ *
19644
+ * The agent uses `ask_question` tool calls to present questions one at a time.
19645
+ * Each question is routed through the `answerProvider` callback, which the caller
19646
+ * implements to show the question to the user and collect their response.
19647
+ */
19648
+ async runInterview(idea, answerProvider) {
19649
+ this.idea = idea;
19650
+ this.answerProvider = answerProvider;
19651
+ this.transcript = [];
19652
+ this.insights = {};
19653
+ this.questionNumber = 0;
19654
+ this.ended = false;
19655
+ const startTime = Date.now();
19656
+ interviewEmitter.emit({
19657
+ event: "interview:start",
19658
+ ideaNumber: idea.issueNumber,
19659
+ mode: "interview",
19660
+ ideaTopic: idea.topic,
19661
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19662
+ });
19663
+ const contextMessage = this.buildIdeaContext(idea);
19664
+ try {
19665
+ await this.run(contextMessage);
19666
+ const result = {
19667
+ ideaNumber: idea.issueNumber,
19668
+ transcript: this.transcript,
19669
+ insights: this.insights,
19670
+ updatedFields: this.getUpdatedFields(),
19671
+ durationMs: Date.now() - startTime,
19672
+ endedBy: this.ended ? "agent" : "user"
19673
+ };
19674
+ interviewEmitter.emit({
19675
+ event: "interview:complete",
19676
+ result,
19677
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19678
+ });
19679
+ return result;
19680
+ } catch (error) {
19681
+ const message = error instanceof Error ? error.message : String(error);
19682
+ interviewEmitter.emit({
19683
+ event: "interview:error",
19684
+ error: message,
19685
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19686
+ });
19687
+ throw error;
19688
+ }
19689
+ }
19690
+ buildIdeaContext(idea) {
19691
+ const talkingPoints = idea.talkingPoints.length > 0 ? idea.talkingPoints.map((p) => `- ${p}`).join("\n") : "- (none yet)";
19692
+ return [
19693
+ "Here is the idea to explore through Socratic questioning:",
19694
+ "",
19695
+ `**Topic:** ${idea.topic}`,
19696
+ `**Hook:** ${idea.hook}`,
19697
+ `**Audience:** ${idea.audience}`,
19698
+ `**Key Takeaway:** ${idea.keyTakeaway}`,
19699
+ "**Talking Points:**",
19700
+ talkingPoints,
19701
+ `**Publish By:** ${idea.publishBy}`,
19702
+ `**Trend Context:** ${idea.trendContext ?? "Not specified"}`,
19703
+ "",
19704
+ "Begin by asking your first Socratic question to explore and develop this idea."
19705
+ ].join("\n");
19706
+ }
19707
+ getUpdatedFields() {
19708
+ const fields = [];
19709
+ if (this.insights.talkingPoints !== void 0) fields.push("talkingPoints");
19710
+ if (this.insights.keyTakeaway !== void 0) fields.push("keyTakeaway");
19711
+ if (this.insights.hook !== void 0) fields.push("hook");
19712
+ if (this.insights.audience !== void 0) fields.push("audience");
19713
+ if (this.insights.trendContext !== void 0) fields.push("trendContext");
19714
+ if (this.insights.tags !== void 0) fields.push("tags");
19715
+ return fields;
19716
+ }
19717
+ setupEventHandlers(session) {
19718
+ session.on("delta", () => {
19719
+ });
19720
+ session.on("tool_start", (event) => {
19721
+ const toolName = event.data?.name ?? "unknown";
19722
+ if (toolName !== "ask_question") {
19723
+ interviewEmitter.emit({
19724
+ event: "tool:start",
19725
+ toolName,
19726
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19727
+ });
19728
+ }
19729
+ });
19730
+ session.on("tool_end", (event) => {
19731
+ const toolName = event.data?.name ?? "unknown";
19732
+ if (toolName !== "ask_question") {
19733
+ interviewEmitter.emit({
19734
+ event: "tool:end",
19735
+ toolName,
19736
+ durationMs: 0,
19737
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19738
+ });
19739
+ }
19740
+ });
19741
+ session.on("error", (event) => {
19742
+ logger_default.error(`[InterviewAgent] error: ${JSON.stringify(event.data)}`);
19743
+ });
19744
+ }
19745
+ };
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
+
20037
+ // src/L5-assets/pipelineServices.ts
20038
+ var costTracker3 = {
18817
20039
  reset: (...args) => costTracker2.reset(...args),
18818
20040
  setStage: (...args) => costTracker2.setStage(...args),
18819
20041
  getReport: (...args) => costTracker2.getReport(...args),
@@ -18835,9 +20057,18 @@ function markFailed3(...args) {
18835
20057
  function generateIdeas2(...args) {
18836
20058
  return generateIdeas(...args);
18837
20059
  }
20060
+ function createInterviewAgent(...args) {
20061
+ return new InterviewAgent(...args);
20062
+ }
18838
20063
  function createScheduleAgent(...args) {
18839
20064
  return new ScheduleAgent(...args);
18840
20065
  }
20066
+ function createAgendaAgent(...args) {
20067
+ return new AgendaAgent(...args);
20068
+ }
20069
+ function createIdeaDiscoveryAgent(...args) {
20070
+ return new IdeaDiscoveryAgent(...args);
20071
+ }
18841
20072
 
18842
20073
  // src/L6-pipeline/pipeline.ts
18843
20074
  init_types();
@@ -18894,7 +20125,7 @@ async function runStage(stageName, fn, stageResults) {
18894
20125
  return void 0;
18895
20126
  }
18896
20127
  }
18897
- async function processVideo(videoPath, ideas) {
20128
+ async function processVideo(videoPath, ideas, publishBy) {
18898
20129
  const pipelineStart = Date.now();
18899
20130
  const stageResults = [];
18900
20131
  const cfg = getConfig();
@@ -19061,6 +20292,15 @@ async function processVideo(videoPath, ideas) {
19061
20292
  } catch (err) {
19062
20293
  logger_default.warn(`[Pipeline] Failed to generate main video thumbnail: ${err instanceof Error ? err.message : String(err)}`);
19063
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
+ }
19064
20304
  let socialPosts = [];
19065
20305
  if (!cfg.SKIP_SOCIAL) {
19066
20306
  const mainPosts = await trackStage("social-media" /* SocialMedia */, () => asset.getSocialPosts()) ?? [];
@@ -19185,13 +20425,13 @@ function generateCostMarkdown(report) {
19185
20425
  }
19186
20426
  return md;
19187
20427
  }
19188
- async function processVideoSafe(videoPath, ideas) {
20428
+ async function processVideoSafe(videoPath, ideas, publishBy) {
19189
20429
  const filename = basename(videoPath);
19190
20430
  const slug = filename.replace(/\.(mp4|mov|webm|avi|mkv)$/i, "");
19191
20431
  await markPending3(slug, videoPath);
19192
20432
  await markProcessing3(slug);
19193
20433
  try {
19194
- const result = await processVideo(videoPath, ideas);
20434
+ const result = await processVideo(videoPath, ideas, publishBy);
19195
20435
  await markCompleted3(slug);
19196
20436
  return result;
19197
20437
  } catch (err) {
@@ -19497,8 +20737,8 @@ function getFFprobePath3(...args) {
19497
20737
  init_scheduleConfig();
19498
20738
  var rl = createReadlineInterface({ input: process.stdin, output: process.stdout });
19499
20739
  function ask(question) {
19500
- return new Promise((resolve3) => {
19501
- rl.question(question, (answer) => resolve3(answer));
20740
+ return new Promise((resolve4) => {
20741
+ rl.question(question, (answer) => resolve4(answer));
19502
20742
  });
19503
20743
  }
19504
20744
  async function runInit() {
@@ -19789,15 +21029,225 @@ async function runRealign(options = {}) {
19789
21029
  init_environment();
19790
21030
  init_configLogger();
19791
21031
 
19792
- // src/L1-infra/readline/readline.ts
19793
- import { createInterface } from "readline";
19794
- function createChatInterface(options) {
19795
- return createInterface({
19796
- input: options?.input ?? process.stdin,
19797
- output: options?.output ?? process.stdout,
19798
- terminal: false
21032
+ // src/L1-infra/terminal/altScreenChat.tsx
21033
+ import { useState, useCallback, useEffect } from "react";
21034
+ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
21035
+ import TextInput from "ink-text-input";
21036
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21037
+ function Header({ title, subtitle }) {
21038
+ const { stdout } = useStdout();
21039
+ const cols = stdout?.columns ?? 80;
21040
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: cols, children: [
21041
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color: "white", bold: true, children: ` \u{1F4DD} ${title}`.padEnd(cols) }) }),
21042
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color: "white", dimColor: true, children: ` ${subtitle}`.padEnd(cols) }) }),
21043
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) })
21044
+ ] });
21045
+ }
21046
+ var FIELD_EMOJI = {
21047
+ topic: "\u{1F4CC} topic",
21048
+ hook: "\u{1FA9D} hook",
21049
+ audience: "\u{1F3AF} audience",
21050
+ keyTakeaway: "\u{1F48E} takeaway",
21051
+ talkingPoints: "\u{1F4CB} talking points",
21052
+ platforms: "\u{1F4F1} platforms",
21053
+ tags: "\u{1F3F7}\uFE0F tags",
21054
+ publishBy: "\u{1F4C5} deadline",
21055
+ trendContext: "\u{1F525} trend"
21056
+ };
21057
+ function QuestionCardView({ card, latestInsight, statusText }) {
21058
+ if (!card) {
21059
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: statusText || "Preparing first question..." }) });
21060
+ }
21061
+ const fieldLabel = FIELD_EMOJI[card.targetField] ?? `\u{1F4CE} ${card.targetField}`;
21062
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 2, children: [
21063
+ /* @__PURE__ */ jsx(Text, { children: " " }),
21064
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
21065
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
21066
+ "Question ",
21067
+ card.questionNumber
21068
+ ] }),
21069
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: fieldLabel })
21070
+ ] }),
21071
+ /* @__PURE__ */ jsx(Text, { children: " " }),
21072
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, wrap: "wrap", children: card.question }),
21073
+ /* @__PURE__ */ jsx(Text, { children: " " }),
21074
+ /* @__PURE__ */ jsxs(Box, { children: [
21075
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u{1F4AD} " }),
21076
+ /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: card.rationale })
21077
+ ] }),
21078
+ latestInsight && /* @__PURE__ */ jsxs(Fragment, { children: [
21079
+ /* @__PURE__ */ jsx(Text, { children: " " }),
21080
+ /* @__PURE__ */ jsxs(Box, { children: [
21081
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u{1F4A1} " }),
21082
+ /* @__PURE__ */ jsx(Text, { color: "yellow", wrap: "wrap", children: latestInsight })
21083
+ ] })
21084
+ ] }),
21085
+ /* @__PURE__ */ jsx(Text, { children: " " }),
21086
+ statusText && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: statusText }) })
21087
+ ] });
21088
+ }
21089
+ function InputLine({ prompt, value, onChange, onSubmit, active }) {
21090
+ const { stdout } = useStdout();
21091
+ const cols = stdout?.columns ?? 80;
21092
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
21093
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) }),
21094
+ /* @__PURE__ */ jsxs(Box, { children: [
21095
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: prompt }),
21096
+ active ? /* @__PURE__ */ jsx(TextInput, { value, onChange, onSubmit }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: " waiting..." })
21097
+ ] })
21098
+ ] });
21099
+ }
21100
+ function ChatApp({ controller }) {
21101
+ const { exit } = useApp();
21102
+ const [card, setCard] = useState(null);
21103
+ const [latestInsight, setLatestInsight] = useState(null);
21104
+ const [statusText, setStatusText] = useState("");
21105
+ const [inputValue, setInputValue] = useState("");
21106
+ const [inputActive, setInputActive] = useState(false);
21107
+ const [, setTick] = useState(0);
21108
+ useEffect(() => {
21109
+ controller._wire({
21110
+ setCard,
21111
+ setLatestInsight,
21112
+ setStatusText,
21113
+ setInputActive,
21114
+ exit,
21115
+ forceRender: () => setTick((t) => t + 1)
21116
+ });
21117
+ return () => controller._unwire();
21118
+ }, [controller, exit]);
21119
+ useInput((_input, key) => {
21120
+ if (key.ctrl && _input === "c") {
21121
+ controller.interrupted = true;
21122
+ const resolve4 = controller._pendingResolve;
21123
+ controller._pendingResolve = null;
21124
+ if (resolve4) resolve4("");
21125
+ exit();
21126
+ }
19799
21127
  });
19800
- }
21128
+ const handleSubmit = useCallback((value) => {
21129
+ setInputValue("");
21130
+ setInputActive(false);
21131
+ const resolve4 = controller._pendingResolve;
21132
+ controller._pendingResolve = null;
21133
+ if (resolve4) resolve4(value.trim());
21134
+ }, [controller]);
21135
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: "100%", children: [
21136
+ /* @__PURE__ */ jsx(
21137
+ Header,
21138
+ {
21139
+ title: controller.title,
21140
+ subtitle: controller.subtitle
21141
+ }
21142
+ ),
21143
+ /* @__PURE__ */ jsx(
21144
+ QuestionCardView,
21145
+ {
21146
+ card,
21147
+ latestInsight,
21148
+ statusText
21149
+ }
21150
+ ),
21151
+ /* @__PURE__ */ jsx(
21152
+ InputLine,
21153
+ {
21154
+ prompt: controller.inputPrompt,
21155
+ value: inputValue,
21156
+ onChange: setInputValue,
21157
+ onSubmit: handleSubmit,
21158
+ active: inputActive
21159
+ }
21160
+ )
21161
+ ] });
21162
+ }
21163
+ var AltScreenChat = class {
21164
+ title;
21165
+ subtitle;
21166
+ inputPrompt;
21167
+ maxScrollback;
21168
+ messages = [];
21169
+ bridge = null;
21170
+ inkInstance = null;
21171
+ /** Set to true when Ctrl+C is pressed. Callers should check this after promptInput(). */
21172
+ interrupted = false;
21173
+ /** @internal */
21174
+ _pendingResolve = null;
21175
+ constructor(options) {
21176
+ this.title = options.title;
21177
+ this.subtitle = options.subtitle ?? "Type /end to finish, Ctrl+C to quit";
21178
+ this.inputPrompt = options.inputPrompt ?? "> ";
21179
+ this.maxScrollback = options.maxScrollback ?? 500;
21180
+ }
21181
+ /** @internal */
21182
+ _wire(bridge) {
21183
+ this.bridge = bridge;
21184
+ }
21185
+ /** @internal */
21186
+ _unwire() {
21187
+ this.bridge = null;
21188
+ }
21189
+ /** Enter fullscreen and render the Ink UI. */
21190
+ enter() {
21191
+ this.inkInstance = render(
21192
+ /* @__PURE__ */ jsx(ChatApp, { controller: this }),
21193
+ { exitOnCtrlC: false }
21194
+ );
21195
+ }
21196
+ /** Leave fullscreen and clean up Ink. */
21197
+ leave() {
21198
+ if (this.inkInstance) {
21199
+ this.inkInstance.unmount();
21200
+ this.inkInstance = null;
21201
+ }
21202
+ }
21203
+ /** Clean up everything. */
21204
+ destroy() {
21205
+ this.leave();
21206
+ this.messages = [];
21207
+ this.bridge = null;
21208
+ this._pendingResolve = null;
21209
+ }
21210
+ /**
21211
+ * Show a focused question card. Replaces the entire display content
21212
+ * with this one question — no scrolling chat history.
21213
+ */
21214
+ showQuestion(question, rationale, targetField, questionNumber) {
21215
+ this.bridge?.setCard({ question, rationale, targetField, questionNumber });
21216
+ }
21217
+ /**
21218
+ * Show a discovered insight on the current card.
21219
+ */
21220
+ showInsight(text) {
21221
+ this.bridge?.setLatestInsight(text);
21222
+ }
21223
+ /**
21224
+ * Add a message to the internal log (for transcript purposes).
21225
+ * Does NOT affect the card display — use showQuestion for that.
21226
+ */
21227
+ addMessage(role, content) {
21228
+ const msg = { role, content, timestamp: /* @__PURE__ */ new Date() };
21229
+ this.messages.push(msg);
21230
+ if (this.messages.length > this.maxScrollback) {
21231
+ this.messages = this.messages.slice(-this.maxScrollback);
21232
+ }
21233
+ }
21234
+ /** Set the status bar text. */
21235
+ setStatus(text) {
21236
+ this.bridge?.setStatusText(text);
21237
+ }
21238
+ /** Clear the status bar. */
21239
+ clearStatus() {
21240
+ this.bridge?.setStatusText("");
21241
+ }
21242
+ /** Prompt for user input. Returns their trimmed text. */
21243
+ promptInput(_prompt) {
21244
+ return new Promise((resolve4) => {
21245
+ this._pendingResolve = resolve4;
21246
+ this.bridge?.setInputActive(true);
21247
+ this.bridge?.forceRender();
21248
+ });
21249
+ }
21250
+ };
19801
21251
 
19802
21252
  // src/L6-pipeline/scheduleChat.ts
19803
21253
  function createScheduleAgent2(...args) {
@@ -19808,89 +21258,64 @@ function createScheduleAgent2(...args) {
19808
21258
  async function runChat() {
19809
21259
  initConfig();
19810
21260
  setChatMode(true);
19811
- const rl2 = createChatInterface();
21261
+ const chat = new AltScreenChat({
21262
+ title: "\u{1F4AC} VidPipe Chat",
21263
+ subtitle: "Schedule management assistant. Type exit or quit to leave.",
21264
+ inputPrompt: "vidpipe> "
21265
+ });
19812
21266
  const handleUserInput = (request) => {
19813
- return new Promise((resolve3) => {
19814
- console.log();
19815
- console.log(`\x1B[33m\u{1F916} Agent asks:\x1B[0m ${request.question}`);
19816
- if (request.choices && request.choices.length > 0) {
19817
- for (let i = 0; i < request.choices.length; i++) {
19818
- console.log(` ${i + 1}. ${request.choices[i]}`);
19819
- }
19820
- if (request.allowFreeform !== false) {
19821
- console.log(` (or type a custom answer)`);
19822
- }
19823
- }
19824
- rl2.question("\x1B[33m> \x1B[0m", (answer) => {
21267
+ chat.addMessage("agent", request.question);
21268
+ if (request.choices && request.choices.length > 0) {
21269
+ const choiceText = request.choices.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
21270
+ chat.addMessage("system", choiceText + (request.allowFreeform !== false ? "\n (or type a custom answer)" : ""));
21271
+ }
21272
+ return new Promise((resolve4) => {
21273
+ chat.promptInput("> ").then((answer) => {
19825
21274
  const trimmed = answer.trim();
21275
+ chat.addMessage("user", trimmed);
19826
21276
  if (request.choices && request.choices.length > 0) {
19827
21277
  const num = parseInt(trimmed, 10);
19828
21278
  if (num >= 1 && num <= request.choices.length) {
19829
- resolve3({ answer: request.choices[num - 1], wasFreeform: false });
21279
+ resolve4({ answer: request.choices[num - 1], wasFreeform: false });
19830
21280
  return;
19831
21281
  }
19832
21282
  }
19833
- resolve3({ answer: trimmed, wasFreeform: true });
21283
+ resolve4({ answer: trimmed, wasFreeform: true });
19834
21284
  });
19835
21285
  });
19836
21286
  };
19837
21287
  const agent = createScheduleAgent2(handleUserInput);
19838
21288
  agent.setChatOutput((message) => {
19839
- process.stderr.write(`${message}
19840
- `);
19841
- });
19842
- console.log(`
19843
- \x1B[36m\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
19844
- \u2551 VidPipe Chat \u2551
19845
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\x1B[0m
19846
-
19847
- Schedule management assistant. Ask me about your posting schedule,
19848
- reschedule posts, check what's coming up, or reprioritize content.
19849
-
19850
- Type \x1B[33mexit\x1B[0m or \x1B[33mquit\x1B[0m to leave. Press Ctrl+C to stop.
19851
- `);
19852
- let closeRejector = null;
19853
- const closePromise = new Promise((_, reject) => {
19854
- closeRejector = reject;
19855
- rl2.once("close", () => reject(new Error("readline closed")));
21289
+ chat.setStatus(message);
19856
21290
  });
19857
- const prompt = () => {
19858
- return Promise.race([
19859
- new Promise((resolve3) => {
19860
- rl2.question("\x1B[32mvidpipe>\x1B[0m ", (answer) => {
19861
- resolve3(answer);
19862
- });
19863
- }),
19864
- closePromise
19865
- ]);
19866
- };
21291
+ chat.enter();
21292
+ chat.addMessage("system", "Ask me about your posting schedule, reschedule posts, check what's coming up, or reprioritize content.");
19867
21293
  try {
19868
21294
  while (true) {
19869
- let input;
19870
- try {
19871
- input = await prompt();
19872
- } catch {
19873
- break;
19874
- }
21295
+ const input = await chat.promptInput();
19875
21296
  const trimmed = input.trim();
19876
21297
  if (!trimmed) continue;
19877
21298
  if (trimmed === "exit" || trimmed === "quit") {
19878
- console.log("\nGoodbye! \u{1F44B}");
21299
+ chat.addMessage("system", "Goodbye! \u{1F44B}");
19879
21300
  break;
19880
21301
  }
21302
+ chat.addMessage("user", trimmed);
21303
+ chat.setStatus("\u{1F914} Thinking...");
19881
21304
  try {
19882
- await agent.run(trimmed);
19883
- console.log("\n");
21305
+ const response = await agent.run(trimmed);
21306
+ chat.clearStatus();
21307
+ if (response) {
21308
+ chat.addMessage("agent", response);
21309
+ }
19884
21310
  } catch (err) {
21311
+ chat.clearStatus();
19885
21312
  const message = err instanceof Error ? err.message : String(err);
19886
- console.error(`
19887
- \x1B[31mError: ${message}\x1B[0m
19888
- `);
21313
+ chat.addMessage("error", message);
19889
21314
  }
19890
21315
  }
19891
21316
  } finally {
19892
21317
  await agent.destroy();
19893
- rl2.close();
21318
+ chat.destroy();
19894
21319
  setChatMode(false);
19895
21320
  }
19896
21321
  }
@@ -19903,6 +21328,32 @@ init_ideaService();
19903
21328
  function generateIdeas3(...args) {
19904
21329
  return generateIdeas2(...args);
19905
21330
  }
21331
+ async function startInterview(idea, answerProvider, onEvent) {
21332
+ if (onEvent) interviewEmitter.addListener(onEvent);
21333
+ const agent = createInterviewAgent();
21334
+ try {
21335
+ return await agent.runInterview(idea, answerProvider);
21336
+ } finally {
21337
+ await agent.destroy();
21338
+ if (onEvent) interviewEmitter.removeListener(onEvent);
21339
+ }
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
+ }
19906
21357
 
19907
21358
  // src/L7-app/commands/ideate.ts
19908
21359
  init_types();
@@ -20072,10 +21523,352 @@ async function handleAdd(options) {
20072
21523
  }
20073
21524
  }
20074
21525
 
21526
+ // src/L7-app/commands/ideateStart.ts
21527
+ init_environment();
21528
+ init_configLogger();
21529
+
21530
+ // src/L3-services/interview/interviewService.ts
21531
+ init_configLogger();
21532
+ init_githubClient();
21533
+ init_ideaService();
21534
+ async function loadAndValidateIdea(issueNumber) {
21535
+ const idea = await getIdea(issueNumber);
21536
+ if (!idea) {
21537
+ throw new Error(`Idea #${issueNumber} not found`);
21538
+ }
21539
+ if (idea.status !== "draft") {
21540
+ throw new Error(
21541
+ `Idea #${issueNumber} has status "${idea.status}" \u2014 only draft ideas can be started`
21542
+ );
21543
+ }
21544
+ return idea;
21545
+ }
21546
+ function formatTranscriptComment(transcript) {
21547
+ const now = (/* @__PURE__ */ new Date()).toISOString();
21548
+ const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
21549
+ year: "numeric",
21550
+ month: "long",
21551
+ day: "numeric"
21552
+ });
21553
+ const lines = [
21554
+ "<!-- vidpipe:idea-comment -->",
21555
+ `<!-- {"type":"interview-transcript","savedAt":"${now}"} -->`,
21556
+ "",
21557
+ "## \u{1F399}\uFE0F Interview Transcript",
21558
+ "",
21559
+ `**Questions asked:** ${transcript.length}`,
21560
+ `**Date:** ${date}`,
21561
+ "",
21562
+ "---"
21563
+ ];
21564
+ for (const qa of transcript) {
21565
+ lines.push("");
21566
+ lines.push(`### Q${qa.questionNumber}: ${qa.question}`);
21567
+ lines.push(`> ${qa.answer}`);
21568
+ }
21569
+ return lines.join("\n");
21570
+ }
21571
+ async function saveTranscript(issueNumber, transcript) {
21572
+ const body = formatTranscriptComment(transcript);
21573
+ const client = getGitHubClient();
21574
+ await client.addComment(issueNumber, body);
21575
+ logger_default.info(`Saved interview transcript (${transcript.length} Q&A) to issue #${issueNumber}`);
21576
+ }
21577
+ async function updateIdeaFromInsights(issueNumber, insights) {
21578
+ const updates = {};
21579
+ const updatedFields = [];
21580
+ if (insights.hook !== void 0) {
21581
+ updates.hook = insights.hook;
21582
+ updatedFields.push("hook");
21583
+ }
21584
+ if (insights.audience !== void 0) {
21585
+ updates.audience = insights.audience;
21586
+ updatedFields.push("audience");
21587
+ }
21588
+ if (insights.keyTakeaway !== void 0) {
21589
+ updates.keyTakeaway = insights.keyTakeaway;
21590
+ updatedFields.push("keyTakeaway");
21591
+ }
21592
+ if (insights.trendContext !== void 0) {
21593
+ updates.trendContext = insights.trendContext;
21594
+ updatedFields.push("trendContext");
21595
+ }
21596
+ if (insights.talkingPoints !== void 0 && insights.talkingPoints.length > 0) {
21597
+ updates.talkingPoints = insights.talkingPoints;
21598
+ updatedFields.push("talkingPoints");
21599
+ }
21600
+ if (insights.tags !== void 0 && insights.tags.length > 0) {
21601
+ updates.tags = insights.tags;
21602
+ updatedFields.push("tags");
21603
+ }
21604
+ if (updatedFields.length === 0) {
21605
+ logger_default.info(`No fields to update for idea #${issueNumber} from insights`);
21606
+ return;
21607
+ }
21608
+ await updateIdea(issueNumber, updates);
21609
+ logger_default.info(
21610
+ `Updated idea #${issueNumber} fields: ${updatedFields.join(", ")}`
21611
+ );
21612
+ }
21613
+
21614
+ // src/L7-app/commands/ideateStart.ts
21615
+ init_ideaService();
21616
+ var VALID_MODES = ["interview"];
21617
+ async function runIdeateStart(issueNumber, options) {
21618
+ const parsed = Number.parseInt(issueNumber, 10);
21619
+ if (Number.isNaN(parsed) || parsed < 1) {
21620
+ console.error(`Invalid issue number: "${issueNumber}". Must be a positive integer.`);
21621
+ process.exit(1);
21622
+ }
21623
+ initConfig();
21624
+ const mode = options.mode ?? "interview";
21625
+ if (!VALID_MODES.includes(mode)) {
21626
+ console.error(`Unknown mode: "${options.mode}". Valid modes: ${VALID_MODES.join(", ")}`);
21627
+ process.exit(1);
21628
+ }
21629
+ if (options.progress) {
21630
+ interviewEmitter.enable();
21631
+ }
21632
+ const idea = await loadAndValidateIdea(parsed);
21633
+ const chat = new AltScreenChat({
21634
+ title: `\u{1F4DD} Interview: ${idea.topic}`,
21635
+ subtitle: "Type /end to finish the interview. Press Ctrl+C to save and quit.",
21636
+ inputPrompt: "Your answer> "
21637
+ });
21638
+ const answerProvider = async (question, context) => {
21639
+ chat.showQuestion(
21640
+ question,
21641
+ context.rationale,
21642
+ context.targetField ? String(context.targetField) : "general",
21643
+ context.questionNumber
21644
+ );
21645
+ const answer = await chat.promptInput();
21646
+ if (chat.interrupted) {
21647
+ return "/end";
21648
+ }
21649
+ chat.addMessage("agent", question);
21650
+ chat.addMessage("user", answer);
21651
+ return answer;
21652
+ };
21653
+ const handleEvent = (event) => {
21654
+ switch (event.event) {
21655
+ case "thinking:start":
21656
+ chat.setStatus("\u{1F914} Thinking of next question...");
21657
+ break;
21658
+ case "thinking:end":
21659
+ chat.clearStatus();
21660
+ break;
21661
+ case "tool:start":
21662
+ chat.setStatus(`\u{1F527} ${event.toolName}...`);
21663
+ break;
21664
+ case "tool:end":
21665
+ chat.clearStatus();
21666
+ break;
21667
+ case "insight:discovered":
21668
+ chat.showInsight(`${event.field}: ${event.insight}`);
21669
+ break;
21670
+ }
21671
+ };
21672
+ setChatMode(true);
21673
+ chat.enter();
21674
+ chat.addMessage("system", `Starting interview for idea #${idea.issueNumber}: ${idea.topic}`);
21675
+ chat.addMessage("system", "The agent will ask Socratic questions to help develop your idea.");
21676
+ try {
21677
+ const result = await startInterview(idea, answerProvider, handleEvent);
21678
+ await saveResults(result, chat, parsed);
21679
+ } catch (error) {
21680
+ if (error instanceof Error) {
21681
+ chat.addMessage("error", error.message);
21682
+ }
21683
+ throw error;
21684
+ } finally {
21685
+ chat.destroy();
21686
+ setChatMode(false);
21687
+ }
21688
+ }
21689
+ async function saveResults(result, chat, issueNumber) {
21690
+ const durationSec = Math.round(result.durationMs / 1e3);
21691
+ const fieldList = result.updatedFields.length > 0 ? result.updatedFields.join(", ") : "none";
21692
+ chat.showQuestion(
21693
+ `Interview ${result.endedBy === "user" ? "ended" : "completed"} \u2014 ${result.transcript.length} questions in ${durationSec}s`,
21694
+ `Updated fields: ${fieldList}`,
21695
+ "summary",
21696
+ result.transcript.length
21697
+ );
21698
+ if (result.transcript.length > 0) {
21699
+ chat.setStatus("\u{1F4BE} Saving transcript...");
21700
+ await saveTranscript(issueNumber, result.transcript);
21701
+ }
21702
+ if (result.insights && Object.keys(result.insights).length > 0) {
21703
+ chat.setStatus("\u{1F4BE} Updating idea fields...");
21704
+ await updateIdeaFromInsights(issueNumber, result.insights);
21705
+ }
21706
+ chat.clearStatus();
21707
+ chat.showInsight("\u2705 Saved! Mark this idea as ready? (yes/no)");
21708
+ const response = await chat.promptInput();
21709
+ if (response.toLowerCase().startsWith("y")) {
21710
+ await updateIdea(issueNumber, { status: "ready" });
21711
+ chat.showInsight(`\u2705 Idea #${issueNumber} marked as ready`);
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
+ }
21863
+ }
21864
+ console.log(`
21865
+ \u{1F3C1} Done: ${totalUpdated} items updated, ${totalFailed} skipped/failed`);
21866
+ }
21867
+
20075
21868
  // src/L1-infra/readline/readlinePromises.ts
20076
- import { createInterface as createInterface2 } from "readline/promises";
21869
+ import { createInterface } from "readline/promises";
20077
21870
  function createPromptInterface(options) {
20078
- return createInterface2({
21871
+ return createInterface({
20079
21872
  input: options?.input ?? process.stdin,
20080
21873
  output: options?.output ?? process.stdout
20081
21874
  });
@@ -21215,8 +23008,8 @@ init_configLogger();
21215
23008
  var queue = [];
21216
23009
  var processing = false;
21217
23010
  function enqueueApproval(itemIds) {
21218
- return new Promise((resolve3) => {
21219
- queue.push({ itemIds, resolve: resolve3 });
23011
+ return new Promise((resolve4) => {
23012
+ queue.push({ itemIds, resolve: resolve4 });
21220
23013
  if (!processing) drain();
21221
23014
  });
21222
23015
  }
@@ -21270,23 +23063,26 @@ async function processApprovalBatch(itemIds) {
21270
23063
  }
21271
23064
  }
21272
23065
  const enriched = loadedItems.map(({ id, item }) => {
23066
+ const createdAt = item?.metadata.createdAt ?? null;
21273
23067
  if (!item?.metadata.ideaIds?.length) {
21274
- return { id, publishBy: null, hasIdeas: false };
23068
+ return { id, publishBy: null, hasIdeas: false, createdAt };
21275
23069
  }
21276
23070
  const dates = item.metadata.ideaIds.map((ideaId) => ideaMap.get(ideaId)?.publishBy).filter((publishBy) => Boolean(publishBy)).sort();
21277
- return { id, publishBy: dates[0] ?? null, hasIdeas: true };
23071
+ return { id, publishBy: dates[0] ?? null, hasIdeas: true, createdAt };
21278
23072
  });
21279
- const now = Date.now();
21280
- const sevenDays = 7 * 24 * 60 * 60 * 1e3;
21281
23073
  enriched.sort((a, b) => {
21282
- const aPublishByTime = a.publishBy ? new Date(a.publishBy).getTime() : Number.NaN;
21283
- const bPublishByTime = b.publishBy ? new Date(b.publishBy).getTime() : Number.NaN;
21284
- const aUrgent = a.hasIdeas && Number.isFinite(aPublishByTime) && aPublishByTime - now < sevenDays;
21285
- const bUrgent = b.hasIdeas && Number.isFinite(bPublishByTime) && bPublishByTime - now < sevenDays;
21286
- if (aUrgent && !bUrgent) return -1;
21287
- if (!aUrgent && bUrgent) return 1;
21288
23074
  if (a.hasIdeas && !b.hasIdeas) return -1;
21289
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
+ }
21290
23086
  return 0;
21291
23087
  });
21292
23088
  const sortedIds = enriched.map((entry) => entry.id);
@@ -21634,7 +23430,7 @@ async function startReviewServer(options = {}) {
21634
23430
  res.sendFile(join(publicDir, "index.html"));
21635
23431
  }
21636
23432
  });
21637
- return new Promise((resolve3, reject) => {
23433
+ return new Promise((resolve4, reject) => {
21638
23434
  const tryPort = (p, attempts) => {
21639
23435
  const server = app.listen(p, "127.0.0.1", () => {
21640
23436
  logger_default.info(`Review server running at http://localhost:${p}`);
@@ -21643,7 +23439,7 @@ async function startReviewServer(options = {}) {
21643
23439
  connections.add(conn);
21644
23440
  conn.on("close", () => connections.delete(conn));
21645
23441
  });
21646
- resolve3({
23442
+ resolve4({
21647
23443
  port: p,
21648
23444
  close: () => new Promise((res) => {
21649
23445
  let done = false;
@@ -21678,6 +23474,28 @@ async function startReviewServer(options = {}) {
21678
23474
  });
21679
23475
  }
21680
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
+
21681
23499
  // src/L7-app/cli.ts
21682
23500
  init_fileSystem();
21683
23501
  init_paths();
@@ -21743,6 +23561,28 @@ program.command("chat").description("Interactive chat session with the schedule
21743
23561
  program.command("doctor").description("Check all prerequisites and dependencies").action(async () => {
21744
23562
  await runDoctor();
21745
23563
  });
23564
+ program.command("ideate-start <issue-number>").description("Start an interactive session to develop a content idea").option("--mode <mode>", "Session mode: interview (default)", "interview").option("--progress", "Emit structured JSON interview events to stderr").action(async (issueNumber, opts) => {
23565
+ await runIdeateStart(issueNumber, opts);
23566
+ process.exit(0);
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
+ });
21746
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) => {
21747
23587
  initConfig();
21748
23588
  await runIdeate(opts);
@@ -21787,7 +23627,7 @@ program.command("thumbnail").description("Generate a thumbnail for a recording f
21787
23627
  });
21788
23628
  process.exit(0);
21789
23629
  });
21790
- 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) => {
21791
23631
  const opts = defaultCmd.opts();
21792
23632
  if (opts.doctor) {
21793
23633
  await runDoctor();
@@ -21838,7 +23678,21 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
21838
23678
  if (videoPath) {
21839
23679
  const resolvedPath = resolve(videoPath);
21840
23680
  logger_default.info(`Processing single video: ${resolvedPath}`);
21841
- 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);
21842
23696
  if (ideas && ideas.length > 0) {
21843
23697
  try {
21844
23698
  const { markRecorded: markRecorded3 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));