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 +2171 -317
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +189 -1
- package/dist/index.js +1839 -452
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
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((
|
|
256
|
+
return new Promise((resolve4, reject) => {
|
|
257
257
|
tmp.dir({ prefix, mode: 448 }, (err, path) => {
|
|
258
258
|
if (err) reject(err);
|
|
259
|
-
else
|
|
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: "
|
|
756
|
-
{ stage: "
|
|
757
|
-
{ stage: "
|
|
758
|
-
{ stage: "
|
|
759
|
-
{ stage: "
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
917
|
-
const cmd = createFFmpeg(audioPath).setStartTime(startTime).setDuration(chunkDuration).audioCodec("copy").output(chunkPath).on("end", () =>
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
1000
|
-
createFFmpeg(videoPath).setStartTime(bufferedStart).setDuration(bufferedEnd - bufferedStart).outputOptions(["-threads", "4", "-preset", "ultrafast"]).output(tempPath).on("end", () =>
|
|
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((
|
|
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", () =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
1387
|
+
resolve4(parseFloat(stdout.trim()));
|
|
1387
1388
|
}
|
|
1388
1389
|
);
|
|
1389
1390
|
});
|
|
1390
1391
|
}
|
|
1391
1392
|
async function getVideoResolution(videoPath) {
|
|
1392
|
-
return new Promise((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
3543
|
-
this._waiting.push({ thunk, resolve:
|
|
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 (
|
|
5041
|
+
return new Promise(async (resolve4, reject) => {
|
|
5034
5042
|
const resolveWithCleanup = (r) => {
|
|
5035
|
-
|
|
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((
|
|
5455
|
+
return new Promise((resolve4, reject) => {
|
|
5448
5456
|
const callback = (error) => {
|
|
5449
5457
|
if (error === void 0 || error === null) {
|
|
5450
|
-
|
|
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((
|
|
5703
|
-
connectResolve =
|
|
5710
|
+
const connected = new Promise((resolve4, _reject) => {
|
|
5711
|
+
connectResolve = resolve4;
|
|
5704
5712
|
});
|
|
5705
|
-
return new Promise((
|
|
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
|
-
|
|
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((
|
|
5736
|
-
connectResolve =
|
|
5743
|
+
const connected = new Promise((resolve4, _reject) => {
|
|
5744
|
+
connectResolve = resolve4;
|
|
5737
5745
|
});
|
|
5738
|
-
return new Promise((
|
|
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
|
-
|
|
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((
|
|
5970
|
-
resolveIdle =
|
|
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((
|
|
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((
|
|
6953
|
-
resolveLock =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
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((
|
|
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
|
|
10540
|
-
throw new Error(`${fieldName} must be a
|
|
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
|
-
|
|
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
|
-
|
|
10905
|
-
for
|
|
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.
|
|
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 &&
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
10980
|
-
const
|
|
10981
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`[
|
|
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
|
|
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
|
|
11020
|
-
|
|
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
|
-
|
|
11027
|
-
const
|
|
11028
|
-
const
|
|
11029
|
-
|
|
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 && !
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
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 ?
|
|
11050
|
-
const bDate = bId ?
|
|
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
|
|
11057
|
-
|
|
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
|
|
11193
|
+
const isIdea = Boolean(item.metadata.ideaIds?.length);
|
|
11194
|
+
const label = `${item.id} (${itemPlatform}/${clipType})`;
|
|
11075
11195
|
try {
|
|
11076
|
-
const platformConfig = getPlatformSchedule(
|
|
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
|
-
|
|
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
|
-
|
|
11245
|
+
bookedMap.set(newSlotMs, {
|
|
11093
11246
|
scheduledFor: newSlotDatetime,
|
|
11094
11247
|
source: "late",
|
|
11095
11248
|
postId: latePostId,
|
|
11096
|
-
platform,
|
|
11097
|
-
ideaLinked:
|
|
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
|
|
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
|
-
|
|
11277
|
+
bookedMap.set(newSlotMs, {
|
|
11117
11278
|
scheduledFor: newSlotDatetime,
|
|
11118
11279
|
source: "late",
|
|
11119
11280
|
postId: latePostId,
|
|
11120
|
-
platform,
|
|
11121
|
-
ideaLinked:
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
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",
|
|
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((
|
|
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",
|
|
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
|
|
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
|
-
|
|
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 =
|
|
17515
|
-
const bIdea =
|
|
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 =
|
|
17540
|
-
const
|
|
17541
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
|
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
|
|
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 =
|
|
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/
|
|
18816
|
-
|
|
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((
|
|
19501
|
-
rl.question(question, (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/
|
|
19793
|
-
import {
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
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
|
|
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
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19817
|
-
|
|
19818
|
-
|
|
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
|
-
|
|
21279
|
+
resolve4({ answer: request.choices[num - 1], wasFreeform: false });
|
|
19830
21280
|
return;
|
|
19831
21281
|
}
|
|
19832
21282
|
}
|
|
19833
|
-
|
|
21283
|
+
resolve4({ answer: trimmed, wasFreeform: true });
|
|
19834
21284
|
});
|
|
19835
21285
|
});
|
|
19836
21286
|
};
|
|
19837
21287
|
const agent = createScheduleAgent2(handleUserInput);
|
|
19838
21288
|
agent.setChatOutput((message) => {
|
|
19839
|
-
|
|
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
|
-
|
|
19858
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
21869
|
+
import { createInterface } from "readline/promises";
|
|
20077
21870
|
function createPromptInterface(options) {
|
|
20078
|
-
return
|
|
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((
|
|
21219
|
-
queue.push({ itemIds, resolve:
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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));
|