vidpipe 1.3.5 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1429 -285
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -316,7 +316,9 @@ function initConfig(cli = {}) {
316
316
  LATE_PROFILE_ID: cli.lateProfileId || process.env.LATE_PROFILE_ID || "",
317
317
  SKIP_SOCIAL_PUBLISH: cli.socialPublish === false,
318
318
  GEMINI_API_KEY: process.env.GEMINI_API_KEY || "",
319
- GEMINI_MODEL: process.env.GEMINI_MODEL || "gemini-2.5-pro"
319
+ GEMINI_MODEL: process.env.GEMINI_MODEL || "gemini-2.5-pro",
320
+ IDEAS_REPO: cli.ideasRepo || process.env.IDEAS_REPO || "htekdev/content-management",
321
+ GITHUB_TOKEN: cli.githubToken || process.env.GITHUB_TOKEN || ""
320
322
  };
321
323
  return config;
322
324
  }
@@ -402,6 +404,54 @@ var init_configLogger = __esm({
402
404
  }
403
405
  });
404
406
 
407
+ // src/L0-pure/types/index.ts
408
+ function toLatePlatform(platform) {
409
+ return platform === "x" /* X */ ? "twitter" : platform;
410
+ }
411
+ function fromLatePlatform(latePlatform) {
412
+ const normalized = normalizePlatformString(latePlatform);
413
+ if (normalized === "twitter") {
414
+ return "x" /* X */;
415
+ }
416
+ const platformValues2 = Object.values(Platform);
417
+ if (platformValues2.includes(normalized)) {
418
+ return normalized;
419
+ }
420
+ throw new Error(`Unsupported platform from Late API: ${latePlatform}`);
421
+ }
422
+ function normalizePlatformString(raw) {
423
+ const lower = raw.toLowerCase().trim();
424
+ if (lower === "x" || lower === "x (twitter)" || lower === "x/twitter") {
425
+ return "twitter";
426
+ }
427
+ return lower;
428
+ }
429
+ function isSupportedVideoExtension(ext) {
430
+ return SUPPORTED_VIDEO_EXTENSIONS.includes(ext.toLowerCase());
431
+ }
432
+ var Platform, PLATFORM_CHAR_LIMITS, SUPPORTED_VIDEO_EXTENSIONS;
433
+ var init_types = __esm({
434
+ "src/L0-pure/types/index.ts"() {
435
+ "use strict";
436
+ Platform = /* @__PURE__ */ ((Platform2) => {
437
+ Platform2["TikTok"] = "tiktok";
438
+ Platform2["YouTube"] = "youtube";
439
+ Platform2["Instagram"] = "instagram";
440
+ Platform2["LinkedIn"] = "linkedin";
441
+ Platform2["X"] = "x";
442
+ return Platform2;
443
+ })(Platform || {});
444
+ PLATFORM_CHAR_LIMITS = {
445
+ tiktok: 2200,
446
+ youtube: 5e3,
447
+ instagram: 2200,
448
+ linkedin: 3e3,
449
+ twitter: 280
450
+ };
451
+ SUPPORTED_VIDEO_EXTENSIONS = [".mp4", ".webm"];
452
+ }
453
+ });
454
+
405
455
  // src/L0-pure/pricing/pricing.ts
406
456
  function calculateTokenCost(model, inputTokens, outputTokens) {
407
457
  const pricing = getModelPricing(model);
@@ -1187,151 +1237,870 @@ var init_modelConfig = __esm({
1187
1237
  }
1188
1238
  });
1189
1239
 
1190
- // src/L1-infra/ideaStore/ideaStore.ts
1191
- function resolveIdeasDir(dir) {
1192
- return dir ? resolve(dir) : DEFAULT_IDEAS_DIR;
1240
+ // src/L2-clients/github/githubClient.ts
1241
+ import { Octokit } from "octokit";
1242
+ function getErrorStatus(error) {
1243
+ if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") {
1244
+ return error.status;
1245
+ }
1246
+ return void 0;
1193
1247
  }
1194
1248
  function getErrorMessage(error) {
1195
- return error instanceof Error ? error.message : String(error);
1249
+ if (error instanceof Error) {
1250
+ return error.message;
1251
+ }
1252
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
1253
+ return error.message ?? "Unknown GitHub API error";
1254
+ }
1255
+ return String(error);
1196
1256
  }
1197
- function validateIdeaId(id) {
1198
- if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id)) {
1199
- throw new Error(`Invalid idea ID: ${id}`);
1257
+ function normalizeLabels(labels) {
1258
+ return Array.from(new Set(labels.map((label) => label.trim()).filter((label) => label.length > 0)));
1259
+ }
1260
+ function isIssueResponse(value) {
1261
+ return !("pull_request" in value);
1262
+ }
1263
+ function getGitHubClient() {
1264
+ const config2 = getConfig();
1265
+ const nextKey = `${config2.IDEAS_REPO}:${config2.GITHUB_TOKEN}`;
1266
+ if (!clientInstance || clientKey !== nextKey) {
1267
+ clientInstance = new GitHubClient(config2.GITHUB_TOKEN, config2.IDEAS_REPO);
1268
+ clientKey = nextKey;
1200
1269
  }
1201
- return id;
1270
+ return clientInstance;
1202
1271
  }
1203
- function getIdeaFilePath(id, dir) {
1204
- return join(resolveIdeasDir(dir), `${validateIdeaId(id)}${IDEA_FILE_EXTENSION}`);
1272
+ var DEFAULT_PER_PAGE, GitHubClientError, GitHubClient, clientInstance, clientKey;
1273
+ var init_githubClient = __esm({
1274
+ "src/L2-clients/github/githubClient.ts"() {
1275
+ "use strict";
1276
+ init_environment();
1277
+ init_configLogger();
1278
+ DEFAULT_PER_PAGE = 100;
1279
+ GitHubClientError = class extends Error {
1280
+ constructor(message, status) {
1281
+ super(message);
1282
+ this.status = status;
1283
+ this.name = "GitHubClientError";
1284
+ }
1285
+ };
1286
+ GitHubClient = class {
1287
+ octokit;
1288
+ owner;
1289
+ repo;
1290
+ constructor(token, repoFullName) {
1291
+ const config2 = getConfig();
1292
+ const authToken = token || config2.GITHUB_TOKEN;
1293
+ if (!authToken) {
1294
+ throw new Error("GITHUB_TOKEN is required for GitHub API access");
1295
+ }
1296
+ const fullName = repoFullName || config2.IDEAS_REPO;
1297
+ const [owner, repo] = fullName.split("/").map((part) => part.trim());
1298
+ if (!owner || !repo) {
1299
+ throw new Error(`Invalid IDEAS_REPO format: "${fullName}" \u2014 expected "owner/repo"`);
1300
+ }
1301
+ this.owner = owner;
1302
+ this.repo = repo;
1303
+ this.octokit = new Octokit({ auth: authToken });
1304
+ }
1305
+ async createIssue(input) {
1306
+ logger_default.debug(`[GitHubClient] Creating issue in ${this.owner}/${this.repo}: ${input.title}`);
1307
+ try {
1308
+ const response = await this.octokit.rest.issues.create({
1309
+ owner: this.owner,
1310
+ repo: this.repo,
1311
+ title: input.title,
1312
+ body: input.body,
1313
+ labels: input.labels ? normalizeLabels(input.labels) : void 0
1314
+ });
1315
+ const issue = this.mapIssue(response.data);
1316
+ logger_default.info(`[GitHubClient] Created issue #${issue.number}: ${input.title}`);
1317
+ return issue;
1318
+ } catch (error) {
1319
+ this.logError("create issue", error);
1320
+ throw new GitHubClientError(`Failed to create GitHub issue: ${getErrorMessage(error)}`, getErrorStatus(error));
1321
+ }
1322
+ }
1323
+ async updateIssue(issueNumber, input) {
1324
+ logger_default.debug(`[GitHubClient] Updating issue #${issueNumber} in ${this.owner}/${this.repo}`);
1325
+ try {
1326
+ const response = await this.octokit.rest.issues.update({
1327
+ owner: this.owner,
1328
+ repo: this.repo,
1329
+ issue_number: issueNumber,
1330
+ title: input.title,
1331
+ body: input.body,
1332
+ state: input.state,
1333
+ labels: input.labels ? normalizeLabels(input.labels) : void 0
1334
+ });
1335
+ return this.mapIssue(response.data);
1336
+ } catch (error) {
1337
+ this.logError(`update issue #${issueNumber}`, error);
1338
+ throw new GitHubClientError(
1339
+ `Failed to update GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1340
+ getErrorStatus(error)
1341
+ );
1342
+ }
1343
+ }
1344
+ async getIssue(issueNumber) {
1345
+ logger_default.debug(`[GitHubClient] Fetching issue #${issueNumber} from ${this.owner}/${this.repo}`);
1346
+ try {
1347
+ const response = await this.octokit.rest.issues.get({
1348
+ owner: this.owner,
1349
+ repo: this.repo,
1350
+ issue_number: issueNumber
1351
+ });
1352
+ return this.mapIssue(response.data);
1353
+ } catch (error) {
1354
+ this.logError(`get issue #${issueNumber}`, error);
1355
+ throw new GitHubClientError(
1356
+ `Failed to fetch GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1357
+ getErrorStatus(error)
1358
+ );
1359
+ }
1360
+ }
1361
+ async listIssues(options = {}) {
1362
+ logger_default.debug(`[GitHubClient] Listing issues for ${this.owner}/${this.repo}`);
1363
+ const issues = [];
1364
+ let page;
1365
+ const maxResults = options.maxResults ?? Number.POSITIVE_INFINITY;
1366
+ try {
1367
+ while (issues.length < maxResults) {
1368
+ const response = await this.octokit.rest.issues.listForRepo({
1369
+ owner: this.owner,
1370
+ repo: this.repo,
1371
+ state: "open",
1372
+ labels: options.labels && options.labels.length > 0 ? normalizeLabels(options.labels).join(",") : void 0,
1373
+ sort: void 0,
1374
+ direction: void 0,
1375
+ per_page: DEFAULT_PER_PAGE,
1376
+ page
1377
+ });
1378
+ const pageItems = response.data.filter(isIssueResponse).map((issue) => this.mapIssue(issue));
1379
+ issues.push(...pageItems);
1380
+ if (pageItems.length < DEFAULT_PER_PAGE) {
1381
+ break;
1382
+ }
1383
+ page = (page ?? 1) + 1;
1384
+ }
1385
+ return issues.slice(0, maxResults);
1386
+ } catch (error) {
1387
+ this.logError("list issues", error);
1388
+ throw new GitHubClientError(`Failed to list GitHub issues: ${getErrorMessage(error)}`, getErrorStatus(error));
1389
+ }
1390
+ }
1391
+ async searchIssues(query, options = {}) {
1392
+ const searchQuery = `repo:${this.owner}/${this.repo} is:issue ${query}`.trim();
1393
+ logger_default.debug(`[GitHubClient] Searching issues in ${this.owner}/${this.repo}: ${query}`);
1394
+ try {
1395
+ const items = await this.octokit.paginate(this.octokit.rest.search.issuesAndPullRequests, {
1396
+ q: searchQuery,
1397
+ per_page: DEFAULT_PER_PAGE
1398
+ });
1399
+ return items.filter(isIssueResponse).map((issue) => this.mapIssue(issue)).slice(0, options.maxResults ?? Number.POSITIVE_INFINITY);
1400
+ } catch (error) {
1401
+ this.logError("search issues", error);
1402
+ throw new GitHubClientError(`Failed to search GitHub issues: ${getErrorMessage(error)}`, getErrorStatus(error));
1403
+ }
1404
+ }
1405
+ async addLabels(issueNumber, labels) {
1406
+ if (labels.length === 0) {
1407
+ return;
1408
+ }
1409
+ logger_default.debug(`[GitHubClient] Adding labels to issue #${issueNumber} in ${this.owner}/${this.repo}`);
1410
+ try {
1411
+ await this.octokit.rest.issues.addLabels({
1412
+ owner: this.owner,
1413
+ repo: this.repo,
1414
+ issue_number: issueNumber,
1415
+ labels
1416
+ });
1417
+ } catch (error) {
1418
+ this.logError(`add labels to issue #${issueNumber}`, error);
1419
+ throw new GitHubClientError(
1420
+ `Failed to add labels to GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1421
+ getErrorStatus(error)
1422
+ );
1423
+ }
1424
+ }
1425
+ async removeLabel(issueNumber, label) {
1426
+ logger_default.debug(`[GitHubClient] Removing label "${label}" from issue #${issueNumber} in ${this.owner}/${this.repo}`);
1427
+ try {
1428
+ await this.octokit.rest.issues.removeLabel({
1429
+ owner: this.owner,
1430
+ repo: this.repo,
1431
+ issue_number: issueNumber,
1432
+ name: label
1433
+ });
1434
+ } catch (error) {
1435
+ if (getErrorStatus(error) === 404) {
1436
+ return;
1437
+ }
1438
+ this.logError(`remove label from issue #${issueNumber}`, error);
1439
+ throw new GitHubClientError(
1440
+ `Failed to remove label from GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1441
+ getErrorStatus(error)
1442
+ );
1443
+ }
1444
+ }
1445
+ async setLabels(issueNumber, labels) {
1446
+ logger_default.debug(`[GitHubClient] Setting labels on issue #${issueNumber} in ${this.owner}/${this.repo}`);
1447
+ try {
1448
+ await this.octokit.rest.issues.setLabels({
1449
+ owner: this.owner,
1450
+ repo: this.repo,
1451
+ issue_number: issueNumber,
1452
+ labels
1453
+ });
1454
+ } catch (error) {
1455
+ this.logError(`set labels on issue #${issueNumber}`, error);
1456
+ throw new GitHubClientError(
1457
+ `Failed to set labels on GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1458
+ getErrorStatus(error)
1459
+ );
1460
+ }
1461
+ }
1462
+ async addComment(issueNumber, body) {
1463
+ logger_default.debug(`[GitHubClient] Adding comment to issue #${issueNumber} in ${this.owner}/${this.repo}`);
1464
+ try {
1465
+ const response = await this.octokit.rest.issues.createComment({
1466
+ owner: this.owner,
1467
+ repo: this.repo,
1468
+ issue_number: issueNumber,
1469
+ body
1470
+ });
1471
+ return this.mapComment(response.data);
1472
+ } catch (error) {
1473
+ this.logError(`add comment to issue #${issueNumber}`, error);
1474
+ throw new GitHubClientError(
1475
+ `Failed to add comment to GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1476
+ getErrorStatus(error)
1477
+ );
1478
+ }
1479
+ }
1480
+ async listComments(issueNumber) {
1481
+ logger_default.debug(`[GitHubClient] Listing comments for issue #${issueNumber} in ${this.owner}/${this.repo}`);
1482
+ try {
1483
+ const comments = await this.octokit.paginate(this.octokit.rest.issues.listComments, {
1484
+ owner: this.owner,
1485
+ repo: this.repo,
1486
+ issue_number: issueNumber,
1487
+ per_page: DEFAULT_PER_PAGE
1488
+ });
1489
+ return comments.map((comment) => this.mapComment(comment));
1490
+ } catch (error) {
1491
+ this.logError(`list comments for issue #${issueNumber}`, error);
1492
+ throw new GitHubClientError(
1493
+ `Failed to list comments for GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1494
+ getErrorStatus(error)
1495
+ );
1496
+ }
1497
+ }
1498
+ mapIssue(issue) {
1499
+ return {
1500
+ number: issue.number,
1501
+ title: issue.title,
1502
+ body: issue.body ?? "",
1503
+ state: issue.state,
1504
+ labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").map((label) => label.trim()).filter((label) => label.length > 0),
1505
+ created_at: issue.created_at,
1506
+ updated_at: issue.updated_at,
1507
+ html_url: issue.html_url
1508
+ };
1509
+ }
1510
+ mapComment(comment) {
1511
+ return {
1512
+ id: comment.id,
1513
+ body: comment.body ?? "",
1514
+ created_at: comment.created_at,
1515
+ updated_at: comment.updated_at,
1516
+ html_url: comment.html_url
1517
+ };
1518
+ }
1519
+ logError(action, error) {
1520
+ logger_default.error(`[GitHubClient] Failed to ${action} in ${this.owner}/${this.repo}: ${getErrorMessage(error)}`);
1521
+ }
1522
+ };
1523
+ clientInstance = null;
1524
+ clientKey = "";
1525
+ }
1526
+ });
1527
+
1528
+ // src/L3-services/ideaService/ideaService.ts
1529
+ var ideaService_exports = {};
1530
+ __export(ideaService_exports, {
1531
+ createIdea: () => createIdea,
1532
+ findRelatedIdeas: () => findRelatedIdeas,
1533
+ getIdea: () => getIdea,
1534
+ getPublishHistory: () => getPublishHistory,
1535
+ getReadyIdeas: () => getReadyIdeas,
1536
+ linkVideoToIdea: () => linkVideoToIdea,
1537
+ listIdeas: () => listIdeas,
1538
+ markPublished: () => markPublished,
1539
+ markRecorded: () => markRecorded,
1540
+ recordPublish: () => recordPublish,
1541
+ searchIdeas: () => searchIdeas,
1542
+ updateIdea: () => updateIdea
1543
+ });
1544
+ function getErrorMessage2(error) {
1545
+ return error instanceof Error ? error.message : String(error);
1205
1546
  }
1206
- function isRecord(value) {
1207
- return typeof value === "object" && value !== null;
1547
+ function sanitizeMultilineValue(value) {
1548
+ return (value ?? "").trim();
1208
1549
  }
1209
- function isStringArray(value) {
1210
- return Array.isArray(value) && value.every((item) => typeof item === "string");
1550
+ function normalizeTag(tag) {
1551
+ return tag.trim().toLowerCase().replace(/\s+/g, "-");
1211
1552
  }
1212
- function isIdeaStatus(value) {
1213
- return typeof value === "string" && ideaStatuses.has(value);
1553
+ function normalizeTags(tags) {
1554
+ return Array.from(new Set(tags.map((tag) => normalizeTag(tag)).filter((tag) => tag.length > 0)));
1214
1555
  }
1215
1556
  function isPlatform(value) {
1216
- return typeof value === "string" && ideaPlatforms.has(value);
1557
+ return platformValues.has(value);
1217
1558
  }
1218
- function isPlatformArray(value) {
1219
- return Array.isArray(value) && value.every((item) => isPlatform(item));
1220
- }
1221
- function isValidIsoDateString(value) {
1222
- return typeof value === "string" && !Number.isNaN(new Date(value).getTime());
1559
+ function isIdeaStatus(value) {
1560
+ return ideaStatuses.has(value);
1223
1561
  }
1224
- function isIdeaPublishRecord(value) {
1225
- return isRecord(value) && typeof value.queueItemId === "string" && typeof value.publishedAt === "string" && typeof value.clipType === "string" && ideaClipTypes.has(value.clipType) && isPlatform(value.platform) && (value.publishedUrl === void 0 || typeof value.publishedUrl === "string");
1562
+ function uniquePlatforms(platforms) {
1563
+ return Array.from(new Set(platforms));
1226
1564
  }
1227
- function isIdea(value) {
1228
- return isRecord(value) && typeof value.id === "string" && typeof value.topic === "string" && typeof value.hook === "string" && typeof value.audience === "string" && typeof value.keyTakeaway === "string" && isStringArray(value.talkingPoints) && isPlatformArray(value.platforms) && isIdeaStatus(value.status) && isStringArray(value.tags) && typeof value.createdAt === "string" && typeof value.updatedAt === "string" && isValidIsoDateString(value.publishBy) && (value.sourceVideoSlug === void 0 || typeof value.sourceVideoSlug === "string") && (value.trendContext === void 0 || typeof value.trendContext === "string") && (value.publishedContent === void 0 || Array.isArray(value.publishedContent) && value.publishedContent.every((item) => isIdeaPublishRecord(item)));
1565
+ function classifyIdeaPriority(publishBy, createdAtIso) {
1566
+ const publishByTimestamp = new Date(publishBy).getTime();
1567
+ const createdAtTimestamp = new Date(createdAtIso).getTime();
1568
+ if (Number.isNaN(publishByTimestamp) || Number.isNaN(createdAtTimestamp)) {
1569
+ return "evergreen";
1570
+ }
1571
+ const diffDays = Math.ceil((publishByTimestamp - createdAtTimestamp) / (1e3 * 60 * 60 * 24));
1572
+ if (diffDays <= 7) {
1573
+ return "hot-trend";
1574
+ }
1575
+ if (diffDays <= 14) {
1576
+ return "timely";
1577
+ }
1578
+ return "evergreen";
1229
1579
  }
1230
- async function readIdeaBank(dir) {
1231
- const ideasDir = resolveIdeasDir(dir);
1232
- const ideaIds = await listIdeaIds(ideasDir);
1233
- const ideas = await Promise.all(
1234
- ideaIds.map(async (id) => {
1235
- try {
1236
- return await readIdea(id, ideasDir);
1237
- } catch (error) {
1238
- logger_default.warn(`Skipping invalid idea file ${id}${IDEA_FILE_EXTENSION}: ${getErrorMessage(error)}`);
1239
- return null;
1580
+ function extractLabelsFromIdea(idea, publishBy, createdAtIso) {
1581
+ const labels = [
1582
+ `${STATUS_LABEL_PREFIX}${idea.status}`,
1583
+ ...uniquePlatforms(idea.platforms).map((platform) => `${PLATFORM_LABEL_PREFIX}${platform}`),
1584
+ `${PRIORITY_LABEL_PREFIX}${classifyIdeaPriority(publishBy, createdAtIso)}`,
1585
+ ...normalizeTags(idea.tags)
1586
+ ];
1587
+ return Array.from(new Set(labels));
1588
+ }
1589
+ function parseLabelsToIdea(labels) {
1590
+ let status = "draft";
1591
+ const platforms = [];
1592
+ const tags = [];
1593
+ for (const label of labels) {
1594
+ const normalized = label.trim().toLowerCase();
1595
+ if (!normalized) {
1596
+ continue;
1597
+ }
1598
+ if (normalized.startsWith(STATUS_LABEL_PREFIX)) {
1599
+ const value = normalized.slice(STATUS_LABEL_PREFIX.length);
1600
+ if (isIdeaStatus(value)) {
1601
+ status = value;
1240
1602
  }
1241
- })
1242
- );
1243
- return ideas.filter((idea) => idea !== null);
1603
+ continue;
1604
+ }
1605
+ if (normalized.startsWith(PLATFORM_LABEL_PREFIX)) {
1606
+ const value = normalized.slice(PLATFORM_LABEL_PREFIX.length);
1607
+ if (isPlatform(value)) {
1608
+ platforms.push(value);
1609
+ }
1610
+ continue;
1611
+ }
1612
+ if (normalized.startsWith(PRIORITY_LABEL_PREFIX)) {
1613
+ continue;
1614
+ }
1615
+ tags.push(normalized);
1616
+ }
1617
+ return {
1618
+ status,
1619
+ platforms: uniquePlatforms(platforms),
1620
+ tags: Array.from(new Set(tags))
1621
+ };
1244
1622
  }
1245
- async function writeIdea(idea, dir) {
1246
- const ideasDir = resolveIdeasDir(dir);
1247
- const ideaPath = getIdeaFilePath(idea.id, ideasDir);
1248
- const now = (/* @__PURE__ */ new Date()).toISOString();
1249
- if (!isValidIsoDateString(idea.publishBy)) {
1250
- throw new Error(`Invalid publishBy date: ${idea.publishBy}`);
1623
+ function formatIdeaBody(input) {
1624
+ const sections = [
1625
+ `${MARKDOWN_SECTION_PREFIX}Hook`,
1626
+ sanitizeMultilineValue(input.hook),
1627
+ "",
1628
+ `${MARKDOWN_SECTION_PREFIX}Audience`,
1629
+ sanitizeMultilineValue(input.audience),
1630
+ "",
1631
+ `${MARKDOWN_SECTION_PREFIX}Key Takeaway`,
1632
+ sanitizeMultilineValue(input.keyTakeaway),
1633
+ "",
1634
+ `${MARKDOWN_SECTION_PREFIX}Talking Points`,
1635
+ ...input.talkingPoints.map((point) => `- ${sanitizeMultilineValue(point)}`),
1636
+ "",
1637
+ `${MARKDOWN_SECTION_PREFIX}Publish By`,
1638
+ sanitizeMultilineValue(input.publishBy)
1639
+ ];
1640
+ const trendContext = sanitizeMultilineValue(input.trendContext);
1641
+ if (trendContext) {
1642
+ sections.push("", `${MARKDOWN_SECTION_PREFIX}Trend Context`, trendContext);
1643
+ }
1644
+ return sections.join("\n").trim();
1645
+ }
1646
+ function parseIdeaBody(body, fallbackPublishBy) {
1647
+ const normalizedBody = body.replace(/\r\n/g, "\n");
1648
+ const sections = /* @__PURE__ */ new Map();
1649
+ let currentSection = null;
1650
+ for (const line of normalizedBody.split("\n")) {
1651
+ if (line.startsWith(MARKDOWN_SECTION_PREFIX)) {
1652
+ currentSection = line.slice(MARKDOWN_SECTION_PREFIX.length).trim();
1653
+ sections.set(currentSection, []);
1654
+ continue;
1655
+ }
1656
+ if (currentSection) {
1657
+ sections.get(currentSection)?.push(line);
1658
+ }
1251
1659
  }
1252
- idea.updatedAt = now;
1253
- await ensureDirectory(ideasDir);
1254
- await writeJsonFile(ideaPath, idea);
1660
+ const getSection = (heading) => (sections.get(heading) ?? []).join("\n").trim();
1661
+ const talkingPointsSection = sections.get("Talking Points") ?? [];
1662
+ const talkingPoints = talkingPointsSection.map((line) => line.trim()).filter((line) => line.startsWith("- ") || line.startsWith("* ")).map((line) => line.slice(2).trim()).filter((line) => line.length > 0);
1663
+ return {
1664
+ hook: getSection("Hook"),
1665
+ audience: getSection("Audience"),
1666
+ keyTakeaway: getSection("Key Takeaway"),
1667
+ talkingPoints,
1668
+ publishBy: getSection("Publish By") || fallbackPublishBy,
1669
+ trendContext: getSection("Trend Context") || void 0
1670
+ };
1255
1671
  }
1256
- async function readIdea(id, dir) {
1257
- const ideaPath = getIdeaFilePath(id, dir);
1258
- if (!await fileExists(ideaPath)) {
1672
+ function formatIdeaComment(data) {
1673
+ return [
1674
+ COMMENT_MARKER,
1675
+ "```json",
1676
+ JSON.stringify(data, null, 2),
1677
+ "```"
1678
+ ].join("\n");
1679
+ }
1680
+ function formatPublishRecordComment(record) {
1681
+ return [
1682
+ "Published content recorded for this idea.",
1683
+ "",
1684
+ `- Clip type: ${record.clipType}`,
1685
+ `- Platform: ${record.platform}`,
1686
+ `- Queue item: ${record.queueItemId}`,
1687
+ `- Published at: ${record.publishedAt}`,
1688
+ `- Late post ID: ${record.latePostId}`,
1689
+ `- Late URL: ${record.lateUrl}`,
1690
+ "",
1691
+ formatIdeaComment({ type: "publish-record", record })
1692
+ ].join("\n");
1693
+ }
1694
+ function formatVideoLinkComment(videoSlug, linkedAt) {
1695
+ return [
1696
+ "Linked a source video to this idea.",
1697
+ "",
1698
+ `- Video slug: ${videoSlug}`,
1699
+ `- Linked at: ${linkedAt}`,
1700
+ "",
1701
+ formatIdeaComment({ type: "video-link", videoSlug, linkedAt })
1702
+ ].join("\n");
1703
+ }
1704
+ function parseIdeaComment(commentBody) {
1705
+ const markerIndex = commentBody.indexOf(COMMENT_MARKER);
1706
+ if (markerIndex === -1) {
1259
1707
  return null;
1260
1708
  }
1261
- const idea = await readJsonFile(ideaPath);
1262
- if (!isIdea(idea)) {
1263
- throw new Error(`File does not contain a valid idea: ${ideaPath}`);
1709
+ const commentPayload = commentBody.slice(markerIndex + COMMENT_MARKER.length);
1710
+ const fencedJsonMatch = commentPayload.match(/```json\s*([\s\S]*?)\s*```/);
1711
+ const jsonText = fencedJsonMatch?.[1]?.trim() ?? commentPayload.trim();
1712
+ if (!jsonText) {
1713
+ return null;
1714
+ }
1715
+ try {
1716
+ const parsed = JSON.parse(jsonText);
1717
+ if (parsed.type === "video-link" && typeof parsed.videoSlug === "string" && typeof parsed.linkedAt === "string") {
1718
+ return {
1719
+ type: "video-link",
1720
+ videoSlug: parsed.videoSlug,
1721
+ linkedAt: parsed.linkedAt
1722
+ };
1723
+ }
1724
+ if (parsed.type === "publish-record" && parsed.record) {
1725
+ const record = parsed.record;
1726
+ if (typeof record.clipType === "string" && typeof record.platform === "string" && isPlatform(record.platform) && typeof record.queueItemId === "string" && typeof record.publishedAt === "string" && typeof record.latePostId === "string" && typeof record.lateUrl === "string") {
1727
+ return {
1728
+ type: "publish-record",
1729
+ record: {
1730
+ clipType: record.clipType,
1731
+ platform: record.platform,
1732
+ queueItemId: record.queueItemId,
1733
+ publishedAt: record.publishedAt,
1734
+ latePostId: record.latePostId,
1735
+ lateUrl: record.lateUrl
1736
+ }
1737
+ };
1738
+ }
1739
+ }
1740
+ } catch {
1741
+ return null;
1264
1742
  }
1265
- return idea;
1743
+ return null;
1266
1744
  }
1267
- async function listIdeaIds(dir) {
1268
- const ideasDir = resolveIdeasDir(dir);
1269
- if (!await fileExists(ideasDir)) {
1745
+ function buildLabelFilters(filters) {
1746
+ if (!filters) {
1270
1747
  return [];
1271
1748
  }
1272
- const entries = await listDirectory(ideasDir);
1273
- return entries.filter((entry) => entry.toLowerCase().endsWith(IDEA_FILE_EXTENSION)).map((entry) => entry.slice(0, -IDEA_FILE_EXTENSION.length));
1749
+ const labels = [];
1750
+ if (filters.status) {
1751
+ labels.push(`${STATUS_LABEL_PREFIX}${filters.status}`);
1752
+ }
1753
+ if (filters.platform) {
1754
+ labels.push(`${PLATFORM_LABEL_PREFIX}${filters.platform}`);
1755
+ }
1756
+ if (filters.tag) {
1757
+ labels.push(normalizeTag(filters.tag));
1758
+ }
1759
+ if (filters.priority) {
1760
+ labels.push(`${PRIORITY_LABEL_PREFIX}${filters.priority}`);
1761
+ }
1762
+ return labels;
1763
+ }
1764
+ function buildLabelsFromIssue(issue, overrides = {}) {
1765
+ const parsedLabels = parseLabelsToIdea(issue.labels);
1766
+ const parsedBody = parseIdeaBody(issue.body, issue.created_at.slice(0, 10));
1767
+ return extractLabelsFromIdea(
1768
+ {
1769
+ status: overrides.status ?? parsedLabels.status,
1770
+ platforms: overrides.platforms ?? parsedLabels.platforms,
1771
+ tags: overrides.tags ?? parsedLabels.tags
1772
+ },
1773
+ parsedBody.publishBy,
1774
+ issue.created_at
1775
+ );
1776
+ }
1777
+ function isNotFoundError(error) {
1778
+ return error instanceof GitHubClientError && error.status === 404;
1779
+ }
1780
+ function mapIssueToIdea(issue, comments) {
1781
+ const config2 = getConfig();
1782
+ const parsedLabels = parseLabelsToIdea(issue.labels);
1783
+ const parsedBody = parseIdeaBody(issue.body, issue.created_at.slice(0, 10));
1784
+ const publishRecords = [];
1785
+ let sourceVideoSlug;
1786
+ for (const comment of comments) {
1787
+ const parsedComment = parseIdeaComment(comment.body);
1788
+ if (!parsedComment) {
1789
+ continue;
1790
+ }
1791
+ if (parsedComment.type === "publish-record") {
1792
+ publishRecords.push(parsedComment.record);
1793
+ continue;
1794
+ }
1795
+ sourceVideoSlug = parsedComment.videoSlug;
1796
+ }
1797
+ return {
1798
+ issueNumber: issue.number,
1799
+ issueUrl: issue.html_url,
1800
+ repoFullName: config2.IDEAS_REPO,
1801
+ id: `idea-${issue.number}`,
1802
+ topic: issue.title,
1803
+ ...parsedBody,
1804
+ ...parsedLabels,
1805
+ createdAt: issue.created_at,
1806
+ updatedAt: issue.updated_at,
1807
+ sourceVideoSlug,
1808
+ publishedContent: publishRecords.length > 0 ? publishRecords : void 0
1809
+ };
1810
+ }
1811
+ async function createIdea(input) {
1812
+ const client = getGitHubClient();
1813
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1814
+ try {
1815
+ const issue = await client.createIssue({
1816
+ title: input.topic,
1817
+ body: formatIdeaBody(input),
1818
+ labels: extractLabelsFromIdea(
1819
+ {
1820
+ status: "draft",
1821
+ platforms: input.platforms,
1822
+ tags: input.tags
1823
+ },
1824
+ input.publishBy,
1825
+ createdAt
1826
+ )
1827
+ });
1828
+ logger_default.info(`[IdeaService] Created idea #${issue.number}: ${input.topic}`);
1829
+ return mapIssueToIdea(issue, []);
1830
+ } catch (error) {
1831
+ const message = getErrorMessage2(error);
1832
+ logger_default.error(`[IdeaService] Failed to create idea "${input.topic}": ${message}`);
1833
+ throw new Error(`Failed to create idea "${input.topic}": ${message}`);
1834
+ }
1835
+ }
1836
+ async function updateIdea(issueNumber, updates) {
1837
+ const client = getGitHubClient();
1838
+ try {
1839
+ const currentIdea = await getIdea(issueNumber);
1840
+ if (!currentIdea) {
1841
+ throw new Error(`Idea #${issueNumber} was not found`);
1842
+ }
1843
+ const nextInput = {
1844
+ topic: updates.topic ?? currentIdea.topic,
1845
+ hook: updates.hook ?? currentIdea.hook,
1846
+ audience: updates.audience ?? currentIdea.audience,
1847
+ keyTakeaway: updates.keyTakeaway ?? currentIdea.keyTakeaway,
1848
+ talkingPoints: updates.talkingPoints ?? currentIdea.talkingPoints,
1849
+ platforms: updates.platforms ?? currentIdea.platforms,
1850
+ tags: updates.tags ?? currentIdea.tags,
1851
+ publishBy: updates.publishBy ?? currentIdea.publishBy,
1852
+ trendContext: updates.trendContext ?? currentIdea.trendContext
1853
+ };
1854
+ const shouldUpdateBody = updates.hook !== void 0 || updates.audience !== void 0 || updates.keyTakeaway !== void 0 || updates.talkingPoints !== void 0 || updates.publishBy !== void 0 || updates.trendContext !== void 0;
1855
+ const shouldUpdateLabels = updates.status !== void 0 || updates.platforms !== void 0 || updates.tags !== void 0 || updates.publishBy !== void 0;
1856
+ const issue = await client.updateIssue(issueNumber, {
1857
+ title: updates.topic,
1858
+ body: shouldUpdateBody ? formatIdeaBody(nextInput) : void 0,
1859
+ labels: shouldUpdateLabels ? extractLabelsFromIdea(
1860
+ {
1861
+ status: updates.status ?? currentIdea.status,
1862
+ platforms: nextInput.platforms,
1863
+ tags: nextInput.tags
1864
+ },
1865
+ nextInput.publishBy,
1866
+ currentIdea.createdAt
1867
+ ) : void 0
1868
+ });
1869
+ const comments = await client.listComments(issueNumber);
1870
+ return mapIssueToIdea(issue, comments);
1871
+ } catch (error) {
1872
+ const message = getErrorMessage2(error);
1873
+ logger_default.error(`[IdeaService] Failed to update idea #${issueNumber}: ${message}`);
1874
+ throw new Error(`Failed to update idea #${issueNumber}: ${message}`);
1875
+ }
1876
+ }
1877
+ async function getIdea(issueNumber) {
1878
+ const client = getGitHubClient();
1879
+ try {
1880
+ const [issue, comments] = await Promise.all([
1881
+ client.getIssue(issueNumber),
1882
+ client.listComments(issueNumber)
1883
+ ]);
1884
+ return mapIssueToIdea(issue, comments);
1885
+ } catch (error) {
1886
+ if (isNotFoundError(error)) {
1887
+ return null;
1888
+ }
1889
+ const message = getErrorMessage2(error);
1890
+ logger_default.error(`[IdeaService] Failed to get idea #${issueNumber}: ${message}`);
1891
+ throw new Error(`Failed to get idea #${issueNumber}: ${message}`);
1892
+ }
1893
+ }
1894
+ async function listIdeas(filters) {
1895
+ const client = getGitHubClient();
1896
+ try {
1897
+ const issues = await client.listIssues({
1898
+ labels: buildLabelFilters(filters),
1899
+ maxResults: filters?.limit
1900
+ });
1901
+ const ideas = await Promise.all(
1902
+ issues.map(async (issue) => {
1903
+ const comments = await client.listComments(issue.number);
1904
+ return mapIssueToIdea(issue, comments);
1905
+ })
1906
+ );
1907
+ return filters?.limit ? ideas.slice(0, filters.limit) : ideas;
1908
+ } catch (error) {
1909
+ const message = getErrorMessage2(error);
1910
+ logger_default.error(`[IdeaService] Failed to list ideas: ${message}`);
1911
+ throw new Error(`Failed to list ideas: ${message}`);
1912
+ }
1913
+ }
1914
+ async function searchIdeas(query) {
1915
+ const client = getGitHubClient();
1916
+ try {
1917
+ const issues = await client.searchIssues(query);
1918
+ return await Promise.all(
1919
+ issues.map(async (issue) => {
1920
+ const comments = await client.listComments(issue.number);
1921
+ return mapIssueToIdea(issue, comments);
1922
+ })
1923
+ );
1924
+ } catch (error) {
1925
+ const message = getErrorMessage2(error);
1926
+ logger_default.error(`[IdeaService] Failed to search ideas: ${message}`);
1927
+ throw new Error(`Failed to search ideas: ${message}`);
1928
+ }
1929
+ }
1930
+ async function findRelatedIdeas(idea) {
1931
+ const client = getGitHubClient();
1932
+ try {
1933
+ const relatedIssues = /* @__PURE__ */ new Map();
1934
+ for (const tag of normalizeTags(idea.tags)) {
1935
+ const matches = await client.listIssues({ labels: [tag], maxResults: 5 });
1936
+ for (const match of matches) {
1937
+ if (match.number !== idea.issueNumber) {
1938
+ relatedIssues.set(match.number, match);
1939
+ }
1940
+ }
1941
+ }
1942
+ const sortedIssues = Array.from(relatedIssues.values()).sort((left, right) => right.updated_at.localeCompare(left.updated_at)).slice(0, 5);
1943
+ return await Promise.all(
1944
+ sortedIssues.map(async (issue) => {
1945
+ const comments = await client.listComments(issue.number);
1946
+ return mapIssueToIdea(issue, comments);
1947
+ })
1948
+ );
1949
+ } catch (error) {
1950
+ const message = getErrorMessage2(error);
1951
+ logger_default.error(`[IdeaService] Failed to find related ideas for #${idea.issueNumber}: ${message}`);
1952
+ throw new Error(`Failed to find related ideas for #${idea.issueNumber}: ${message}`);
1953
+ }
1954
+ }
1955
+ async function linkVideoToIdea(issueNumber, videoSlug) {
1956
+ const client = getGitHubClient();
1957
+ try {
1958
+ const [issue] = await Promise.all([
1959
+ client.getIssue(issueNumber),
1960
+ client.addComment(issueNumber, formatVideoLinkComment(videoSlug, (/* @__PURE__ */ new Date()).toISOString()))
1961
+ ]);
1962
+ await client.updateIssue(issueNumber, {
1963
+ labels: buildLabelsFromIssue(issue, { status: "recorded" })
1964
+ });
1965
+ } catch (error) {
1966
+ const message = getErrorMessage2(error);
1967
+ logger_default.error(`[IdeaService] Failed to link video ${videoSlug} to idea #${issueNumber}: ${message}`);
1968
+ throw new Error(`Failed to link video ${videoSlug} to idea #${issueNumber}: ${message}`);
1969
+ }
1970
+ }
1971
+ async function recordPublish(issueNumber, record) {
1972
+ const client = getGitHubClient();
1973
+ try {
1974
+ const [issue, comments] = await Promise.all([
1975
+ client.getIssue(issueNumber),
1976
+ client.listComments(issueNumber)
1977
+ ]);
1978
+ const hasDuplicate = comments.some((comment) => {
1979
+ const parsedComment = parseIdeaComment(comment.body);
1980
+ return parsedComment?.type === "publish-record" && parsedComment.record.queueItemId === record.queueItemId;
1981
+ });
1982
+ if (!hasDuplicate) {
1983
+ await client.addComment(issueNumber, formatPublishRecordComment(record));
1984
+ }
1985
+ if (!issue.labels.includes(`${STATUS_LABEL_PREFIX}published`)) {
1986
+ await client.updateIssue(issueNumber, {
1987
+ labels: buildLabelsFromIssue(issue, { status: "published" })
1988
+ });
1989
+ }
1990
+ } catch (error) {
1991
+ const message = getErrorMessage2(error);
1992
+ logger_default.error(`[IdeaService] Failed to record publish for idea #${issueNumber}: ${message}`);
1993
+ throw new Error(`Failed to record publish for idea #${issueNumber}: ${message}`);
1994
+ }
1995
+ }
1996
+ async function getPublishHistory(issueNumber) {
1997
+ const client = getGitHubClient();
1998
+ try {
1999
+ const comments = await client.listComments(issueNumber);
2000
+ return comments.flatMap((comment) => {
2001
+ const parsedComment = parseIdeaComment(comment.body);
2002
+ return parsedComment?.type === "publish-record" ? [parsedComment.record] : [];
2003
+ });
2004
+ } catch (error) {
2005
+ const message = getErrorMessage2(error);
2006
+ logger_default.error(`[IdeaService] Failed to get publish history for idea #${issueNumber}: ${message}`);
2007
+ throw new Error(`Failed to get publish history for idea #${issueNumber}: ${message}`);
2008
+ }
2009
+ }
2010
+ async function getReadyIdeas() {
2011
+ return listIdeas({ status: "ready" });
1274
2012
  }
1275
- var DEFAULT_IDEAS_DIR, IDEA_FILE_EXTENSION, ideaStatuses, ideaClipTypes, ideaPlatforms;
1276
- var init_ideaStore = __esm({
1277
- "src/L1-infra/ideaStore/ideaStore.ts"() {
2013
+ async function markRecorded(issueNumber, videoSlug) {
2014
+ await linkVideoToIdea(issueNumber, videoSlug);
2015
+ }
2016
+ async function markPublished(issueNumber, record) {
2017
+ await recordPublish(issueNumber, record);
2018
+ }
2019
+ var STATUS_LABEL_PREFIX, PLATFORM_LABEL_PREFIX, PRIORITY_LABEL_PREFIX, COMMENT_MARKER, MARKDOWN_SECTION_PREFIX, platformValues, ideaStatuses;
2020
+ var init_ideaService = __esm({
2021
+ "src/L3-services/ideaService/ideaService.ts"() {
1278
2022
  "use strict";
1279
- init_fileSystem();
2023
+ init_types();
2024
+ init_githubClient();
2025
+ init_environment();
1280
2026
  init_configLogger();
1281
- init_paths();
1282
- DEFAULT_IDEAS_DIR = join(resolve("."), "ideas");
1283
- IDEA_FILE_EXTENSION = ".json";
2027
+ STATUS_LABEL_PREFIX = "status:";
2028
+ PLATFORM_LABEL_PREFIX = "platform:";
2029
+ PRIORITY_LABEL_PREFIX = "priority:";
2030
+ COMMENT_MARKER = "<!-- vidpipe:idea-comment -->";
2031
+ MARKDOWN_SECTION_PREFIX = "## ";
2032
+ platformValues = new Set(Object.values(Platform));
1284
2033
  ideaStatuses = /* @__PURE__ */ new Set(["draft", "ready", "recorded", "published"]);
1285
- ideaClipTypes = /* @__PURE__ */ new Set(["video", "short", "medium-clip"]);
1286
- ideaPlatforms = /* @__PURE__ */ new Set(["tiktok", "youtube", "instagram", "linkedin", "x"]);
1287
2034
  }
1288
2035
  });
1289
2036
 
1290
2037
  // src/L3-services/ideation/ideaService.ts
1291
- var ideaService_exports = {};
1292
- __export(ideaService_exports, {
2038
+ var ideaService_exports2 = {};
2039
+ __export(ideaService_exports2, {
1293
2040
  getIdeasByIds: () => getIdeasByIds,
1294
- getReadyIdeas: () => getReadyIdeas,
1295
- markPublished: () => markPublished,
1296
- markRecorded: () => markRecorded,
2041
+ getReadyIdeas: () => getReadyIdeas2,
2042
+ markPublished: () => markPublished2,
2043
+ markRecorded: () => markRecorded2,
1297
2044
  matchIdeasToTranscript: () => matchIdeasToTranscript
1298
2045
  });
1299
- async function getIdeasByIds(ids, dir) {
1300
- return Promise.all(
1301
- ids.map(async (id) => {
1302
- const idea = await readIdea(id, dir);
1303
- if (!idea) {
1304
- throw new Error(`Idea not found: ${id}`);
1305
- }
2046
+ function normalizeIdeaIdentifier(id) {
2047
+ return id.trim();
2048
+ }
2049
+ function buildIdeaLookup(ideas) {
2050
+ const lookup = /* @__PURE__ */ new Map();
2051
+ for (const idea of ideas) {
2052
+ lookup.set(idea.id, idea);
2053
+ lookup.set(String(idea.issueNumber), idea);
2054
+ }
2055
+ return lookup;
2056
+ }
2057
+ async function resolveIdeaByIdentifier(id, ideas) {
2058
+ const normalizedId = normalizeIdeaIdentifier(id);
2059
+ if (!normalizedId) {
2060
+ return null;
2061
+ }
2062
+ const issueNumber = Number.parseInt(normalizedId, 10);
2063
+ if (Number.isInteger(issueNumber)) {
2064
+ const idea = await getIdea(issueNumber);
2065
+ if (idea) {
1306
2066
  return idea;
1307
- })
1308
- );
2067
+ }
2068
+ }
2069
+ const availableIdeas = ideas ?? await listIdeas();
2070
+ return buildIdeaLookup(availableIdeas).get(normalizedId) ?? null;
1309
2071
  }
1310
- async function getReadyIdeas(dir) {
1311
- const ideas = await readIdeaBank(dir);
1312
- return ideas.filter((idea) => idea.status === "ready");
2072
+ async function getIdeasByIds(ids, _dir) {
2073
+ const ideas = await listIdeas();
2074
+ const lookup = buildIdeaLookup(ideas);
2075
+ return ids.map((id) => {
2076
+ const normalizedId = normalizeIdeaIdentifier(id);
2077
+ const idea = lookup.get(normalizedId);
2078
+ if (!idea) {
2079
+ throw new Error(`Idea not found: ${id}`);
2080
+ }
2081
+ return idea;
2082
+ });
2083
+ }
2084
+ async function getReadyIdeas2(_dir) {
2085
+ return getReadyIdeas();
1313
2086
  }
1314
- async function markRecorded(id, videoSlug, dir) {
1315
- const idea = await readIdea(id, dir);
2087
+ async function markRecorded2(id, videoSlug, _dir) {
2088
+ const idea = await resolveIdeaByIdentifier(id);
1316
2089
  if (!idea) {
1317
2090
  throw new Error(`Idea not found: ${id}`);
1318
2091
  }
1319
- idea.status = "recorded";
1320
- idea.sourceVideoSlug = videoSlug;
1321
- await writeIdea(idea, dir);
2092
+ await markRecorded(idea.issueNumber, videoSlug);
1322
2093
  }
1323
- async function markPublished(id, record, dir) {
1324
- const idea = await readIdea(id, dir);
2094
+ async function markPublished2(id, record, _dir) {
2095
+ const idea = await resolveIdeaByIdentifier(id);
1325
2096
  if (!idea) {
1326
2097
  throw new Error(`Idea not found: ${id}`);
1327
2098
  }
1328
- idea.publishedContent = [...idea.publishedContent ?? [], record];
1329
- idea.status = "published";
1330
- await writeIdea(idea, dir);
2099
+ await markPublished(idea.issueNumber, record);
1331
2100
  }
1332
- async function matchIdeasToTranscript(transcript, ideas, dir) {
2101
+ async function matchIdeasToTranscript(transcript, ideas, _dir) {
1333
2102
  try {
1334
- const readyIdeas = (ideas ?? await readIdeaBank(dir)).filter((idea) => idea.status === "ready");
2103
+ const readyIdeas = (ideas ?? await getReadyIdeas()).filter((idea) => idea.status === "ready");
1335
2104
  if (readyIdeas.length === 0) {
1336
2105
  return [];
1337
2106
  }
@@ -1349,9 +2118,7 @@ async function matchIdeasToTranscript(transcript, ideas, dir) {
1349
2118
  hook: idea.hook,
1350
2119
  keyTakeaway: idea.keyTakeaway
1351
2120
  }));
1352
- const knownIdeaIds = new Set(
1353
- ideas ? readyIdeaIds : await listIdeaIds(dir)
1354
- );
2121
+ const knownIdeaIds = readyIdeaIds;
1355
2122
  const session = await provider.createSession({
1356
2123
  systemPrompt: MATCH_IDEAS_SYSTEM_PROMPT,
1357
2124
  tools: [],
@@ -1370,7 +2137,7 @@ async function matchIdeasToTranscript(transcript, ideas, dir) {
1370
2137
  return matchedIdea ? [matchedIdea] : [];
1371
2138
  });
1372
2139
  }
1373
- return await getIdeasByIds(matchedIds, dir);
2140
+ return await getIdeasByIds(matchedIds);
1374
2141
  } finally {
1375
2142
  await session.close().catch((error) => {
1376
2143
  const message = error instanceof Error ? error.message : String(error);
@@ -1403,12 +2170,12 @@ function parseMatchedIdeaIds(rawContent, knownIdeaIds) {
1403
2170
  return Array.from(new Set(matchedIds.filter((id) => knownIdeaIds.has(id))));
1404
2171
  }
1405
2172
  var IDEA_MATCH_AGENT_NAME, IDEA_MATCH_LIMIT, TRANSCRIPT_SUMMARY_LIMIT, MATCH_IDEAS_SYSTEM_PROMPT;
1406
- var init_ideaService = __esm({
2173
+ var init_ideaService2 = __esm({
1407
2174
  "src/L3-services/ideation/ideaService.ts"() {
1408
2175
  "use strict";
1409
2176
  init_modelConfig();
1410
- init_ideaStore();
1411
2177
  init_configLogger();
2178
+ init_ideaService();
1412
2179
  init_providerFactory();
1413
2180
  IDEA_MATCH_AGENT_NAME = "IdeaService";
1414
2181
  IDEA_MATCH_LIMIT = 3;
@@ -1440,6 +2207,7 @@ init_environment();
1440
2207
  init_paths();
1441
2208
  init_fileSystem();
1442
2209
  init_configLogger();
2210
+ init_types();
1443
2211
  var FileWatcher = class _FileWatcher extends EventEmitter {
1444
2212
  watchFolder;
1445
2213
  watcher = null;
@@ -1469,8 +2237,8 @@ var FileWatcher = class _FileWatcher extends EventEmitter {
1469
2237
  }
1470
2238
  }
1471
2239
  async handleDetectedFile(filePath) {
1472
- if (extname(filePath).toLowerCase() !== ".mp4") {
1473
- logger_default.debug(`[watcher] Ignoring non-mp4 file: ${filePath}`);
2240
+ if (!isSupportedVideoExtension(extname(filePath).toLowerCase())) {
2241
+ logger_default.debug(`[watcher] Ignoring unsupported file: ${filePath}`);
1474
2242
  return;
1475
2243
  }
1476
2244
  let fileSize;
@@ -1505,7 +2273,7 @@ var FileWatcher = class _FileWatcher extends EventEmitter {
1505
2273
  throw err;
1506
2274
  }
1507
2275
  for (const file of files) {
1508
- if (extname(file).toLowerCase() === ".mp4") {
2276
+ if (isSupportedVideoExtension(extname(file).toLowerCase())) {
1509
2277
  const filePath = join(this.watchFolder, file);
1510
2278
  this.handleDetectedFile(filePath).catch(
1511
2279
  (err) => logger_default.error(`Error processing ${filePath}: ${err instanceof Error ? err.message : String(err)}`)
@@ -1535,7 +2303,7 @@ var FileWatcher = class _FileWatcher extends EventEmitter {
1535
2303
  });
1536
2304
  this.watcher.on("change", (filePath) => {
1537
2305
  logger_default.debug(`[watcher] 'change' event: ${filePath}`);
1538
- if (extname(filePath).toLowerCase() !== ".mp4") return;
2306
+ if (!isSupportedVideoExtension(extname(filePath).toLowerCase())) return;
1539
2307
  logger_default.info(`Change detected on video file: ${filePath}`);
1540
2308
  this.handleDetectedFile(filePath).catch(
1541
2309
  (err) => logger_default.error(`Error processing ${filePath}: ${err instanceof Error ? err.message : String(err)}`)
@@ -1556,7 +2324,7 @@ var FileWatcher = class _FileWatcher extends EventEmitter {
1556
2324
  this.scanExistingFiles();
1557
2325
  }
1558
2326
  });
1559
- logger_default.info(`Watching for new .mp4 files in: ${this.watchFolder}`);
2327
+ logger_default.info(`Watching for new video files in: ${this.watchFolder}`);
1560
2328
  }
1561
2329
  stop() {
1562
2330
  if (this.watcher) {
@@ -2880,6 +3648,41 @@ async function compositeOverlays(videoPath, overlays, outputPath, videoWidth, vi
2880
3648
  });
2881
3649
  }
2882
3650
 
3651
+ // src/L2-clients/ffmpeg/transcoding.ts
3652
+ init_fileSystem();
3653
+ init_paths();
3654
+ init_configLogger();
3655
+ function transcodeToMp4(inputPath, outputPath) {
3656
+ const outputDir = dirname(outputPath);
3657
+ return new Promise((resolve3, reject) => {
3658
+ ensureDirectory(outputDir).then(() => {
3659
+ logger_default.info(`Transcoding to MP4: ${inputPath} \u2192 ${outputPath}`);
3660
+ createFFmpeg(inputPath).outputOptions([
3661
+ "-c:v",
3662
+ "libx264",
3663
+ "-preset",
3664
+ "ultrafast",
3665
+ "-crf",
3666
+ "23",
3667
+ "-threads",
3668
+ "4",
3669
+ "-c:a",
3670
+ "aac",
3671
+ "-b:a",
3672
+ "128k",
3673
+ "-movflags",
3674
+ "+faststart"
3675
+ ]).output(outputPath).on("end", () => {
3676
+ logger_default.info(`Transcoding complete: ${outputPath}`);
3677
+ resolve3(outputPath);
3678
+ }).on("error", (err) => {
3679
+ logger_default.error(`Transcoding failed: ${err.message}`);
3680
+ reject(new Error(`Transcoding to MP4 failed: ${err.message}`));
3681
+ }).run();
3682
+ }).catch(reject);
3683
+ });
3684
+ }
3685
+
2883
3686
  // src/L3-services/videoOperations/videoOperations.ts
2884
3687
  function ffprobe2(...args) {
2885
3688
  return ffprobe(...args);
@@ -2917,6 +3720,9 @@ function getVideoResolution2(...args) {
2917
3720
  function compositeOverlays2(...args) {
2918
3721
  return compositeOverlays(...args);
2919
3722
  }
3723
+ function transcodeToMp42(...args) {
3724
+ return transcodeToMp4(...args);
3725
+ }
2920
3726
 
2921
3727
  // src/L0-pure/captions/captionGenerator.ts
2922
3728
  function pad(n, width) {
@@ -4367,46 +5173,7 @@ var SocialPostAsset = class extends TextAsset {
4367
5173
  // src/L5-assets/ShortVideoAsset.ts
4368
5174
  init_paths();
4369
5175
  init_fileSystem();
4370
-
4371
- // src/L0-pure/types/index.ts
4372
- var Platform = /* @__PURE__ */ ((Platform2) => {
4373
- Platform2["TikTok"] = "tiktok";
4374
- Platform2["YouTube"] = "youtube";
4375
- Platform2["Instagram"] = "instagram";
4376
- Platform2["LinkedIn"] = "linkedin";
4377
- Platform2["X"] = "x";
4378
- return Platform2;
4379
- })(Platform || {});
4380
- var PLATFORM_CHAR_LIMITS = {
4381
- tiktok: 2200,
4382
- youtube: 5e3,
4383
- instagram: 2200,
4384
- linkedin: 3e3,
4385
- twitter: 280
4386
- };
4387
- function toLatePlatform(platform) {
4388
- return platform === "x" /* X */ ? "twitter" : platform;
4389
- }
4390
- function fromLatePlatform(latePlatform) {
4391
- const normalized = normalizePlatformString(latePlatform);
4392
- if (normalized === "twitter") {
4393
- return "x" /* X */;
4394
- }
4395
- const platformValues = Object.values(Platform);
4396
- if (platformValues.includes(normalized)) {
4397
- return normalized;
4398
- }
4399
- throw new Error(`Unsupported platform from Late API: ${latePlatform}`);
4400
- }
4401
- function normalizePlatformString(raw) {
4402
- const lower = raw.toLowerCase().trim();
4403
- if (lower === "x" || lower === "x (twitter)" || lower === "x/twitter") {
4404
- return "twitter";
4405
- }
4406
- return lower;
4407
- }
4408
-
4409
- // src/L5-assets/ShortVideoAsset.ts
5176
+ init_types();
4410
5177
  var ShortVideoAsset = class extends VideoAsset {
4411
5178
  /** Reference to the source video this short was extracted from */
4412
5179
  parent;
@@ -4539,6 +5306,7 @@ var ShortVideoAsset = class extends VideoAsset {
4539
5306
  // src/L5-assets/MediumClipAsset.ts
4540
5307
  init_paths();
4541
5308
  init_fileSystem();
5309
+ init_types();
4542
5310
  var MediumClipAsset = class extends VideoAsset {
4543
5311
  /** Parent video this clip was extracted from */
4544
5312
  parent;
@@ -6738,6 +7506,7 @@ init_fileSystem();
6738
7506
  init_paths();
6739
7507
  init_configLogger();
6740
7508
  init_environment();
7509
+ init_types();
6741
7510
  var SYSTEM_PROMPT5 = `You are a viral social-media content strategist.
6742
7511
  Given a video transcript and summary you MUST generate one post for each of the 5 platforms listed below.
6743
7512
  Each post must match the platform's tone, format, and constraints exactly.
@@ -7249,8 +8018,11 @@ async function commitAndPush(videoSlug, message) {
7249
8018
  init_fileSystem();
7250
8019
  init_paths();
7251
8020
  init_configLogger();
8021
+ init_types();
8022
+ init_types();
7252
8023
 
7253
8024
  // src/L3-services/socialPosting/platformContentStrategy.ts
8025
+ init_types();
7254
8026
  var CONTENT_MATRIX = {
7255
8027
  ["youtube" /* YouTube */]: {
7256
8028
  video: { captions: true, variantKey: null },
@@ -7283,6 +8055,7 @@ function platformAcceptsMedia(platform, clipType) {
7283
8055
  }
7284
8056
 
7285
8057
  // src/L3-services/postStore/postStore.ts
8058
+ init_types();
7286
8059
  init_environment();
7287
8060
  init_configLogger();
7288
8061
  init_fileSystem();
@@ -7491,14 +8264,40 @@ async function approveItem(id, publishData) {
7491
8264
  item.metadata.reviewedAt = now;
7492
8265
  if (item.metadata.ideaIds && item.metadata.ideaIds.length > 0) {
7493
8266
  try {
7494
- const { markPublished: markPublished2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
7495
- for (const ideaId of item.metadata.ideaIds) {
7496
- await markPublished2(ideaId, {
8267
+ const { getIdea: getIdea2, listIdeas: listIdeas2, markPublished: markPublished3 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
8268
+ let cachedIdeas;
8269
+ for (const rawIdeaId of item.metadata.ideaIds) {
8270
+ const normalizedIdeaId = String(rawIdeaId).trim();
8271
+ if (!normalizedIdeaId) {
8272
+ continue;
8273
+ }
8274
+ const parsedIssueNumber = Number.parseInt(normalizedIdeaId, 10);
8275
+ let issueNumber;
8276
+ if (Number.isInteger(parsedIssueNumber)) {
8277
+ issueNumber = parsedIssueNumber;
8278
+ } else {
8279
+ if (!cachedIdeas) {
8280
+ const ideas = await listIdeas2();
8281
+ cachedIdeas = new Map(ideas.flatMap((idea2) => [[idea2.id, idea2.issueNumber], [String(idea2.issueNumber), idea2.issueNumber]]));
8282
+ }
8283
+ issueNumber = cachedIdeas.get(normalizedIdeaId);
8284
+ }
8285
+ if (!issueNumber) {
8286
+ logger_default.warn(`Skipping publish record for unknown idea identifier: ${normalizedIdeaId}`);
8287
+ continue;
8288
+ }
8289
+ const idea = await getIdea2(issueNumber);
8290
+ if (!idea) {
8291
+ logger_default.warn(`Skipping publish record for missing idea #${issueNumber}`);
8292
+ continue;
8293
+ }
8294
+ await markPublished3(issueNumber, {
7497
8295
  clipType: item.metadata.clipType,
7498
8296
  platform: fromLatePlatform(item.metadata.platform),
7499
8297
  queueItemId: id,
7500
8298
  publishedAt: now,
7501
- publishedUrl: item.metadata.publishedUrl ?? void 0
8299
+ latePostId: item.metadata.latePostId ?? "",
8300
+ lateUrl: item.metadata.publishedUrl || (item.metadata.latePostId ? `https://app.late.co/dashboard/post/${item.metadata.latePostId}` : "")
7502
8301
  });
7503
8302
  }
7504
8303
  } catch (err) {
@@ -8085,6 +8884,7 @@ async function enhanceVideo(videoPath, transcript, video) {
8085
8884
  // src/L5-assets/MainVideoAsset.ts
8086
8885
  init_environment();
8087
8886
  init_configLogger();
8887
+ init_types();
8088
8888
  var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
8089
8889
  sourcePath;
8090
8890
  videoDir;
@@ -8231,26 +9031,40 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
8231
9031
  await ensureDirectory(socialPostsDir);
8232
9032
  const destFilename = `${slug}.mp4`;
8233
9033
  const destPath = join(videoDir, destFilename);
8234
- let needsCopy = true;
9034
+ const sourceExt = extname(sourcePath).toLowerCase();
9035
+ const needsTranscode = sourceExt !== ".mp4";
9036
+ let needsIngest = true;
8235
9037
  try {
8236
9038
  const destStats = await getFileStats(destPath);
8237
- const srcStats = await getFileStats(sourcePath);
8238
- if (destStats.size === srcStats.size) {
8239
- logger_default.info(`Video already copied (same size), skipping copy`);
8240
- needsCopy = false;
9039
+ if (needsTranscode) {
9040
+ if (destStats.size > 1024) {
9041
+ logger_default.info(`Transcoded MP4 already exists (${(destStats.size / 1024 / 1024).toFixed(1)} MB), skipping transcode`);
9042
+ needsIngest = false;
9043
+ }
9044
+ } else {
9045
+ const srcStats = await getFileStats(sourcePath);
9046
+ if (destStats.size === srcStats.size) {
9047
+ logger_default.info(`Video already copied (same size), skipping copy`);
9048
+ needsIngest = false;
9049
+ }
8241
9050
  }
8242
9051
  } catch {
8243
9052
  }
8244
- if (needsCopy) {
8245
- await new Promise((resolve3, reject) => {
8246
- const readStream = openReadStream(sourcePath);
8247
- const writeStream = openWriteStream(destPath);
8248
- readStream.on("error", reject);
8249
- writeStream.on("error", reject);
8250
- writeStream.on("finish", resolve3);
8251
- readStream.pipe(writeStream);
8252
- });
8253
- logger_default.info(`Copied video to ${destPath}`);
9053
+ if (needsIngest) {
9054
+ if (needsTranscode) {
9055
+ await transcodeToMp42(sourcePath, destPath);
9056
+ logger_default.info(`Transcoded video to ${destPath}`);
9057
+ } else {
9058
+ await new Promise((resolve3, reject) => {
9059
+ const readStream = openReadStream(sourcePath);
9060
+ const writeStream = openWriteStream(destPath);
9061
+ readStream.on("error", reject);
9062
+ writeStream.on("error", reject);
9063
+ writeStream.on("finish", resolve3);
9064
+ readStream.pipe(writeStream);
9065
+ });
9066
+ logger_default.info(`Copied video to ${destPath}`);
9067
+ }
8254
9068
  }
8255
9069
  const asset = new _MainVideoAsset(sourcePath, videoDir, slug);
8256
9070
  try {
@@ -8957,7 +9771,7 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
8957
9771
  */
8958
9772
  async buildPublishQueueData(shorts, mediumClips, socialPosts, captionedVideoPath) {
8959
9773
  const video = await this.toVideoFile();
8960
- const ideaIds = this._ideas.length > 0 ? this._ideas.map((idea) => idea.id) : void 0;
9774
+ const ideaIds = this._ideas.length > 0 ? this._ideas.map((idea) => String(idea.issueNumber)) : void 0;
8961
9775
  return buildPublishQueue2(video, shorts, mediumClips, socialPosts, captionedVideoPath, ideaIds);
8962
9776
  }
8963
9777
  /**
@@ -10677,17 +11491,26 @@ var ScheduleAgent = class extends BaseAgent {
10677
11491
  init_fileSystem();
10678
11492
  init_environment();
10679
11493
  init_modelConfig();
10680
- init_ideaStore();
10681
11494
  init_configLogger();
11495
+ init_ideaService();
10682
11496
  init_providerFactory();
11497
+ init_types();
10683
11498
  var BASE_SYSTEM_PROMPT = `You are a content strategist for a tech content creator. Your role is to research trending topics, analyze what's working, and generate compelling video ideas grounded in real-world data.
10684
11499
 
10685
11500
  ## CRITICAL: Research Before Creating
10686
11501
  You MUST research before creating ideas. Do NOT skip the research phase. Ideas generated without research will be generic and stale. The value you provide is grounding ideas in what's ACTUALLY trending right now.
10687
11502
 
11503
+ ## GitHub Issue Workflow
11504
+ Ideas are stored as GitHub Issues in a dedicated repository. Treat the issue tracker as the source of truth:
11505
+ - Use get_past_ideas to inspect the current issue backlog with optional filters.
11506
+ - Use search_ideas for full-text lookups before creating something new.
11507
+ - Use find_related_ideas to cluster overlapping ideas by tags and avoid duplicates.
11508
+ - Use create_idea to create new draft issues.
11509
+ - Use update_idea or organize_ideas when an existing issue should be refined instead of creating a duplicate.
11510
+
10688
11511
  ## Your Research Process
10689
11512
  1. Load the brand context (get_brand_context) to understand the creator's voice, expertise, and content pillars.
10690
- 2. Check existing ideas (get_past_ideas) to avoid duplicates.
11513
+ 2. Check existing GitHub issue ideas (get_past_ideas and search_ideas) to avoid duplicates.
10691
11514
  3. **RESEARCH PHASE** \u2014 This is the most important step. Use the available MCP tools:
10692
11515
  - **web_search_exa**: Search for trending topics, viral content, recent announcements, and hot takes in the creator's niche. Search for specific topics from the creator's content pillars.
10693
11516
  - **youtube_search_videos** or **youtube_search**: Find what videos are performing well right now. Look at view counts, recent uploads on trending topics, and gaps in existing content.
@@ -10713,15 +11536,27 @@ Every idea must:
10713
11536
  - Written (LinkedIn, X/Twitter): Thought leadership, hot takes, thread-worthy
10714
11537
 
10715
11538
  Generate 3-5 high-quality ideas. Quality over quantity. Every idea must be backed by research.`;
10716
- var SUPPORTED_PLATFORMS = ["tiktok", "youtube", "instagram", "linkedin", "x"];
11539
+ var SUPPORTED_PLATFORMS = [
11540
+ "tiktok" /* TikTok */,
11541
+ "youtube" /* YouTube */,
11542
+ "instagram" /* Instagram */,
11543
+ "linkedin" /* LinkedIn */,
11544
+ "x" /* X */
11545
+ ];
11546
+ var SUPPORTED_STATUSES = ["draft", "ready", "recorded", "published"];
11547
+ var SUPPORTED_PRIORITIES = ["hot-trend", "timely", "evergreen"];
10717
11548
  var MIN_IDEA_COUNT = 3;
10718
11549
  var MAX_IDEA_COUNT = 5;
10719
- function isRecord2(value) {
11550
+ var DEFAULT_EXISTING_IDEA_LIMIT = 50;
11551
+ function isRecord(value) {
10720
11552
  return typeof value === "object" && value !== null;
10721
11553
  }
10722
- function isStringArray2(value) {
11554
+ function isStringArray(value) {
10723
11555
  return Array.isArray(value) && value.every((item) => typeof item === "string");
10724
11556
  }
11557
+ function hasField(source, field) {
11558
+ return Object.prototype.hasOwnProperty.call(source, field);
11559
+ }
10725
11560
  function normalizeCount(count) {
10726
11561
  if (typeof count !== "number" || Number.isNaN(count)) {
10727
11562
  return MIN_IDEA_COUNT;
@@ -10734,7 +11569,7 @@ function normalizeSeedTopics(seedTopics) {
10734
11569
  }
10735
11570
  function extractStringArrayField(source, field) {
10736
11571
  const value = source[field];
10737
- return isStringArray2(value) ? value : [];
11572
+ return isStringArray(value) ? value : [];
10738
11573
  }
10739
11574
  function extractContentPillars(brand) {
10740
11575
  const raw = brand.contentPillars;
@@ -10746,7 +11581,7 @@ function extractContentPillars(brand) {
10746
11581
  const pillar2 = entry.trim();
10747
11582
  return pillar2 ? [{ pillar: pillar2 }] : [];
10748
11583
  }
10749
- if (!isRecord2(entry)) {
11584
+ if (!isRecord(entry)) {
10750
11585
  return [];
10751
11586
  }
10752
11587
  const pillar = typeof entry.pillar === "string" ? entry.pillar.trim() : "";
@@ -10755,21 +11590,22 @@ function extractContentPillars(brand) {
10755
11590
  }
10756
11591
  const description = typeof entry.description === "string" ? entry.description.trim() : void 0;
10757
11592
  const frequency = typeof entry.frequency === "string" ? entry.frequency.trim() : void 0;
10758
- const formats = isStringArray2(entry.formats) ? entry.formats.map((format) => format.trim()).filter((format) => format.length > 0) : void 0;
11593
+ const formats = isStringArray(entry.formats) ? entry.formats.map((format) => format.trim()).filter((format) => format.length > 0) : void 0;
10759
11594
  return [{ pillar, description, frequency, formats }];
10760
11595
  });
10761
11596
  }
10762
11597
  function summarizeExistingIdeas(ideas) {
10763
11598
  if (ideas.length === 0) {
10764
- return "No existing ideas found in the bank.";
11599
+ return "No existing GitHub issue ideas found in the repository.";
10765
11600
  }
10766
- return ideas.slice(0, 25).map((idea) => `- ${idea.id}: ${idea.topic} [${idea.status}]`).join("\n");
11601
+ return ideas.slice(0, 25).map((idea) => `- #${idea.issueNumber}: ${idea.topic} [${idea.status}] (${idea.issueUrl})`).join("\n");
10767
11602
  }
10768
11603
  function buildPlatformGuidance() {
10769
11604
  return [
10770
11605
  `Allowed platforms for create_idea: ${SUPPORTED_PLATFORMS.join(", ")}`,
10771
11606
  `Create between ${MIN_IDEA_COUNT} and ${MAX_IDEA_COUNT} ideas unless the user explicitly requests fewer within that range.`,
10772
- "Call create_idea once per idea, then call finalize_ideas exactly once when done."
11607
+ "Call create_idea once per new idea issue, then call finalize_ideas exactly once when done.",
11608
+ "Prefer update_idea or organize_ideas when you discover that a GitHub issue already covers the concept."
10773
11609
  ].join("\n");
10774
11610
  }
10775
11611
  function buildBrandPromptSection(brand) {
@@ -10803,20 +11639,33 @@ function buildBrandPromptSection(brand) {
10803
11639
  lines.push("Content pillars:");
10804
11640
  lines.push(
10805
11641
  ...contentPillars.map((pillar) => {
10806
- const details = [pillar.description, pillar.frequency && `Frequency: ${pillar.frequency}`, pillar.formats?.length ? `Formats: ${pillar.formats.join(", ")}` : void 0].filter((value) => typeof value === "string" && value.length > 0).join(" | ");
11642
+ const details = [
11643
+ pillar.description,
11644
+ pillar.frequency && `Frequency: ${pillar.frequency}`,
11645
+ pillar.formats?.length ? `Formats: ${pillar.formats.join(", ")}` : void 0
11646
+ ].filter((value) => typeof value === "string" && value.length > 0).join(" | ");
10807
11647
  return details ? `- ${pillar.pillar}: ${details}` : `- ${pillar.pillar}`;
10808
11648
  })
10809
11649
  );
10810
11650
  }
10811
11651
  return lines.join("\n");
10812
11652
  }
10813
- function buildSystemPrompt3(brand, existingIdeas, seedTopics, count) {
11653
+ function buildIdeaRepoPromptSection(ideaRepo) {
11654
+ return [
11655
+ "## GitHub Idea Repository",
11656
+ `Dedicated issue repo: ${ideaRepo}`,
11657
+ "Every idea is a GitHub Issue. The issue tracker is the source of truth for duplicates, lifecycle status, tags, and related concepts."
11658
+ ].join("\n");
11659
+ }
11660
+ function buildSystemPrompt3(brand, existingIdeas, seedTopics, count, ideaRepo) {
10814
11661
  const promptSections = [
10815
11662
  BASE_SYSTEM_PROMPT,
10816
11663
  "",
11664
+ buildIdeaRepoPromptSection(ideaRepo),
11665
+ "",
10817
11666
  buildBrandPromptSection(brand),
10818
11667
  "",
10819
- "## Existing Idea Bank",
11668
+ "## Existing Idea Issues",
10820
11669
  summarizeExistingIdeas(existingIdeas),
10821
11670
  "",
10822
11671
  "## Planning Constraints",
@@ -10832,7 +11681,7 @@ function buildUserMessage(count, seedTopics, hasMcpServers) {
10832
11681
  const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
10833
11682
  const steps = [
10834
11683
  "1. Call get_brand_context to load the creator profile.",
10835
- "2. Call get_past_ideas to see what already exists."
11684
+ "2. Call get_past_ideas (and search_ideas if needed) to inspect existing GitHub issue ideas before proposing anything new."
10836
11685
  ];
10837
11686
  if (hasMcpServers) {
10838
11687
  steps.push(
@@ -10842,12 +11691,14 @@ function buildUserMessage(count, seedTopics, hasMcpServers) {
10842
11691
  " - Use perplexity-search to get current analysis on promising topics.",
10843
11692
  " Do at least 2-3 research queries. Each idea you create MUST reference specific findings from this research in its trendContext field.",
10844
11693
  `4. Call create_idea for each of the ${count} ideas, grounding each in your research findings.`,
10845
- "5. Call finalize_ideas when done."
11694
+ "5. If you uncover overlap with existing issues, prefer update_idea or organize_ideas over creating duplicates.",
11695
+ "6. Call finalize_ideas when done."
10846
11696
  );
10847
11697
  } else {
10848
11698
  steps.push(
10849
11699
  `3. Call create_idea for each of the ${count} ideas.`,
10850
- "4. Call finalize_ideas when done."
11700
+ "4. If you uncover overlap with existing issues, prefer update_idea or organize_ideas over creating duplicates.",
11701
+ "5. Call finalize_ideas when done."
10851
11702
  );
10852
11703
  }
10853
11704
  return [
@@ -10864,50 +11715,181 @@ async function loadBrandContext(brandPath) {
10864
11715
  }
10865
11716
  return readJsonFile(brandPath);
10866
11717
  }
10867
- function normalizePlatforms(platforms) {
10868
- const normalized = platforms.map((platform) => platform.trim().toLowerCase());
10869
- const invalid = normalized.filter((platform) => !SUPPORTED_PLATFORMS.includes(platform));
10870
- if (invalid.length > 0) {
10871
- throw new Error(`Unsupported platforms: ${invalid.join(", ")}`);
11718
+ function normalizeRequiredString(value, field) {
11719
+ if (typeof value !== "string") {
11720
+ throw new Error(`Invalid ${field}: expected string`);
11721
+ }
11722
+ const normalized = value.trim();
11723
+ if (!normalized) {
11724
+ throw new Error(`Invalid ${field}: value cannot be empty`);
10872
11725
  }
10873
11726
  return normalized;
10874
11727
  }
10875
- function assertKebabCaseId(id) {
10876
- const normalized = id.trim();
10877
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalized)) {
10878
- throw new Error(`Idea ID must be kebab-case: ${id}`);
11728
+ function normalizeOptionalString(value, field) {
11729
+ if (value === void 0) {
11730
+ return void 0;
11731
+ }
11732
+ if (typeof value !== "string") {
11733
+ throw new Error(`Invalid ${field}: expected string`);
11734
+ }
11735
+ const normalized = value.trim();
11736
+ return normalized || void 0;
11737
+ }
11738
+ function normalizeStringList(value, field) {
11739
+ if (!isStringArray(value)) {
11740
+ throw new Error(`Invalid ${field}: expected string[]`);
10879
11741
  }
10880
- return normalized;
11742
+ return value.map((item) => item.trim()).filter((item) => item.length > 0);
10881
11743
  }
10882
- function buildIdea(args) {
10883
- const now = (/* @__PURE__ */ new Date()).toISOString();
10884
- const publishBy = args.publishBy.trim();
10885
- if (args.hook.trim().length > 80) {
10886
- throw new Error(`Idea hook must be 80 characters or fewer: ${args.id}`);
11744
+ function normalizeIssueNumber(value) {
11745
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
11746
+ throw new Error("Invalid issueNumber: expected a positive integer");
10887
11747
  }
11748
+ return value;
11749
+ }
11750
+ function normalizePublishBy(value, field = "publishBy") {
11751
+ const publishBy = normalizeRequiredString(value, field);
10888
11752
  if (Number.isNaN(new Date(publishBy).getTime())) {
10889
- throw new Error(`Invalid publishBy date: ${args.publishBy}`);
11753
+ throw new Error(`Invalid ${field} date: ${publishBy}`);
11754
+ }
11755
+ return publishBy;
11756
+ }
11757
+ function normalizePlatforms(platforms) {
11758
+ const values = normalizeStringList(platforms, "platforms").map((platform) => platform.toLowerCase());
11759
+ const invalid = values.filter((platform) => !SUPPORTED_PLATFORMS.includes(platform));
11760
+ if (invalid.length > 0) {
11761
+ throw new Error(`Unsupported platforms: ${invalid.join(", ")}`);
11762
+ }
11763
+ return values;
11764
+ }
11765
+ function normalizeStatus(value) {
11766
+ const status = normalizeRequiredString(value, "status").toLowerCase();
11767
+ if (!SUPPORTED_STATUSES.includes(status)) {
11768
+ throw new Error(`Unsupported status: ${status}`);
11769
+ }
11770
+ return status;
11771
+ }
11772
+ function parseCreateIdeaInput(args) {
11773
+ const hook = normalizeRequiredString(args.hook, "hook");
11774
+ if (hook.length > 80) {
11775
+ throw new Error(`Idea hook must be 80 characters or fewer: ${hook}`);
10890
11776
  }
10891
11777
  return {
10892
- id: assertKebabCaseId(args.id),
10893
- topic: args.topic.trim(),
10894
- hook: args.hook.trim(),
10895
- audience: args.audience.trim(),
10896
- keyTakeaway: args.keyTakeaway.trim(),
10897
- talkingPoints: args.talkingPoints.map((point) => point.trim()).filter((point) => point.length > 0),
11778
+ topic: normalizeRequiredString(args.topic, "topic"),
11779
+ hook,
11780
+ audience: normalizeRequiredString(args.audience, "audience"),
11781
+ keyTakeaway: normalizeRequiredString(args.keyTakeaway, "keyTakeaway"),
11782
+ talkingPoints: normalizeStringList(args.talkingPoints, "talkingPoints"),
10898
11783
  platforms: normalizePlatforms(args.platforms),
10899
- status: "draft",
10900
- tags: args.tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0),
10901
- trendContext: args.trendContext?.trim() || void 0,
10902
- createdAt: now,
10903
- updatedAt: now,
10904
- publishBy
11784
+ tags: normalizeStringList(args.tags, "tags"),
11785
+ publishBy: normalizePublishBy(args.publishBy),
11786
+ trendContext: normalizeOptionalString(args.trendContext, "trendContext")
10905
11787
  };
10906
11788
  }
11789
+ function parseIdeaFilters(args) {
11790
+ const filters = {};
11791
+ if (hasField(args, "status") && args.status !== void 0) {
11792
+ filters.status = normalizeStatus(args.status);
11793
+ }
11794
+ if (hasField(args, "platform") && args.platform !== void 0) {
11795
+ filters.platform = normalizePlatforms([args.platform].flat())[0];
11796
+ }
11797
+ if (hasField(args, "tag") && args.tag !== void 0) {
11798
+ filters.tag = normalizeRequiredString(args.tag, "tag");
11799
+ }
11800
+ if (hasField(args, "priority") && args.priority !== void 0) {
11801
+ const priority = normalizeRequiredString(args.priority, "priority").toLowerCase();
11802
+ if (!SUPPORTED_PRIORITIES.includes(priority)) {
11803
+ throw new Error(`Unsupported priority: ${priority}`);
11804
+ }
11805
+ filters.priority = priority;
11806
+ }
11807
+ if (hasField(args, "limit") && args.limit !== void 0) {
11808
+ if (typeof args.limit !== "number" || !Number.isInteger(args.limit) || args.limit <= 0) {
11809
+ throw new Error("Invalid limit: expected a positive integer");
11810
+ }
11811
+ filters.limit = args.limit;
11812
+ }
11813
+ return filters;
11814
+ }
11815
+ function extractIdeaUpdates(source) {
11816
+ const updates = {};
11817
+ if (hasField(source, "topic")) {
11818
+ updates.topic = normalizeRequiredString(source.topic, "updates.topic");
11819
+ }
11820
+ if (hasField(source, "hook")) {
11821
+ const hook = normalizeRequiredString(source.hook, "updates.hook");
11822
+ if (hook.length > 80) {
11823
+ throw new Error(`Idea hook must be 80 characters or fewer: ${hook}`);
11824
+ }
11825
+ updates.hook = hook;
11826
+ }
11827
+ if (hasField(source, "audience")) {
11828
+ updates.audience = normalizeRequiredString(source.audience, "updates.audience");
11829
+ }
11830
+ if (hasField(source, "keyTakeaway")) {
11831
+ updates.keyTakeaway = normalizeRequiredString(source.keyTakeaway, "updates.keyTakeaway");
11832
+ }
11833
+ if (hasField(source, "talkingPoints")) {
11834
+ updates.talkingPoints = normalizeStringList(source.talkingPoints, "updates.talkingPoints");
11835
+ }
11836
+ if (hasField(source, "platforms")) {
11837
+ updates.platforms = normalizePlatforms(source.platforms);
11838
+ }
11839
+ if (hasField(source, "tags")) {
11840
+ updates.tags = normalizeStringList(source.tags, "updates.tags");
11841
+ }
11842
+ if (hasField(source, "publishBy")) {
11843
+ updates.publishBy = normalizePublishBy(source.publishBy, "updates.publishBy");
11844
+ }
11845
+ if (hasField(source, "trendContext")) {
11846
+ updates.trendContext = normalizeOptionalString(source.trendContext, "updates.trendContext");
11847
+ }
11848
+ if (hasField(source, "status")) {
11849
+ updates.status = normalizeStatus(source.status);
11850
+ }
11851
+ return updates;
11852
+ }
11853
+ function parseUpdateIdeaArgs(args) {
11854
+ if (!isRecord(args.updates)) {
11855
+ throw new Error("Invalid update_idea arguments: updates must be an object");
11856
+ }
11857
+ return {
11858
+ issueNumber: normalizeIssueNumber(args.issueNumber),
11859
+ updates: extractIdeaUpdates(args.updates)
11860
+ };
11861
+ }
11862
+ function parseOrganizeIdeasArgs(args) {
11863
+ const { items } = args;
11864
+ if (!Array.isArray(items)) {
11865
+ throw new Error("Invalid organize_ideas arguments: items must be an array");
11866
+ }
11867
+ return items.map((item, index) => {
11868
+ if (!isRecord(item)) {
11869
+ throw new Error(`Invalid organize_ideas item at index ${index}`);
11870
+ }
11871
+ if (item.updates !== void 0 && !isRecord(item.updates)) {
11872
+ throw new Error(`Invalid organize_ideas item at index ${index}: updates must be an object`);
11873
+ }
11874
+ return {
11875
+ issueNumber: normalizeIssueNumber(item.issueNumber),
11876
+ updates: item.updates ? extractIdeaUpdates(item.updates) : void 0,
11877
+ includeRelated: item.includeRelated === void 0 ? true : Boolean(item.includeRelated)
11878
+ };
11879
+ });
11880
+ }
11881
+ function summarizeLinkedIssues(ideas) {
11882
+ return ideas.map((idea) => ({
11883
+ issueNumber: idea.issueNumber,
11884
+ issueUrl: idea.issueUrl,
11885
+ topic: idea.topic,
11886
+ tags: idea.tags
11887
+ }));
11888
+ }
10907
11889
  var IdeationAgent = class extends BaseAgent {
10908
11890
  brandContext;
10909
11891
  existingIdeas;
10910
- ideasDir;
11892
+ ideaRepo;
10911
11893
  targetCount;
10912
11894
  generatedIdeas = [];
10913
11895
  finalized = false;
@@ -10915,7 +11897,7 @@ var IdeationAgent = class extends BaseAgent {
10915
11897
  super("IdeationAgent", systemPrompt, getProvider2(), model ?? getModelForAgent("IdeationAgent"));
10916
11898
  this.brandContext = context.brandContext;
10917
11899
  this.existingIdeas = [...context.existingIdeas];
10918
- this.ideasDir = context.ideasDir;
11900
+ this.ideaRepo = context.ideaRepo;
10919
11901
  this.targetCount = context.targetCount;
10920
11902
  }
10921
11903
  resetForRetry() {
@@ -10966,20 +11948,25 @@ var IdeationAgent = class extends BaseAgent {
10966
11948
  },
10967
11949
  {
10968
11950
  name: "get_past_ideas",
10969
- description: "Return the current idea bank to help avoid duplicate ideas.",
11951
+ description: "List GitHub-backed ideas with optional status, platform, tag, priority, or limit filters.",
10970
11952
  parameters: {
10971
11953
  type: "object",
10972
- properties: {}
11954
+ properties: {
11955
+ status: { type: "string", enum: [...SUPPORTED_STATUSES] },
11956
+ platform: { type: "string", enum: [...SUPPORTED_PLATFORMS] },
11957
+ tag: { type: "string" },
11958
+ priority: { type: "string", enum: [...SUPPORTED_PRIORITIES] },
11959
+ limit: { type: "integer", minimum: 1 }
11960
+ }
10973
11961
  },
10974
11962
  handler: async (args) => this.handleToolCall("get_past_ideas", args)
10975
11963
  },
10976
11964
  {
10977
11965
  name: "create_idea",
10978
- description: "Create a new draft content idea and persist it to the idea bank.",
11966
+ description: `Create a new draft GitHub Issue in ${this.ideaRepo} using the full idea schema.`,
10979
11967
  parameters: {
10980
11968
  type: "object",
10981
11969
  properties: {
10982
- id: { type: "string", description: "Kebab-case idea identifier" },
10983
11970
  topic: { type: "string", description: "Main topic or title" },
10984
11971
  hook: { type: "string", description: "Attention-grabbing hook (80 chars max)" },
10985
11972
  audience: { type: "string", description: "Target audience" },
@@ -11011,10 +11998,98 @@ var IdeationAgent = class extends BaseAgent {
11011
11998
  description: "Why this idea is timely right now"
11012
11999
  }
11013
12000
  },
11014
- required: ["id", "topic", "hook", "audience", "keyTakeaway", "talkingPoints", "platforms", "tags", "publishBy"]
12001
+ required: ["topic", "hook", "audience", "keyTakeaway", "talkingPoints", "platforms", "tags", "publishBy"]
11015
12002
  },
11016
12003
  handler: async (args) => this.handleToolCall("create_idea", args)
11017
12004
  },
12005
+ {
12006
+ name: "search_ideas",
12007
+ description: "Search GitHub-backed ideas with full-text search across issue content.",
12008
+ parameters: {
12009
+ type: "object",
12010
+ properties: {
12011
+ query: { type: "string", description: "Full-text search query" }
12012
+ },
12013
+ required: ["query"]
12014
+ },
12015
+ handler: async (args) => this.handleToolCall("search_ideas", args)
12016
+ },
12017
+ {
12018
+ name: "find_related_ideas",
12019
+ description: "Find related ideas for an issue by looking up the issue and matching similar tagged GitHub ideas.",
12020
+ parameters: {
12021
+ type: "object",
12022
+ properties: {
12023
+ issueNumber: { type: "integer", description: "GitHub issue number for the idea" }
12024
+ },
12025
+ required: ["issueNumber"]
12026
+ },
12027
+ handler: async (args) => this.handleToolCall("find_related_ideas", args)
12028
+ },
12029
+ {
12030
+ name: "update_idea",
12031
+ description: "Update an existing GitHub idea issue: refine copy, adjust labels, or change lifecycle status.",
12032
+ parameters: {
12033
+ type: "object",
12034
+ properties: {
12035
+ issueNumber: { type: "integer", description: "GitHub issue number for the idea" },
12036
+ updates: {
12037
+ type: "object",
12038
+ properties: {
12039
+ topic: { type: "string" },
12040
+ hook: { type: "string" },
12041
+ audience: { type: "string" },
12042
+ keyTakeaway: { type: "string" },
12043
+ talkingPoints: { type: "array", items: { type: "string" } },
12044
+ platforms: { type: "array", items: { type: "string", enum: [...SUPPORTED_PLATFORMS] } },
12045
+ tags: { type: "array", items: { type: "string" } },
12046
+ publishBy: { type: "string" },
12047
+ trendContext: { type: "string" },
12048
+ status: { type: "string", enum: [...SUPPORTED_STATUSES] }
12049
+ }
12050
+ }
12051
+ },
12052
+ required: ["issueNumber", "updates"]
12053
+ },
12054
+ handler: async (args) => this.handleToolCall("update_idea", args)
12055
+ },
12056
+ {
12057
+ name: "organize_ideas",
12058
+ description: "Batch update GitHub idea issue labels/statuses and return related issue links for clustering.",
12059
+ parameters: {
12060
+ type: "object",
12061
+ properties: {
12062
+ items: {
12063
+ type: "array",
12064
+ items: {
12065
+ type: "object",
12066
+ properties: {
12067
+ issueNumber: { type: "integer" },
12068
+ updates: {
12069
+ type: "object",
12070
+ properties: {
12071
+ topic: { type: "string" },
12072
+ hook: { type: "string" },
12073
+ audience: { type: "string" },
12074
+ keyTakeaway: { type: "string" },
12075
+ talkingPoints: { type: "array", items: { type: "string" } },
12076
+ platforms: { type: "array", items: { type: "string", enum: [...SUPPORTED_PLATFORMS] } },
12077
+ tags: { type: "array", items: { type: "string" } },
12078
+ publishBy: { type: "string" },
12079
+ trendContext: { type: "string" },
12080
+ status: { type: "string", enum: [...SUPPORTED_STATUSES] }
12081
+ }
12082
+ },
12083
+ includeRelated: { type: "boolean" }
12084
+ },
12085
+ required: ["issueNumber"]
12086
+ }
12087
+ }
12088
+ },
12089
+ required: ["items"]
12090
+ },
12091
+ handler: async (args) => this.handleToolCall("organize_ideas", args)
12092
+ },
11018
12093
  {
11019
12094
  name: "finalize_ideas",
11020
12095
  description: "Signal that idea generation is complete.",
@@ -11030,16 +12105,18 @@ var IdeationAgent = class extends BaseAgent {
11030
12105
  switch (toolName) {
11031
12106
  case "get_brand_context":
11032
12107
  return this.brandContext ?? await Promise.resolve(getBrandConfig());
11033
- case "get_past_ideas": {
11034
- const ideas = await readIdeaBank(this.ideasDir);
11035
- return ideas.map((idea) => ({
11036
- id: idea.id,
11037
- topic: idea.topic,
11038
- status: idea.status
11039
- }));
11040
- }
12108
+ case "get_past_ideas":
12109
+ return await listIdeas(parseIdeaFilters(args));
11041
12110
  case "create_idea":
11042
- return this.handleCreateIdea(args);
12111
+ return await this.handleCreateIdea(args);
12112
+ case "search_ideas":
12113
+ return await searchIdeas(normalizeRequiredString(args.query, "query"));
12114
+ case "find_related_ideas":
12115
+ return await this.handleFindRelatedIdeas(args);
12116
+ case "update_idea":
12117
+ return await this.handleUpdateIdea(args);
12118
+ case "organize_ideas":
12119
+ return await this.handleOrganizeIdeas(args);
11043
12120
  case "finalize_ideas":
11044
12121
  this.finalized = true;
11045
12122
  return { success: true, count: this.generatedIdeas.length };
@@ -11051,43 +12128,70 @@ var IdeationAgent = class extends BaseAgent {
11051
12128
  if (this.generatedIdeas.length >= this.targetCount) {
11052
12129
  throw new Error(`Target idea count already reached (${this.targetCount})`);
11053
12130
  }
11054
- const createArgs = this.parseCreateIdeaArgs(args);
11055
- const idea = buildIdea(createArgs);
11056
- const duplicateTopic = this.findDuplicateTopic(idea.topic);
12131
+ const input = parseCreateIdeaInput(args);
12132
+ const duplicateTopic = this.findDuplicateTopic(input.topic);
11057
12133
  if (duplicateTopic) {
11058
12134
  throw new Error(`Duplicate idea topic detected: ${duplicateTopic}`);
11059
12135
  }
11060
- const duplicateId = this.findDuplicateId(idea.id);
11061
- if (duplicateId) {
11062
- throw new Error(`Duplicate idea ID detected: ${duplicateId}`);
11063
- }
11064
- await writeIdea(idea, this.ideasDir);
11065
- this.generatedIdeas.push(idea);
11066
- logger_default.info(`[IdeationAgent] Created idea ${idea.id}: ${idea.topic}`);
12136
+ const idea = await createIdea(input);
12137
+ this.upsertIdea(this.existingIdeas, idea);
12138
+ this.upsertIdea(this.generatedIdeas, idea);
12139
+ logger_default.info(`[IdeationAgent] Created GitHub idea #${idea.issueNumber}: ${idea.topic}`);
11067
12140
  return { success: true, idea };
11068
12141
  }
11069
- parseCreateIdeaArgs(args) {
11070
- const { id, topic, hook, audience, keyTakeaway, talkingPoints, platforms, tags, publishBy, trendContext } = args;
11071
- if (typeof id !== "string" || typeof topic !== "string" || typeof hook !== "string" || typeof audience !== "string" || typeof keyTakeaway !== "string" || !isStringArray2(talkingPoints) || !isStringArray2(platforms) || !isStringArray2(tags) || typeof publishBy !== "string" || trendContext !== void 0 && typeof trendContext !== "string") {
11072
- throw new Error("Invalid create_idea arguments");
12142
+ async handleFindRelatedIdeas(args) {
12143
+ const issueNumber = normalizeIssueNumber(args.issueNumber);
12144
+ const idea = await getIdea(issueNumber);
12145
+ if (!idea) {
12146
+ throw new Error(`Idea #${issueNumber} was not found in ${this.ideaRepo}`);
11073
12147
  }
12148
+ return await findRelatedIdeas(idea);
12149
+ }
12150
+ async handleUpdateIdea(args) {
12151
+ const { issueNumber, updates } = parseUpdateIdeaArgs(args);
12152
+ const idea = await updateIdea(issueNumber, updates);
12153
+ this.upsertIdea(this.existingIdeas, idea);
12154
+ this.syncGeneratedIdea(idea);
12155
+ logger_default.info(`[IdeationAgent] Updated GitHub idea #${idea.issueNumber}: ${idea.topic}`);
12156
+ return { success: true, idea };
12157
+ }
12158
+ async handleOrganizeIdeas(args) {
12159
+ const items = parseOrganizeIdeasArgs(args);
12160
+ const organizedItems = await Promise.all(
12161
+ items.map(async (item) => {
12162
+ const currentIdea = await getIdea(item.issueNumber);
12163
+ if (!currentIdea) {
12164
+ throw new Error(`Idea #${item.issueNumber} was not found in ${this.ideaRepo}`);
12165
+ }
12166
+ const nextIdea = item.updates ? await updateIdea(item.issueNumber, item.updates) : currentIdea;
12167
+ this.upsertIdea(this.existingIdeas, nextIdea);
12168
+ this.syncGeneratedIdea(nextIdea);
12169
+ const relatedIdeas = item.includeRelated ? await findRelatedIdeas(nextIdea) : [];
12170
+ return {
12171
+ issueNumber: nextIdea.issueNumber,
12172
+ idea: nextIdea,
12173
+ linkedIssues: summarizeLinkedIssues(relatedIdeas)
12174
+ };
12175
+ })
12176
+ );
11074
12177
  return {
11075
- id,
11076
- topic,
11077
- hook,
11078
- audience,
11079
- keyTakeaway,
11080
- talkingPoints,
11081
- platforms,
11082
- tags,
11083
- publishBy,
11084
- trendContext
12178
+ success: true,
12179
+ items: organizedItems
11085
12180
  };
11086
12181
  }
11087
- findDuplicateId(id) {
11088
- const normalizedId = id.trim().toLowerCase();
11089
- const existing = [...this.existingIdeas, ...this.generatedIdeas].find((idea) => idea.id.trim().toLowerCase() === normalizedId);
11090
- return existing?.id;
12182
+ upsertIdea(collection, nextIdea) {
12183
+ const existingIndex = collection.findIndex((idea) => idea.issueNumber === nextIdea.issueNumber);
12184
+ if (existingIndex === -1) {
12185
+ collection.push(nextIdea);
12186
+ return;
12187
+ }
12188
+ collection.splice(existingIndex, 1, nextIdea);
12189
+ }
12190
+ syncGeneratedIdea(nextIdea) {
12191
+ const existingIndex = this.generatedIdeas.findIndex((idea) => idea.issueNumber === nextIdea.issueNumber);
12192
+ if (existingIndex !== -1) {
12193
+ this.generatedIdeas.splice(existingIndex, 1, nextIdea);
12194
+ }
11091
12195
  }
11092
12196
  findDuplicateTopic(topic) {
11093
12197
  const normalizedTopic = topic.trim().toLowerCase();
@@ -11110,12 +12214,12 @@ async function generateIdeas(options = {}) {
11110
12214
  config2.BRAND_PATH = options.brandPath;
11111
12215
  }
11112
12216
  const brandContext = await loadBrandContext(options.brandPath);
11113
- const existingIdeas = await readIdeaBank(options.ideasDir);
11114
- const systemPrompt = buildSystemPrompt3(brandContext, existingIdeas, seedTopics, count);
12217
+ const existingIdeas = await listIdeas({ limit: DEFAULT_EXISTING_IDEA_LIMIT });
12218
+ const systemPrompt = buildSystemPrompt3(brandContext, existingIdeas, seedTopics, count, config2.IDEAS_REPO);
11115
12219
  const agent = new IdeationAgent(systemPrompt, {
11116
12220
  brandContext,
11117
12221
  existingIdeas,
11118
- ideasDir: options.ideasDir,
12222
+ ideaRepo: config2.IDEAS_REPO,
11119
12223
  targetCount: count
11120
12224
  });
11121
12225
  try {
@@ -11161,6 +12265,7 @@ function createScheduleAgent(...args) {
11161
12265
  }
11162
12266
 
11163
12267
  // src/L6-pipeline/pipeline.ts
12268
+ init_types();
11164
12269
  async function runStage(stageName, fn, stageResults) {
11165
12270
  const start = Date.now();
11166
12271
  try {
@@ -12048,7 +13153,7 @@ Type \x1B[33mexit\x1B[0m or \x1B[33mquit\x1B[0m to leave. Press Ctrl+C to stop.
12048
13153
 
12049
13154
  // src/L7-app/commands/ideate.ts
12050
13155
  init_environment();
12051
- init_ideaStore();
13156
+ init_ideaService();
12052
13157
 
12053
13158
  // src/L6-pipeline/ideation.ts
12054
13159
  function generateIdeas3(...args) {
@@ -12059,8 +13164,8 @@ function generateIdeas3(...args) {
12059
13164
  async function runIdeate(options = {}) {
12060
13165
  initConfig();
12061
13166
  if (options.list) {
12062
- const ideas2 = await readIdeaBank(options.output);
12063
- const filtered = options.status ? ideas2.filter((i) => i.status === options.status) : ideas2;
13167
+ const ideas2 = await listIdeas();
13168
+ const filtered = options.status ? ideas2.filter((idea) => idea.status === options.status) : ideas2;
12064
13169
  if (filtered.length === 0) {
12065
13170
  console.log("No ideas found.");
12066
13171
  if (options.status) {
@@ -12110,9 +13215,9 @@ ${filtered.length} idea(s) total`);
12110
13215
  console.log(` Status: ${idea.status}`);
12111
13216
  console.log("");
12112
13217
  }
12113
- console.log("Ideas saved to ./ideas/ directory.");
13218
+ console.log("Ideas saved to the GitHub-backed idea service.");
12114
13219
  console.log("Use `vidpipe ideate --list` to view all ideas.");
12115
- console.log("Use `vidpipe process video.mp4 --ideas <id1>,<id2>` to link ideas to a recording.");
13220
+ console.log("Use `vidpipe process video.mp4 --ideas <issueNumber1>,<issueNumber2>` to link ideas to a recording.");
12116
13221
  }
12117
13222
 
12118
13223
  // src/L1-infra/http/http.ts
@@ -12123,14 +13228,16 @@ import { Router } from "express";
12123
13228
  init_paths();
12124
13229
 
12125
13230
  // src/L7-app/review/routes.ts
12126
- init_ideaService();
13231
+ init_ideaService2();
13232
+ init_types();
12127
13233
  init_configLogger();
12128
13234
 
12129
13235
  // src/L7-app/review/approvalQueue.ts
12130
13236
  init_fileSystem();
12131
- init_ideaService();
13237
+ init_ideaService2();
12132
13238
 
12133
13239
  // src/L3-services/socialPosting/accountMapping.ts
13240
+ init_types();
12134
13241
  init_configLogger();
12135
13242
  init_fileSystem();
12136
13243
  init_paths();
@@ -12234,6 +13341,7 @@ async function getAccountId(platform) {
12234
13341
  }
12235
13342
 
12236
13343
  // src/L7-app/review/approvalQueue.ts
13344
+ init_types();
12237
13345
  init_configLogger();
12238
13346
  var queue = [];
12239
13347
  var processing = false;
@@ -12273,20 +13381,32 @@ async function processApprovalBatch(itemIds) {
12273
13381
  itemIds.map(async (id) => ({ id, item: await getItem(id) }))
12274
13382
  );
12275
13383
  const itemMap = new Map(loadedItems.map(({ id, item }) => [id, item]));
12276
- const enriched = await Promise.all(
12277
- loadedItems.map(async ({ id, item }) => {
12278
- if (!item?.metadata.ideaIds?.length) {
12279
- return { id, publishBy: null, hasIdeas: false };
13384
+ const allIdeaIds = /* @__PURE__ */ new Set();
13385
+ for (const { item } of loadedItems) {
13386
+ if (item?.metadata.ideaIds?.length) {
13387
+ for (const ideaId of item.metadata.ideaIds) {
13388
+ allIdeaIds.add(ideaId);
12280
13389
  }
12281
- try {
12282
- const ideas = await getIdeasByIds(item.metadata.ideaIds);
12283
- const dates = ideas.map((idea) => idea.publishBy).filter((publishBy) => Boolean(publishBy)).sort();
12284
- return { id, publishBy: dates[0] ?? null, hasIdeas: true };
12285
- } catch {
12286
- return { id, publishBy: null, hasIdeas: true };
13390
+ }
13391
+ }
13392
+ let ideaMap = /* @__PURE__ */ new Map();
13393
+ if (allIdeaIds.size > 0) {
13394
+ try {
13395
+ const allIdeas = await getIdeasByIds([...allIdeaIds]);
13396
+ for (const idea of allIdeas) {
13397
+ ideaMap.set(idea.id, idea);
13398
+ ideaMap.set(String(idea.issueNumber), idea);
12287
13399
  }
12288
- })
12289
- );
13400
+ } catch {
13401
+ }
13402
+ }
13403
+ const enriched = loadedItems.map(({ id, item }) => {
13404
+ if (!item?.metadata.ideaIds?.length) {
13405
+ return { id, publishBy: null, hasIdeas: false };
13406
+ }
13407
+ const dates = item.metadata.ideaIds.map((ideaId) => ideaMap.get(ideaId)?.publishBy).filter((publishBy) => Boolean(publishBy)).sort();
13408
+ return { id, publishBy: dates[0] ?? null, hasIdeas: true };
13409
+ });
12290
13410
  const now = Date.now();
12291
13411
  const sevenDays = 7 * 24 * 60 * 60 * 1e3;
12292
13412
  enriched.sort((a, b) => {
@@ -12424,7 +13544,31 @@ async function enrichQueueItem(item) {
12424
13544
  };
12425
13545
  }
12426
13546
  async function enrichQueueItems(items) {
12427
- return Promise.all(items.map((item) => enrichQueueItem(item)));
13547
+ const allIdeaIds = /* @__PURE__ */ new Set();
13548
+ for (const item of items) {
13549
+ if (item.metadata.ideaIds?.length) {
13550
+ for (const ideaId of item.metadata.ideaIds) {
13551
+ allIdeaIds.add(ideaId);
13552
+ }
13553
+ }
13554
+ }
13555
+ let publishByMap = /* @__PURE__ */ new Map();
13556
+ if (allIdeaIds.size > 0) {
13557
+ try {
13558
+ const ideas = await getIdeasByIds([...allIdeaIds]);
13559
+ for (const idea of ideas) {
13560
+ publishByMap.set(idea.id, idea.publishBy);
13561
+ publishByMap.set(String(idea.issueNumber), idea.publishBy);
13562
+ }
13563
+ } catch {
13564
+ }
13565
+ }
13566
+ return items.map((item) => {
13567
+ if (!item.metadata.ideaIds?.length) return { ...item };
13568
+ const dates = item.metadata.ideaIds.map((id) => publishByMap.get(id)).filter((publishBy) => Boolean(publishBy)).sort();
13569
+ const ideaPublishBy = dates[0];
13570
+ return ideaPublishBy ? { ...item, ideaPublishBy } : { ...item };
13571
+ });
12428
13572
  }
12429
13573
  async function enrichGroupedQueueItems(groups) {
12430
13574
  return Promise.all(groups.map(async (group) => ({
@@ -12750,7 +13894,7 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
12750
13894
  logger_default.info(`Output dir: ${config2.OUTPUT_DIR}`);
12751
13895
  let ideas;
12752
13896
  if (opts.ideas) {
12753
- const { getIdeasByIds: getIdeasByIds2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
13897
+ const { getIdeasByIds: getIdeasByIds2 } = await Promise.resolve().then(() => (init_ideaService2(), ideaService_exports2));
12754
13898
  const ideaIds = opts.ideas.split(",").map((id) => id.trim()).filter(Boolean);
12755
13899
  try {
12756
13900
  ideas = await getIdeasByIds2(ideaIds);
@@ -12767,10 +13911,10 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
12767
13911
  await processVideoSafe(resolvedPath, ideas);
12768
13912
  if (ideas && ideas.length > 0) {
12769
13913
  try {
12770
- const { markRecorded: markRecorded2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
13914
+ const { markRecorded: markRecorded3 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
12771
13915
  const slug = resolvedPath.replace(/\\/g, "/").split("/").pop()?.replace(/\.(mp4|mov|webm|avi|mkv)$/i, "") || "";
12772
13916
  for (const idea of ideas) {
12773
- await markRecorded2(idea.id, slug);
13917
+ await markRecorded3(idea.issueNumber, slug);
12774
13918
  }
12775
13919
  logger_default.info(`Marked ${ideas.length} idea(s) as recorded`);
12776
13920
  } catch (err) {